c++ primer 5th 笔记:第六章
第六章:函数
笔记
1. 通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是一个函数或指向函数的指针。
2. 在c++语言中,名字有作用域,对象有生命周期。
a. 名字的作用域是程序文本的一部分,名字在其中可见。
b. 对象的生命周期是程序执行过程中该对象存在的一段时间。
3. 函数体是一个语句块,块构成一个新的作用域。
4. 形参和函数体内部定义的变量统称为局部变量(local variable)。
5. 局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
6. 函数只能定义一次,但可以声明多次。函数的声明也称作函数原型。
7. 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
8. 在c++语言中,建议使用引用类型的形参替代指针。
9. 使用引用避免拷贝。拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。
10. 如果函数无须改变引用形参的值,最好将其声明为常量引用。
// 不良设计:第一个形参的类型应该是const string& string::size_type find_char(string &s, char c, string::size_type &occurs); // 只能将find_char函数作用于string对象。 find_char("Hello World", 'o', ctr); // 错误 // 尽量使用常量引用
11. 可以使用非常量初始化一个底层cosnt对象,但是反过来不行。
12. 数组的两个特殊性质对我们定义和使用作用在数组上的函数由影响:a. 不允许拷贝数组;b. 使用数组时通常会将其转换成指针。
13. c++语言允许将变量定义成数组的引用,形参也可以是数组的应用。
// 正确:形参是数组的应用,维度是类型的一部分 void print(int (&arr)[10]) // &arr两端的括号必不可少 { for (auto elem : arr) cout << elem << endl; }
14. 传递多维数组。数组的第二维的大小都是数组类型的一部分,不能省略。
// matrix 指向数组的首元素,该数组的元素是由10个整数构成的数组 void print(int (*matrix)[10], int rowSize) { /* ... */} // 再次强调, *matrix两端的括号必不可少 // int *matrix[10]; 10个指针构成的数组 // int (*matrix)[10]; 指向含有10个整数的数组的指针 // 等价定义 void print(int matrix[][10], int rowSize) { /* ... */} // matrix 的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针
15. main:处理命令行选项:
int main(int argc, char *argv[]) // 第二个形参argv是一个一维数组,它的元素是指向C风格字符串的指针 // 等价定义 int main(int argc, char **argv) { ... }
16. C++新标准提供了两种主要方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
17. return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
18. 不要返回局部对象的引用或指针。
// 严重错误:这个函数试图返回局部对象的引用 const string &manip() { string ret; // 以某种方式改变一下ret if (!ret.empty()) return ret; // 错误:返回局部对象的引用 else return "Empty"; // 错误:"Empty"是一个局部临时量 } // 上面的两条return语句都将返回未定义的值,也就是,试图使用manip函数的返回值将 // 发生未定义的行为。对于第一条return语句来说,显然它返回的是局部对象的引用。在 // 第二条return语句中,字符串字面值转换成一个局部临时string对象,对于manip来说 // 该对象和ret一样都是局部的。当函数结束时临时对象占用的空间也就随之释放掉了,所 // 以两条return语句都指向了不再可用的内存空间。
19. 引用返回左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
// 我们能为返回类型是非常量引用的函数 char &get_val(string &str, string::size_type ix) { return str[ix]; } int main() { string s("a value"); cout << s << endl; // 输出a value get_val(s, 0) = 'A'; // 将s[0]的值改为A cout << s << endl; return 0; } // 如果返回类型是常量引用,我们不能给调用的结果赋值
20. C++新标准规定,函数可以返回花括号包围的值得列表,类似于其他返回结果,此处的列表也用来表示返回的临时量进行初始化。
vector<string> process() { // ... // expected 和 actual是string对象 if (expected.empty()) return {}; else if (expected == actual) return { "functionX", "okay"}; else return { "functionX", expected, actual }; }
// 如果函数返回的是类类型,由类本身定义初始化值如何定义
21. 在c++11新标准中可以使用尾置返回类型。
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组 auto func(int i) -> int (*)[10];
22. 使用decltype
注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想返回指针还必须在函数声明时加一个*符号。
int odd[] = {1, 3, 5, 7, 9}; int even[] = {0, 2, 4, 6, 8}; // 返回一个指针,该指针指向含有5个整数的数组 decltype(odd) *arrPtr(int i) { return (i % 2) ? &odd : &even; }
23. 函数匹配(function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做函数确定(overload resolution)。
24. 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
25. 当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用的默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
26. 内联函数可以避免函数调用的开销。将函数指定为内联函数,通常就是将它们在每个调用点上"内联地"展开。另外,内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
27. 把内联函数和constexpr函数放在头文件内,毕竟,编译器要想展开函数仅有函数的声明是不够的,还需要函数的定义。
28. 调试帮助:assert预处理宏。所谓预处理宏其实就是一个预处理变量。 assert的行为依赖于一个名为NDEBUG的预处理变量的状态。我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。
重点知识点总结:
函数重载:
如果同一作用域的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。
一. 重载和const形参:顶层const不影响传入函数对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone); Record lookup(const Phone); // 重复声明了Record lookup(Phone) Record lookup(Phone*); Record lookup(Phone* const); // 重复声明了Record lookup(Phone*)
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同 // 定义了4个独立的重载函数 Record lookup(Account&); // 函数作用于Account的引用 Record lookup(const Account&); // 新函数,作用于常量引用 Record lookup(Account*); // 新函数,作用于指向Account的指针 Record lookup(const Account*); // 新函数,作用于指向常量的指针
分析:上面的例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其他类型,所以我们只能把const对象(或指向const的指针)传递给cosnt形参。相反的,因为非常量可以转换成const,所以上面的四个函数都能作用于非常量或者指向非常量的指针。不过,当我们传递非常量对象或者非常量对象的指针时,编译器会优先选择非常量版本的函数。
二. const_cast和重载
const_cast 在重载函数的情景中最有用。例如:
// 比较两个string对象的长度,返回较短的那个引用 const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } // 上面的返回结果仍然是const stirng的引用,因此我们需要一种新的shorterString函数 // ,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点 string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); } // 这个引用事实上绑定在了某个初始的非常量的实参上,所以,我们可以再将其转换回一个 // 普通的stirng&,这显然是安全的
三. constexpr函数
constexpr函数只能用于常量表达式的函数。该函数的返回值类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
const int new_sz() { return 42; } const int foo = new_sz(); // 正确:foo是一个常量表达式
编译器把对constexpr函数的调用替换成其结果值。
四.函数匹配(p217)(待写)
五.函数指针(p221)(待写)
术语
调用运算符(call operator)、自动对象(automatic object)、局部静态对象(local static object)、
函数原型(function prototype)、引用传递(passed by reference)、值传递(passed by value)、
递归循环(recursion loop)、最佳匹配(best match)、预处理宏(preprocessor marco)、可行函数(viable function)、
二义性调用(ambiguous call)。