第六章 函数
6.1 函数基础
- 调用运算符:一对圆括号
()
,作用于函数指针 - 函数调用过程:
- 主调函数(calling function)的执行被中断,使用实参初始化对应的形参
- 控制权移交给被调函数,被调函数(called function)开始执行。
- 函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。
局部对象
- 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
- 局部静态对象:
static
类型的局部变量,在程序执行路径第一次经过对象定义语句时进行初始化,直到程序终止才被销毁
函数声明
- 函数声明不需要形参的名字
- 函数三要素:返回类型,函数名,形参类型
6.2 参数传递
- 形参初始化的机理和变量初始化一样
- 形参的顶层
const
被忽略:void func(const int i)
与void func(int i)
具有相同的函数签名 ^f06638- 原因:引用没有顶层const;如果传值,传递的是实参的副本,不会改变实参的值
- 但是前面这个函数体中i是const的,连副本也无法修改
- 形参的顶层
- 两种传参方式:
- 传值参数(值传递,传值调用)
- 传引用参数(引用传递,传引用调用)
- 如果无需改变引用形参的值,最好将其声明为常量引用。
- 不能将const对象、字面值或需要类型转换的对象传递给普通引用形参,但是可以传递给常量引用形参
数组形参
- 数组有两个特殊性质:不允许拷贝数组、使用数组时通常会将其转换为指针
- 数组形参:
1 2 3 4
// 以下几种方式形参等价,编译器只会检查传参类型是否为const int* void func(const int*) void func(const int[]) // 可以看出函数意图是传递数组 void func(const int[10] // 可以提示数组长度
- 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针,因此也要传入数组长度
- 数组引用形参:形参是数组引用,实参要传递相同类型及大小的数组
1 2 3
void func(int (&a)[5]){} int a[5] = {1,2,3,4}; func(a);
- 传递多维数组:数组第二维(及更多维)的大小都是数组类型的一部分,不能省略
1 2
void func(int (*matrix)[10], int size){} // matrix是一个指针,指向int[10]类型 void func(int matrix[][10], int size){} // 等价定义
main处理命令行选项
int main(int argc, char *argv[]) {}
- argc代表参数的个数;argv是一个数组,数组元素是char*(或者说char数组),第一个元素是程序的名字或一个空字符串,
可变形参
- 处理不同数量实参的函数
- 如果所有实参类型相同,可以传递一个
initializer_list
标准库类型 - 如果实参类型不同,可以定义可变参数模板
- 省略符形参:
void func(param_list, ...)
- 一般只用于与C函数交互的接口程序,便于CPP访问某些C代码
- 大多数类类型的对象在传递给省略符形参时都无法正确拷贝
- 省略符形参对应的实参无须类型检查
- 如果所有实参类型相同,可以传递一个
initializer_list
:定义在同名头文件中的模板类型initializer_list
与vector类似,但是它元素永远是常量initializer_list
只能使用列表初始化- 含有
initializer_list
形参的函数也可以有其他形参 - 其他容器使用列表初始化本质上都是采用了
initializer_list
形参的构造函数进行初始化的 - initializer_list 提供的操作:
1 2 3 4 5 6 7
initializer_list<int> lst1, lst2;//默认初始化:空列表 initializer_list<int> initlst{1,2,3,4};//initlast 的元素数量与初始值一样多 lst1(initlst); // 直接初始化,lst1与initlst共享元素(不会复制) lst2(initlst); // 赋值初始化,lst2与initlst共享元素(不会复制) void func(int x, initializer_list<char> lst); func(1, {'a', 'b'});
6.3 返回类型和return语句
- 不要返回局部对象的引用或指针
- 引用返回左值:调用一个返回引用的函数得到左值,其他返回类型得到右值。
- 列表初始化返回值:函数可以返回花括号包围的值的列表,并对函数返回的临时量进行初始化。
vector<int> func() { return {"a", "b", "c"}; }
- 这样可以减少一次拷贝?
- 相关:RVO
- 参考:《程序员的自我修养》P305(声明狼藉的C++返回对象)
- main的返回值:cstdlib头文件定义了两种预处理变量来表示成功(
EXIT_FAILURE
)与失败(EXIT_SUCCESS
)
返回数组指针
- 取别名比较方便
- 可以使用尾置返回类型
|
|
6.4 函数重载
- 语法相关:
- 不允许两个函数除了返回类型以外的其他所有要素都相同,或者说返回值与重载无关
- 因为[[ch06-函数#^f06638|形参的顶层const被忽略]],所以在重载函数中,一个有顶层const形参,另一个重载函数相应参数是普通形参,相当于重复声明,但是可以区分底层const。例子
- 使用:
- 一个函数的形参可能有常量引用和非常量引用两种版本,可以使用const_cast进行类型的转换
1 2 3 4 5
const string& func(const string &s) {/* do something*/} // 底层const string func(string s) { const string& r = func(const_cast<const string&>(s)); return const_cast<string&>(r); }
- 重载和作用域:因为C++中名字查找发生在类型检查之间,所以编译器一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
- 因此,不同的重载版本要定义在同一作用域中,一般是全局作用域
- 一个函数的形参可能有常量引用和非常量引用两种版本,可以使用const_cast进行类型的转换
6.5 特殊用途语言特性
默认实参
- 形参顺序:普通形参,不怎么使用默认值的形参,经常使用默认值的形参
- 设置默认值的形参必须放在没有默认值的形参之后
- 一旦某个形参被赋予(或使用)默认值,那么它之后的形参都必须要有默认值
- 虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
1 2
void screen(int w, int h, char c=' '); // 第一次声明 void screen(int w, int h = 80, char c); // 第二次声明,添加了默认实参
- 默认实参只能出现在函数声明和定义其中一处,通常应该在头文件中的函数声明中指定默认实参。
- 局部变量不能作为函数的默认实参,全局变量和字面值都可以
内联(inline)函数
- 在函数声明和定义中都能使用关键字inline,但是建议只在函数定义时使用。
- 一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和switch语句,否则函数会被编译为普通函数。
constexpr 函数
不是很理解,个人理解是在编译器就能确定返回值的函数
- 内联函数和constexpr函数通常定义在头文件中。
调试帮助
调试帮助:用类似头文件保护的方式,有选择的执行调试代码。即在开发过程中,程序可以包含一些用于调试的代码,当程序发布时,需要先屏蔽掉调试代码。调试帮助通常包含两种预处理功能:assert
和DNEBUG
assert
是一种预处理宏(preprocessor macro):assert(expr);
- 当表达式为假时,assert输出信息并终止程序;如果真,assert什么都不做
- 常用来检查不能发生的条件
NDEBUG
预处理变量:关闭调试状态- 可以使用
#define NDEBUG
来定义NDEBUG
,但很多编译器都提供了命令行选项-D NDEBUG
- 如果定义了
NDEBUG
,则assert什么都不做;默认情况下没有定义NDEBUG
- 几个用于调试的变量名称:
- 可以使用
变量名称 | 内容 |
---|---|
__func__ | 当前函数名称 |
__FILE__ | 当前文件名称 |
__LINE__ | 当前行号 |
__TIME__ | 文件编译时间 |
__DATE__ | 文件编译日期 |
|
|
6.6 函数匹配
函数匹配(或称为重载确定)
- 重载函数匹配的三个步骤:
- 找候选函数:同名函数
- 选可行函数:形参实参数量相等,类型匹配或者能进行转换
- 寻找最佳匹配:实参类型和形参类型越接近,它们匹配越好
- 精确匹配、从数组类型或函数类型转换为对应的指针类型、添加/删除顶层const
- const转换
- 类型提升
- 算数类型转换、指针转换
- 类类型转换
- 如果有若干个匹配,但没有一个最佳匹配时,编译器可能报告二义性调用的信息
6.7 函数指针
- 对于重载函数,函数指针类型必须与重载函数中某一个精确匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
using Func = int(int, int); // 函数类型 typedef int Func2(int, int); // 函数类型,等价于Func using FuncP = int(*)(int, int); // 函数指针类型 typedef int (*FuncP2)(int, int); // 函数指针类型,等价于FuncP int add(int a, int b) {return a + b;} double add(double a, double b) {return a + b;} int op1(int a, int b, int (*f)(int, int) ) {return (*f)(a, b);} int op2(int a, int b, Func* f) {return (*f)(a, b);} // 函数不能做形参 int op3(int a, int b, FuncP fp) {return (*fp)(a, b);} int test() {return 1;} typedef decltype(test) FuncT; // 函数类型 typedef decltype(test) (*FuncTP); // 函数指针类型,decltype返回函数类型,需要在别名类型前加上*表示函数指针 int main(){ Func *f = &add; FuncP g = &add; cout<<op1(1,2,add)<<" "<<op2(1,2,f)<<" "<<op3(1,2,g); }
- 函数指针可以作为形参
- 形参类型可能为函数类型,传入的也可能是函数名,但是最终都是转换为函数指针
- 函数指针可以作为返回值,函数类型不可以作为返回值
|
|
- 复杂例子:假设函数指针类型是
int(*)(int, int)
,数组指针类型是int (*)[5]
1 2
using FuncP = int(*)(int, int); using Arr = int[5];
- 返回值是函数指针,函数形参是数组指针
1 2 3
int ( *f( int(*arr)(int,int) ) ) (int (*)[5]); auto f( int(*arr)[5] ) -> int(*)(int, int); FuncP f(Arr* arr);
- 返回值是函数指针,函数形参是函数指针
1 2 3
int (*f( int(*fp)(int, int) )) (int, int); auto f( int(*fp)(int, int) ) -> int(*)(int, int); FuncP f(FuncP fp);
- 返回值是数组指针,函数形参是函数指针
1 2 3
int ( *f( int(*fp)(int, int) ) )[5]; auto f( int(*fp)(int, int) ) -> int(*)[5]; Arr* f(FuncP fp);
- 返回值是数组指针,函数形参是数组指针
1 2 3
int (*f( int(*arr)[5] ))[5]; auto f( int(*arr)[5] ) -> int(*)[5]; Arr* f(Arr* arr);
- 返回值是函数指针,函数形参是数组指针