Chapter 6 函数
6.1 函数基础
6.1.1 局部变量
自动对象: 只存在于块执行期间的对象称为自动对象(automatic object)。形参 是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
局部静态对象: 局部静态对象(local static object)在程序执行路经第一次经过对象语句时初始化,并且直到程序终止才被销毁。
6.1.2 函数声明
如果一个函数永远不会被用到,那么它可以只有声明没有定义。函数声明无须函数体,用一个分号代替即可。
同时函数的声明不包含函数体,所以也就无须形参的名字。
函数三要素(返回类型、函数名、形参类型)描述了函数接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)。
6.1.3 分离式编译
请参见CMake的使用教程。
6.2 参数传递
NOTES:每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
当形参是引用类型时,我们说它对应的实参被引用传递,或者函数被引用调用。
当形参是被拷贝给形参时,形参和实参是两个相互独立的对象。这样的实参被值传递或者函数被传值调用。
指针形参:指针的行为和其他非引用类型一样,拷贝的是指针的值。拷贝之后,两个指针是不同的指针,指向同一对象,通过指针可以修改它所指对象的值。
在C++中,建议使用引用类型的形参代替指针。
6.2.2 传引用参数
使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)不支持拷贝操作,函数只能通过引用形参访问该类型对象。如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用形参返回额外信息:给函数传入额外的引用实参,来达到隐式的返回额外参数的目的。
6.2.3 const形参和实参
指针或引用形参与const:形参的初始化方式和变量的初始化方式是一样的,所以可以使用非常量初始化一个底层const
对象,但是反过来不行。同时一个普通的引用必须用同类型的对象初始化。
6.2.4 数组形参
数组的两个特殊性质对定义和使用作用在数组的函数有影响:
- 不允许拷贝数组;
- 使用数组时通常会将其转化为指针;
1 | void print(const int*); |
以上三种函数都是等价的。因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
使用标记指定数组长度:要求数组本身包含一个结束标记,比如C风格字符串。C风格字符串存储的字符串数组中,在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止:
1 | void print(const char *cp) |
该方法仅适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。
使用标准库规范: 传递指向数组首元素和尾后元素的指针,该方法收到了标准库技术的启发。
1 | void print(const int *beg, const int *end) |
显式传递一个表示数组大小的形参:专门指定一个表示数组大小的形参,在C程序和过去的C++程序中常常采用这种方法。
1 | // const int ia[] 等价于const int* ia |
只要传递给函数的size值不超过数组实际的大小,函数就是安全的。
数组形参和const: 当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const
的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参:C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上。
1 | void print(int (&arr)[10]) |
传递多维数组:数组的数组,其首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维的大小都是数组类型的一部分。
1 | void print(int (*matrix)[10], int rowSize) { /* ... */ } |
6.2.5 main: 处理命令行选项
1 | int main(int argc, char** argv) { /* ... */ } |
- 第二个形参
argv
是一个数组,它的元素是指向C风格字符串的指针; - 第一个形参
argc
表示数组中字符串的数量;
WARNING:当使用argv
中的实参数时,一定要记得可选的实参从argv[1]
开始,argv[0]
保存程序的名字,而非用户的输入。
6.2.6 含有可变形参的函数
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:
- 如果所有的实参类型相同,可以传递一个名为
initializer_list
标准库类型; - 如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板;
initializer_list形参:是一种标准库类型,用于表示某种特定类型的值的数组。
1 | initializer_list<T> lst; // 默认初始化 T类型元素的空列表 |
和vector不一样的是initializer_list
中的元素永远是常量值,无法改变其中的值。使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参。
1 | void error_msg(initializer_list<string> il) { |
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:
1 | // excepted 和 actual是string 对象 |
含有initializer_list形参的函数也可以同时拥有其他形参。例如,调试系统可能有个名为ErrCode
的用来表示不同类型的错误,之前的程序可以改写为:
1 | void error_msg(ErrCode e, initializer_list<string> il) { |
省略符形参:为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。
WARNING:省略符形参应该仅仅用与C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
1 | void foo(param_list, ...); |
第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。
6.3 返回类型和return语句
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值这样来使用返回引用的函数调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值。
列表初始化返回值:C++11新规定函数可以返回花括号包围的值的列表。
main函数的返回值:为了使返回值与机器无关,cstlib
头文件定义了两个预处理变量:
1 | int main() { |
递归:如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数递归函数。
1 | int factorial(int val) { |
6.3.3 返回数组指针
1 | int (*func(int i))[10]; // a function returns a pointer pointing to a 10-dimension array |
使用尾置返回类型:
1 | // func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组 |
使用decltype:
1 | int odd[] = {1, 3, 5, 7, 9}; |
delctype
并不负责把数组类型转换成对应的指针,所以delctype
的结果是个数组,要想表示arrPtr
返回指针还必须在函数声明时加一个*符号。
6.4 函数重载
NOTE:main
函数不能重载。
重载和const形参:一个拥有顶层const
的形参无法和另一个没有顶层const
的形参区分开来。但是如果形参是某种类型的指针或引用,怎通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const
是底层的。
1 | // 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同。 |
const_cast和重载:const_cast
在重载函数的情景中最有用。比如如下函数:
1 | const string &shorterString(const string &s1, const string &s2) { |
函数的参数和返回类型都是const string
的引用。需要一个新的shorterString
函数,当它的实参不是常量时,得到的结果是一个普通的引用,如下所示:
1 | string &shorterString(string &s1, string &s2) { |
调用重载函数:函数匹配是指一个过程,这个过程中函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载解析(overload resolution)。编译器首先将调用的实参与重载。
当调用重载函数时有三种可能情况:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码;
- 找不到任何一个函数与调用 的实参匹配,此时编译器发出无匹配的错误代码;
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用;
6.4.1 重载与作用域
当user调用print
函数时,编译器首先寻找对该函数名的声明,找到的是接受int
值的那个局部声明。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
NOTE:在C++语言中,名字查找发生在类型检查之前。
6.5 特殊用途语言特性
6.5.1 默认实参
函数在多次调用中被赋予同一个值的参数,称为函数的默认实参。
1 | typedef string::size_type sz; |
6.5.2 内联函数和constexpr函数
调用函数存在潜在的缺陷:调用函数一般比求等价表达式要慢一些。在大多数机器上,一次函数调用包含着一系列的工作:调用前要先保存寄存器,并在返回时回复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可以避免函数调用的开销:将函数在每个调用点上“内联地”展开。如下所示:
1 | cout << shorterStirng(s1, s2) << endl; |
constexpr函数:能用与常量表达式的函数。需遵循一下规定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
1 | constexpr int new_sz() { return 42; } |
执行初始化任务时,编译器把对constexpr
的调用替换成其结果值。为了能在编译过程中随时展开,constexpr
函数被隐式的指定为内联函数。和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义,因为编译器想要展开函数仅有函数声明是不够的,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr
函数通常定义在头文件中。
6.5.3 调试帮助
assert预处理宏:预处理宏其实是一个预处理变量,它的行为类似于内联函数。使用一个表达式作为它的条件:assert(expr)
。预处理名字由预处理器而非编译器管理。一次我们可以直接使用预处理名字而无需提供using声明。也就是说我们应该使用assert而不是std::assert。
NDEBUG预处理变量:如果定义了NDEBUG,那么assert则什么也不做。我们可以通过#define定义NDEBUG,从而关闭调试状态。
1 | CC -D NDEBUG main.cc |
该命令等价于在main.cc
文件的一开始写#define NDEBUG。如果NDEBUG没有定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略。
除了__func__
之外,预处理器还提供了另外4个对于程序调试很有用的名字:
1 | __FILE__; // 存放文件名的字符串字面值 |
6.6 函数匹配
确定候选函数和可行函数:函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:1. 与被调函数同名,2. 其声明在调用点可见。
第二步考察本次调用提供的实参,然后选择能被这组实参调用的函数,这些新的函数称为可行函数。
第三步是从可行函数中选择与本次调用匹配最佳的函数。基本思想是实参类型与形参类型越接近,它们的匹配度越高。
含有多个形参的函数匹配:编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配;
- 至少有一个实参的匹配由于其他可行函数提供的匹配;
如果检查了所有实参之后,没有任何一个函数脱颖而出,则该调用是错误的,编译器将报告二义性调用。
6.6.1 实参类型转换
编译器将实参类型到形参类型的转换分为以下几个等级:
- 精确匹配:
- 实参类型和形参类型相同;
- 实参从数组类型或函数类型转换成对应的指针类型;
- 向实参添加顶层const或者从实参中删除顶层const;
- 通过const转换实现的匹配;
- 通过类型提升实现的匹配;
- 通过算数类型转换或指针转换实现的匹配;
- 通过类类型转换实现的匹配;
需要类型提升和算术类型转换的匹配:小整型一般都会提升到int或更大的整数类型。且所有算术类型的转换的级别都一样,从int向unsigned int的转换并不比从int向double的转换级别高。
函数匹配和const实参:如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
6.7 函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参共同决定,与函数名无关。
1 | bool length_compare(const string &, const string &); |
lenght_compare
的类型是bool(const string&, const string &)
。
使用函数指针:当我们把函数名作为一个值使用时,该函数自动地转换成指针。
1 | pf = length_compare; // pf指向名为length_compare的函数 |
在指向不同函数类型的指针间不存在转换规则。但是可以为函数指针赋也各nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数。
重载函数的指针:当使用重载函数时,上下文必须清晰地界定到底应该引用哪个函数。
函数指针形参:和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。
1 | void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); |