第六章 函数
一、函数基础
- 函数定义:包括返回类型、函数名字和0个或者多个形参(parameter)组成的列表和函数体。
- 调用运算符:调用运算符的形式是一对圆括号 (),作用于一个表达式,该表达式是函数或者指向函数的指针。
- 圆括号内是用逗号隔开的实参(argument)列表。
- 函数调用过程:
- 1.主调函数(calling function)的执行被中断。
- 2.被调函数(called function)开始执行。
- 形参和实参:形参和实参的个数和类型必须匹配上。
- 返回类型: void表示函数不返回任何值。函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。
1. 局部对象
- 两个非常重要的概念:
- 名字:名字的作用于是程序文本的一部分,名字在其中可见。
- 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
- 局部变量(local variable):形参和函数体内部定义的变量统称为局部变量。它对函数而言是局部的,对函数外部而言是隐藏的。它的生命周期依赖于定义的方式。
- 在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会被销毁。
- 自动对象:只存在于块执行期间的对象。当块的执行结束后,它的值就变成未定义的了。
- 局部静态对象: static类型的局部变量,在程序的执行路径第一次经过对象定义语句时被创建,直到程序终止才被销毁。它的生命周期贯穿函数调用前后。
2. 函数声明
- 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型。
- 在头文件中进行声明:建议变量和函数在头文件中声明,在源文件中定义。
3. 分离式编译
- 分离式编译: CC a.cc b.cc直接编译生成可执行文件;CC -c a.cc b.cc编译生成对象代码a.o b.o; CC a.o b.o编译生成可执行文件。
二、参数传递
- 形参初始化的机理和变量初始化一样。
- 引用传递(passed by reference):又称传引用调用(called by reference),指形参是引用类型,引用形参是它对应的实参的别名。
- 值传递(passed by value):又称传值调用(called by value),指实参的值是通过拷贝传递给形参。
1. 传值参数
- 当初始化一个非引用类型的变量时,初始值被拷贝给变量。函数对形参做的所有操作都不会影响实参。
- 指针形参:当执行指针拷贝操作时,拷贝的是指针的值。拷贝过后,虽然两个指针是不同的指针,但是由于指针的特性,通过指针可以修改它所指向对象的值。
- 在C中:常常使用指针类型的形参访问函数外部的对象。在C++中:建议使用引用类型的形参代替指针。
2. 传引用参数
- 通过使用引用形参,允许函数改变一个或多个实参的值。
- 引用形参直接关联到绑定的对象,而非对象的副本。所以经常用引用形参来避免不必要的复制。
- 使用引用形参可以用于返回额外的信息。比如得到多个返回值。
- 如果无需改变引用形参的值,最好将其声明为常量引用。
3. const形参和实参
- 形参的顶层const被忽略。void func(const int i); 调用时既可以传入const int也可以传入int。
由于这个特性,所以void func(const int i); 和 void func(int i); 不是重载。
- 可以使用非常量初始化一个底层const对象,但是反过来不行。
- 尽量使用常量引用。
4. 数组形参
- 两个特殊性质:
- 不允许拷贝数组。所以无法以值传递的方式使用数组参数。
- 使用数组时通常会将其转换成指针。所以在为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 以数组作为新参的函数,也要注意数组的实际长度,不能越界。
- 管理数组实参的第一种方法:要求数组本身包含一个结束标记。
void print(const char *cp) {
if(cp){
while(*cp) cout << *cp++;
}
}
- 管理数组实参的第二种方法:使用标注库规范,传递指向数组首元素和尾元素的指针。推荐使用。
void print(const int *beg, const int *en) {
while(beg != en) cout << *beg++;
}
int j[2] = {0, 1, 2423, 4};
print(begin(j), end(j));
- 管理数组实参的第三种方法:专门定义一个表示数组大小的形参。
// const in ia[] <=> const in *ia
void print(const in ia[], size_t size) {
for (size_t i = 0; i < size; ++i) {
cout << ia[i];
}
}
void print(int (*matrix)[10], int rowSize) {
;
}
// 或者
void print(int matrix[] [10], int rowSize) {
;
}
5. main处理命令行选项
- int main(int argc, char **argv) {...}; 第二个参数argv是一个数组,它的元素是指向C风格字符串的指针。第一个参数argc表示数组中字符串的数量。
- 当使用argv中的实参时,一定要记得可选的参数从argv[1]开始,argv[0]保持程序的名字,而非用户的输入。
6. 含有可变形参的函数
- C++11提供了两种主要的方法来解决处理不同数量实参的函数
- 如果所有的实参类型相同,可以传递一个initializer_list的标准库类型。
- 如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。
- initializer_list形参:initializer_list是一种标准库类型,用于表示某种特定的值的数组,它定义在同名的头文件中。它的操作如下表:
操作 |
含义 |
initializer_list lst; |
默认初始化;T类型元素的空列表。 |
initializer_list lst{a,b,c}; |
lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const。 |
lst2(lst) |
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。 |
lst2 = lst |
原始列表和副本共享元素 |
lst.size() |
列表中的元素数量 |
lst.begin() |
返回指向lst中首元素的指针 |
lst.end() |
返回指向lst中微元素下一位置的指针 |
void err_msg(ErrCode e, initializer_list<string> il){
cout << e.msg() << endl;
for(cosnt auto$elem: il)
cout << elem << " ";
cout << endl;
}
if (expected != actual)
err_msg(ErrCode(42), {"funcX", expected, aactual};
else
err_msg(ErrCode(0), {"funcX", "Okay"};
三、返回类型和return语句
1. 无返回值的函数
- 没有返回值的 return语句只能用在返回类型是 void的函数中,返回 void的函数不要求非得有 return语句。它会默认的加上return 0;
2. 有返回值的函数
- return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。
- 值的返回:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 不要返回局部对象的引用或者指针,因为函数结束,局部对象将被释放。
- 引用返回左值:函数的返回类型决定调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型的函数得到右值。
- C++11:列表初始化返回值:函数可以返回花括号包围的值的列表。
- 主函数main的返回值:如果结尾没有return,编译器将隐式地插入一条返回0的return语句。返回0代表执行成功。
3. 返回数组指针
- type(*function(parameter_list))[dimension]
- 使用类型别名:typedef int arrT[10]; 或者 using arrT = int[10;],然后 arrT* func() {...};
- 使用decltype:decltype(odd) *arrPtr(int i) {...};
- C++11:尾置返回类型:在形参列表后面以一个->开始:auto func(int i) -> int(*)[10]
四、函数重载
- 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
- main函数不能重载。
- 重载和const形参
- 一个有顶层const的形参和没有它的函数无法区分。 Record lookup(Phone* const)和 Record lookup(Phone*)无法区分。
- 相反,是否有某个底层const形参可以区分(形参是某种类型的指针或者引用)。 Record lookup(Account)和 Record lookup(const Account)可以区分。
- const_cast和重载
// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
// 重载一个版本:实参不是常量时,得到一个普通的引用。
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
五、特殊用途语言特性
1. 默认实参
- string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
- 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。
2. 内联(inline)函数
- 普通函数的一个潜在缺点:调用函数一般比求等价表达式的值要慢一些。因为调用前要先保存寄存器,并在返回时恢复;还可能需要拷贝实参;程序转向一个新的位置继续执行等;
- inline函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。但是注意,内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
inline const string & shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
// 使用
cout << shorterString(S1, S2) << endl;
// 编译过程中直接展开为类似下面的形式
cout << (s1.size() <= s2.size() ? s1 : s2) << endl;
- inline函数应该在头文件中定义。且只适合短小的函数。
3. constexpr函数
- constexpr函数:是指能用于常量表达式的函数,但是它不一定返回常量表达式。定义时的几个约定:1.函数的返回类型及所有形参的类型都得是字面值类型。2.函数体中必须有且只有一条return语句。
- constexpr int new_sz() {return 42;}
- constexpr函数应该在头文件中定义。
4. 调试帮助
- assert预处理宏(preprocessor macro):assert(expr);
- NEDBUG预处理变量:CC -D NDEBUG main.c可以定义这个变量NDEBUG。
void print(){
#ifndef NDEBUG
cerr << __func__ << "..." << endl;
#endif
}
六、函数匹配
- 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
- 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
- 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
- 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
七、函数指针
- 函数指针指向的是函数而非对象,它指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
// 比较两个string对象的长度
bool lengthCompare(const string& a, const string& b);
// 该函数的类型
bool (const string&, const string&);
// 变成函数指针,只需要将指针替换函数名即可
bool (*pr) (const string&, const string&);
- 使用函数指针:当把函数名作为一个值使用时,该函数自动转换成指针。
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句:取地址符是可选的
- 函数指针形参:
- 形参中使用函数定义或者函数指针定义效果一样。
- 使用类型别名或者decltype。
- 返回指向函数的指针:1.类型别名;2.尾置返回类型。