第六章 函数
函数是一个命名了的代码块,我们通过调用函数执行相应的代码。
6.1 函数基础
我们通过调用运算符来执行函数,其形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针。调用表达式的类型就是函数的返回类型。
函数的调用完成两项工作:1、用实参初始化函数对应的形参;2、将控制权从主调函数转移给被调函数。
return语句也完成两项工作:1、返回return语句中的值(如有);2、将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果。
实参是形参的初始值,没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。
任意两个形参都不能同名,是否命名形参是可选的,但即使是设置未命名的形参,也不影响调用时提供的实参数量。
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
6.1.1 局部对象
形参和函数体内部定义的变量统称为局部变量。
局部变量的生命周期依赖于定义的方式:
1、自动对象:只存在于块执行期间,形参是一种自动对象。如果自动对象没有显示的初始值,则执行默认初始化。
2、局部静态对象(static关键字):在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被摧毁,在此期间即使对象所在的函数结束执行也不会对它有影响。如果局部静态对象没有显式的初始值,则执行值初始化。
6.1.2 函数声明
函数只能定义一次,但可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
函数声明无须函数体,用一个分号替代即可,函数声明也可以不写形参的名字。
函数声明也被称作函数原型。
函数应该在头文件中声明而在源文件中定义。
6.1.3 分离式编译
分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。
6.2 参数传递
每次函数调用时都会重新创建它的形参,并用传入的实参对形参进行初始化。
如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
引用传递(传引用调用)/ 值传递(传值调用)
6.2.1 传值参数
函数对形参做的所有操作都不会影响实参。
指针形参
拷贝的是指针的值,即指向同一个对象。拷贝后,两个指针是不同的指针,但是可以通过指针修改它所指对象的值。
C++语言中,建议使用引用类型的形参代替指着。
6.2.2 值引用参数
如果形参是引用类型,我们直接传入对象就可以。
使用引用避免拷贝
1、拷贝大的类类型对象或者容器对象比较低效
2、有的类类型不支持拷贝操作
如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值:
1、定义一个新的数据类型
2、使用引用形参
6.2.3 const形参和实参
当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
指针或引用形参与const
如果形参是引用类型,只能用基本类型相同的对象。不能使用字面值、求值结果类型相同的表达式、需要转换的对象或者常量对象,指针类似。
如果形参是常量引用类型,上述四种入参类型都可以使用。
尽量使用常量引用
把函数不会改变的形参定义成普通的引用是一种比较常见的错误:
1、误导函数调用者,即函数可以修改它的实参的值
2、极大地限制函数所能接受的实参类型
6.2.4 数组形参
数组的两个特殊性质:
1、不允许拷贝数组——>无法以值传递的方式使用数组参数
2、使用数组时通常会将其转换成指针——>实际上传递的是指向数组首元素的指针
我们可以把形参写成类似数组的形式:
void print(const int*);
void print(const int[]); //可以看出来,函数的意图是作用于一个数组
void print(const int[10]); //这里的维度表示我们期望数组含有多少个元素,实际不一定
//数组的大小对函数的调用没有影响
一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。
使用标记指定数组长度
要求数组本身包含一个结束标记,适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,如C风格字符串。
使用标准库规范
传递指向数组首元素和尾后元素的指针,可以使用标准库函数begin和end函数提供所需的指针。
显示传递一个表示数组大小的形参
专门定义一个表示数组大小的形参
数组形参和const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
数组引用实参
形参也可以是数组的引用,使用数组引用时,数组的维度会限制使用。
void print(int (&arr) [10]); // 实参必须是含有10个整数的数组
传递多维数组
真正传递的是指向数组首元素的指针,多维数组中,首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
void print(int (*matrix)[10], int rowSize);
void print(int matrix[][10], int rowSize);
对于第二种声明方式,编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内。
6.2.5 main: 处理命令行选项
int main(int argc, char *argv[]);
int main(int argc, char **argv);
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。
argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0.
6.2.6 含有可变形参的函数
为了编写能处理不同数量实参的函数:
1、如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
2、如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板
initializer_list形参
initializer_list是一种模板类型,包含begin和end成员,initializer_list对象中的元素永远都是常量值。
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内,含有initializer_list形参的函数也可以同时拥有其他形参。
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,省略符形参所对应的实参无须类型检查。
6.3 返回类型和return语句
return语句有两种形式:
1、return;
2、return expression;
6.3.1 无返回值函数
没有返回值的return语句只能用在返回类型是void的函数中。这类函数最后一句后面会隐式地执行return,如果想在它的中间位置提前退出,可以使用return语句。
例外:main函数
6.3.2 有返回值的函数
return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的,很多编译器都无法发现此类错误。
值是如何被返回的
返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
通常情况下,返回值将被拷贝到调用点。如果函数返回引用,返回结果不会真正拷贝对象。
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。一旦函数完成,局部对象被释放,指针将指向一个不再有效的内存区域。
一般的来说,函数是可以返回局部变量的。因为函数返回的是局部变量的值,不涉及地址,程序不会出错。准确的来说,函数不能通过返回指向栈内存的指针,返回指向堆内存的指针是可以的。要想返回引用或指针,要么使用局部静态变量,要么在堆上分配内存。
引用返回左值
调用一个返回引用的函数得到左值,其他返回类型得到右值。
列表初始化返回值
函数可以返回花括号包围的值的列表。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。如果函数返回的是内置类型,则花括号包围的列表最多包含一个值。
主函数main的返回值
我们允许main函数没有return语句直接结束,编译器将隐式地插入一条返回0的return语句。
返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。
为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量:EXIT_FAILURE和EXIT_SUCCESS。
递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。
在递归函数中,一定有某条路径是不包含递归调用的,否则函数将不断地调用它自身直到程序栈空间耗尽为止,这叫做递归循环。
6.3.3 返回数组指针
1、使用类型别名
2、不使用类型别名,我们必须牢记被定义的名字后面数组的维度。返回数组指针的函数形式如下
Type (*function(parameter_list)) [dimension]
两端的括号必须存在,就像我们定义一个指向数组的指针时一样。
3、使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。尾置返回类型的形式如下:
auto func(int i) -> int(*)[10];
4、使用decltype,前提是我们知道函数返回的指针将指向哪个数组。
6.4 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
不允许两个函数除了返回类型外其他所有的要素都相同。
重载和const形参
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来,但是底层const可以(指向常量的引用或者指向常量的指针)。
const_cast和重载
可以将实参强制转换成对const的引用,然后再将返回值转换回一个普通的类型,即脱去const。
调用重载函数
这个过程称为函数匹配或函数调用。
在一种情况下,选择函数比较困难,比如当两个重载函数参数数量相同且参数类型可以相互转换时
当调用重载函数时有三种可能的结果:
1、编译器找到一个与实参最佳匹配的函数
2、找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
3、有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用
6.4.1 重载与作用域
重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名。
在C++语言中,名字查找发生在类型检查之前。
6.5 特殊用途语言特性
6.5.1 默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,我们把这个反复出现的值称为函数的默认实参。
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
使用默认实参调用函数
如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。所以在设计含有默认实参的函数时,需要合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
默认实参声明
多次声明一个函数时,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
默认实参初始值
局部变量不能作为默认实参。
只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。用作默认实参的名字在函数声明所在的作用域内解析,但是这些名字的求值过程发生在函数调用时。
6.5.2 内联函数和constexpr函数
调用函数一般比求等价表达式的值要慢一些,因为函数调用有一系列的开销。
将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开(在编译过程中)。在函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数。
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
内联机制用于优化规模较小、流程直接(递归)、频繁调用的函数。
constexpr函数
constexpr函数是指能用于常量表达式的函数。
1、函数的返回类型及所有形参的类型都得是字面值类型(算术类型、引用和指针);
2、函数体中必须有且只有一条return语句。也可以包含其他语句,只要这些语句在运行时不执行任何操作就行,例如空语句、类型别名以及using声明。
编译器把对constexpr函数的调用替换成其结果值,constexpr函数被隐式地指定为内联函数。
我们允许constexpr函数的返回值并非一个常量,如果函数用在需要常量表达式的上下文时,由编译器负责检查函数的结果是否符合要求。如果结果不是常量表达式,编译器将发出错误信息。
内联函数和constexpr函数可以在程序中多次定义,毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。
内联函数和constexpr函数通常定义在头文件中。
6.5.3 调试帮助
assert预处理宏
assert是一种预处理宏,所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数,assert宏使用一个表达式作为它的条件。
assert宏定义在cassert头文件中,预处理名字由预处理器而非编译器管理,我们可以直接使用预处理名字而无须提供using声明。宏名字在程序内必须唯一。
NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,我们可以使用一个#define语句定义NDEBUG,同时很多编译器都提供了一个命令行选项使我们可以定义预处理变量(NDEBUG)。
我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
也可以使用NDEBUG编写自己的条件调试代码,如:
#ifndef NDEBUG
cerr << ... << endl;
#endif
编译器提供的名字:
__fun__:局部静态变量,函数名字
__FILE__:文件名,字符串字面值
__LINE__:当前行号,整型字面值
__TIME__:编译时间,字符串字面值
__DATE__:编译日期,字符串字面值
6.6 函数匹配
1、选定本次调用对应的重载函数集(候选函数):一是与被调用的函数同名,二是其声明在调用点可见;
2、选出能被这组实参调用的函数(可行函数):一是数量相等,二是类型相同或者可以转换得到;
3、从可行函数中选择与本次调用最匹配的函数,实参类型与形参类型越接近,它们匹配的越好。
含有多个形参的函数匹配
如果有且只有一个函数满足下列条件,则匹配成功:
1、该函数每个实参的匹配都不劣于其他可行函数需要的匹配
2、至少有一个实参的匹配优于其他可行函数提供的匹配
如果没有任何一个函数脱颖而出,编译器将报告二义性调用的信息。
6.6.1 实参类型转换
1、精确匹配:类型相同、数组或函数类型转换成对应的指针类型、向实参删除或添加顶层const
2、通过const转换实现的匹配,指向非常量类型的指针或引用指向常量
3、通过整型提升实现的匹配
4、通过算数类型转换或指针转换实现的匹配
5、通过类类型转换实现的匹配
6.7 函数指针
函数指针指向的是函数而非对象,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可,如:
bool (*pf)(const string&, const string&); // *pf两端的括号必不可少
使用函数指针
当我们把函数名作为一个值使用时,该函数自动地转换成指针,取地址符是可选的。
还能直接使用指向函数的指针调用该函数,无须提前解引用指针,解引用符是可选的。
在指向不同函数类型的指针间不存在转换规则,可以赋nullptr或值为0的整型常量表达式。
重载函数的指针
指针类型必须与重载函数中的某一个精确匹配
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。形参可以是函数类型,也可以显式地将形参定义成指向函数的指针,这样两个声明语句声明的是同一个函数。
直接把函数作为实参使用,它会自动转换成指针。
为了简化使用了函数指针的代码,可以使用类型别名:区分定义函数类型和指向函数的指针类型。
decltype会返回函数类型,此时不会将函数类型自动转换成指针类型,数组也是如此。如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程。
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是能返回指向函数的指针。
但和函数类型的形参不一样,返回类型不会自动地转换成指针,我们必须显式地将返回类型指定为指针。
与返回数组指针的函数一样,可以使用尾置返回类型的方式。
术语表:
实参:函数调用时提供的值,用于初始化函数的形参。
形参:在函数的形参列表中声明的局部变量,用实参初始化形参。
函数原型:函数的声明,包含函数名字、返回类型和形参类型。要想调用函数,在调用点之前必须声明该函数的原型。
链接:是一个编译过程,负责把若干对象文件链接起来形成可执行程序。
对象文件:编译器根据给定的源文件生成的保存对象代码的文件。一个或多个对象文件经过链接生成可执行文件。
对象生命周期:当main函数结束时销毁全局对象和局部静态对象。
值传递:非引用类型的形参实际上是相应实参值的一个副本。如果某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。