我们通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
执行函数的第一步是(隐式地)定义并初始化它的形参,即创建与形参同名的匿名变量。
实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
函数的调用规定实参数量应与形参数量一致,所以形参一定会被初始化。
不能将
const char*
转换成int
。形参名是可选的,但是由于我们无法使用未命名的形参,所以形参一般都应该有个名字。偶尔,函数确实有个别形参不会被用到,则此类形参通常不命名以表示在函数体内不会使用它。不管怎样,是否设置未命名的形参并不影响调用时提供的实参数量。即使某个形参不被函数使用,也必须为它提供一个实参。
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
在 C++ 语言中,名字有作用域,对象有生命周期(lifetime):
- 名字的作用域是程序文本的一部分,名字在其中可见;
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)外层作用域中同名的其他所有声明。在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。
把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。 内置类型的未初始化局部变量将产生未定义的值。
局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。内置类型的局部静态变量初始化为 0。
函数只能定义一次,但可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
函数声明也称作函数原型(function prototype)。
含有函数声明的头文件应该被包含到定义函数的源文件中。
大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是 .obj(Windows)或 .o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)。
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上:否则,将实参的值拷贝后赋给形参。 当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。
最佳实践:熟悉 C 的程序员常常使用指针类型的形参访问函数外部的对象。在 C++ 语言中,建议使用引用类型的形参替代指针。
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括 IO 类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
最佳实践: 如果函数无须改变引用形参的值,最好将其声明为常量引用。
值传递形参既可以传入左值,也可以传入右值。非常量引用的初始值只能是左值。
当用实参初始化形参时会忽略掉顶层
const
。换句话说,形参的顶层const
被忽略掉了。当形参有顶层const
时,传给它常量对象或者非常量对象都是可以的。可以使用非常量初始化一个底层
const
对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。 观察下面的代码片段:1
2
3
4
5
6
7int i = 42;
const int *cp = &i; // 正确:但是 cp 不能改变 i
const int &r = i; // 正确:但是 r 不能改变 i
const int &r2 = 42; // 正确
int *p = cp; // 错误:p 的类型和 cp 的类型不匹配
int &r3 = r; // 错误:r3 的类型和 r 的类型不匹配
int &r4 = 42; // 错误:不能用字面值初始化一个非常量引用,即非常量引用的初值只能是左值C++ 允许我们用字面值初始化常量引用,常量引用的常量属性已经限制了函数体内部对所传入实参的修改操作,所以用字面值初始化函数的常量引用形参并无大碍。
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型:我们不能把
const
对象、字面值或者需要类型转换的对象传递给普通的引用形参。数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:
- 不允许拷贝数组;
使用数组时(通常)会将其转换成指针。
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。 尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:1
2
3
4
5// 尽管形式不同,但这三个 print 函数是等价的
// 每个函数都有一个 const int* 类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定数组的大小对函数的调用没有影响。
WARNING:和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
管理指针形参有三种常用的技术:
- 使用标记指定数组长度
管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是 C 风格字符串。C 风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int
这样所有取值都是合法值的数据就不太有效了。 - 使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。 - 显式传递一个表示数组大小的形参。
- 使用标记指定数组长度
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向
const
的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
1
2
3
4
5void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}&arr
两端的括号必不可少:1
2f(int &arr[10]) // 错误:将 arr 声明成了引用的数组
f(int (&arr)[10]) // 正确:arr 是具有 10 个整数的整型数组的引用因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了
print
函数的可用性,我们只能将函数作用于大小为 10 的数组:1
2
3
4
5inti=0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i); // 错误:实参不是含有 10 个整数的数组
print(j); // 错误:实参不是含有 10 个整数的数组
print(k); // 正确:实参是含有 10 个整数的数组和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针:
1
2// matrix 指向数组的首元素,该数组的元素是由 10 个整数构成的数组
void print(int (*matrix)[10], int rowSize) { /* ... */ }再一次强调,*matrix 两端的括号必不可少:
1
2int *matrix[10]; // 10 个指针构成的数组
int (*matrix)[10]; // 指向含有 10 个整数的数组的指针也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度:
1
2// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }matrix 的声明看起来是一个二维数组,实际上形参是指向含有 10 个整数的数组的指针。
有时我们需要给
main
传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main
函数位于可执行文件 prog 之内,我们可以向程序传递下面的选项:1
prog-d-0 ofile data0
这些命令行选项通过两个(可选的)形参传递给
main
函数:1
int main(int argc, char *argv[]) { ... }
第二个形参 argv 是一个数组,它的元素是指向 C 风格字符串的指针;第一个形参 argc 表示数组中字符串的数量。因为第二个形参是数组,所以
main
函数也可以定义成:1
int main(int argc, char **argv) { ... }
其中 argv 指向
char*
。当实参传给main
函数之后,argv 的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为 0。 以上面提供的命令行为例,argc 应该等于 5,argv 应该包含如下的 C 风格字符串:1
2
3
4
5
6argv[0] = "prog"; // 或者 argv[0] 也可以指向一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;WARNING:当使用 argv 中的实参时,一定要记得可选的实参从 argv[1] 开始;argv[0] 保存程序的名字,而非用户输入。
为了编写能处理不同数量实参的函数,C++11 新标准提供了两种主要的方法:
- 如果所有的实参类型相同,可以传递一个名为
initializer_list
的标准库类型; - 如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。
- 如果所有的实参类型相同,可以传递一个名为
C++ 还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。不过需要注意的是,这种功能一般只用于与 C 函数交互的接口程序。
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用
initializer_list
类型的形参。initializer_list
是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list
类型定义在同名的头文件中,它提供的操作如下表所示:initializer_list 提供的操作 initializer_list<T> lst; 默认初始化;T类型元素的空列表 initializer_list lst{a, b, c...}; lst 的元素数量和初始值一样多;lst 的元素是对应初始值的副本;列表中的
元素是 constlst2(lst) 拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素;拷贝后,
原始列表和副本共享元素lst2 = lst lst.size() 列表中的元素数量 lst.begin() 返回指向lst中首元素的指针 lst.end() 返回指向lst中尾元素下一位置的指针 和
vector
一样,initializer_list
也是一种模板类型。定义initializer_list
对象时,必须说明列表中所含元素的类型。和vector
不一样的是,initializer_list
对象中的元素永远是常量值,我们无法改变initializer_list
对象中元素的值。如果想向initializer_list
形参中传递一个值的序列,则必须把序列放在一对花括号内。因为initializer_list
包含begin
和end
成员,所以我们可以使用范围for
循环处理其中的元素。省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的。这些代码使用了名为 varargs 的 C 标准库功能。通常,省略符形参不应用于其它目的。
WARNING:省略符形参应该仅仅用于 C 和 C++ 通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
1
2void foo(parm_list, ...);
void foo(...);省略符形参所对应的实参无须类型检查。 在第一种形式中,形参声明后面的逗号是可选的。
在
范围for
循环中使用initializer_list
对象时,应该将循环控制变量声明成引用类型吗?为什么?
答:引用类型的优势主要是可以直接操作所引用的对象,以及避免拷贝较为复杂的类类型对象和容器对象。因为initializer_list
对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。只有当initializer_list
对象的元素类型是类类型或容器类型(比如string
)时,才有必要把范围for
循环的控制变量设为引用类型。参考自 C++ Primer 学习笔记及作业答案之第六章。return
语句有两种形式:1
2return;
return expression;没有返回值的
return
语句只能用在返回类型是void
的函数中。返回void
的函数不要求非得有return
语句,因为在这类函数的最后一句后面会隐式地执行return
。
一个返回类型是void
的函数也能使用return
语句的第二种形式,不过此时return
语句的 expresssion 必须是另一个返回void
的函数。强行令void
函数返回其他类型的表达式将产生编译错误。return
语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。返回一个值的方式和初始化一一 个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。如果函数返回引用,则该引用仅是它所引对象的一个别名。
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域,不应返回局部对象的引用或指针:
1
2
3
4
5
6
7
8
9
10// 严重错误:这个函数试图返回局部对象的引用
const string &mainip()
{
string ret;
// 以某种方式改变一下 ret
if (!ret.empty())
return ret; // 错误:返回局部对象的引用!
else
return "Empty"; // 错误:"Empty" 是一个局部临时量
}在第二条
return
语句中,字符串字面值转换成一个局部临时string
对象,对于 manip 来说,该对象和 ret 一样都是局部的。调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员:
1
2// 调用 string 对象的 size 成员,该 string 对象是由 shorterString 函数返回的
auto sz = shorterString(s1, s2).size();函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值,如果返回类型 是常量引用,我们不能给调用的结果赋值。
C++11 新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
1
2
3
4
5
6
7
8
9
10
11
12
13vector<string> process()
{
// ...
// expected和actual是string对象
if (expected.empty())
return {};
// 返回一个空vector对象
else if (expected == actual)
return {"functionX", "okay"};
// 返回列表初始化的vector对象
else
return {"functionX", expected, actual};
}第一条
return
语句返回一个空列表,此时,proceess 函数返回的vector
对象是空的。如果 expected 不为空,根据 expected 和 actual 是否相等,函数返回的vector
对象分别用两个或三个元素初始化。
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值而且该值所占空间不应该大于且标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。允许
main
函数没有return
语句直接结束。如果控制到达了main
函数的结尾处而且没有return
语句,编译器将隐式地插入一条返回 0 的return
语句。main
函数的返回值可以看做是状态指示器。返回 0 表示执行成功,返回其他值表示执行失败,其中非 0 值的具体含义依机器而定。为了使返回值与机器无关,cstdlib
头文件定义了两个预处理变量:EXIT_FAILURE
和EXIT_SUCCESS
。因为它们是预处理变量,所以既不能在前面加上std::
,也不能在using
声明中出现。如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。我们有时候会说这种函数含有递归循环(recursion loop)。注意,
main
函数不能调用它自己。编写一个递归函数,输出
vector
对象的内容:1
2
3
4
5
6
7
8
9template <typename T>
void recursion_cout(vector<T> v)
{
if (v.empty())
return;
cout << v.back() << endl;
v.pop_back();
recursion_cout(v);
}因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用:
1
2
3typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有 10 个整数的数组
using arrT = int[10]; // arrT 的等价声明
arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针观察下述代码片段:
1
2
3int arr[10]; // arr 是一个含有 10 个整数的数组
int *p1[10]; // p1 是一个含有 10 个指针的数组
int (*p2)[10] = &arr; // p2 是一个指针,它指向含有 10 个整数的数组如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。 返回数组指针的函数形式如下所示:
1
type (*function(parameter_list)) [dimension]
(*function(parameter_list))
两端的括号必须存在,就像我们定义 p2 时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。在 C++11 新标准中可以使用尾置返回类型(railing return type)简化函数声明。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个
->
符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto
:1
2// func 接受一个 int 类型的实参,返回一个指针,该指针指向含有 10 个整数的数
auto func(int i)->int(*)[10];如果我们知道函数返回的指针将指向哪个数组,就可以使用
decltype
关键字声明返回类型。decltype
并不负责把数组类型转换成对应的指针,所以decltype
的结果是个数组。对数组应用decltype
关键字时,得到的推断类型是数组类型(包括数组维度在内);使用数组作为一个auto
变量的初始值时,得到的推断类型是指向数组元素类型的指针,而非数组。编写一个函数的声明,使其返回数组的引用并且该数组包含 10 个
string
对象:1
2
3
4
5string (&func(parameter_list))[10]; // 普通声明方式
typedef string (&func(parameter_list))[10]; // 使用类型别的名声明方式
auto func(parameter_list)->string(&)[10]; // 使用尾置返回类型的声明方式
string s[10];
decltype(s) &func(parameter_list); // 使用 decltype 关键字的声明方式同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。
main
函数不能重载。对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其他所有的要素都相同。
有时候两个形参列表看起来不一样,但实际上是相同的:
1
2
3
4
5
6
7// 每对声明的是同一个函数
Record lookup(const Account &acct);
Record lookup(const Account&); // 省略了形参的名字
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno 和 Phone 的类型相同顶层
const
不影响传入函数的对象。一个拥有顶层const
的形参无法和另一个没有顶层const
的形参区分开来:1
2
3
4Record lookup(Phone);
Record lookup(const Phone); // 重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明了Record lookup(Phone*)如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的
const
是底层的:1
2
3
4
5
6
7// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了 4 个独立的重载函数
Record lookup(Account&); // 函数作用于 Account 的引用
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*); // 新函数,作用于指向 Account 的指针
Record lookup(const Account*); // 新函数,作用于指向常量的指针编译器可以通过实参是否是常量来推断应该调用哪个函数。因为
const
不能转换成其他类型,所以我们只能把const
对象(或指向const
的指针)传递给const
形参。相反的,因为非常量可以转换成const
,所以上面的 4 个函数都能作用于非常量对象或者指向非常量对象的指针。不过,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。最好只重载那些确实非常相似的操作。有些情况下,给函数起不同的名字能使得程序更易理解。
函数匹配(function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(overload resolution)。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
当调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码;
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息;
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)。
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。剩下的工作就是检查函数调用是否有效了。
在 C++ 语言中,名字查找发生在类型检查之前。
某些函数有这样一种形参,在函数的很多次调用中它们都被赋了一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(defaultargument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。(应该是为了确保函数调用时实参与形参匹配关系的唯一性——博主注)
如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
通常,一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
最佳实践:通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
局部变量不能作为默认实参。 除此之外,只要表达式的类型能转换成形参所需的类型该表达式就能作为默认实参:
1
2
3
4
5
6// wd、def 和 ht 的声明必须出现在函数之外
sz wd=80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // 调用 screen(ht(), 80, ' ')用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:
1
2
3
4
5
6void f2()
{
def = '*'; // 改变默认实参的值
sz wd = 100; // 隐藏了外层定义的 wd,但是没有改变默认值
window = screen(); // 调用 screen(ht(), 80, '*')
}在函数 f2 内部改变了 def 的值,所以对 screen 的调用将会传递这个更新过的值。另一方面,虽然我们的函数还声明了一个局部变量用于隐藏外层的 wd,但是该局部变量与传递给 screen 的默认实参没有任何关系。
调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可避免函数调用的开销。 将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个 75 行的函数也不大可能在调用点内联地展开。
constexpr 函数(constexpr function)是指能用于常量表达式的函数。定义
constexpr
函数的方法与其他函数类似,不过要遵循几项约定:- 函数的返回类型及所有形参的类型都得是字面值类型;
- 函数体中必须有且只有一条
return
语句。
为了能在编译过程中随时展开,
constexpr
函数被隐式地指定为内联函数。constexpr
函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr
函数中可以有空语句、类型别名以及using
声明。允许constexpr
函数的返回值并非一个常量:1
2
3
4
5// 如果 arg 是常量表达式,则 scale(arg) 也是常量表达式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt; // new_sz 是另一个 constexpr 函数
}当 scale 的实参是常量表达式时,它的返回值也是常量表达式;反之则不然。
和其他函数不一样,内联函数和
constexpr
函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr
函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr
函数通常定义在头文件中。assert
是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert
宏使用一个表达式作为它的条件:1
assert(expr);
首先对 expr 求值,如果表达式为假(即 0),
assert
输出信息并终止程序的执行。如果表达式为真(即非 0),assert
什么也不做。assert
宏定义在cassert
头文件中。如我们所知,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而无须提供using
声明。也就是说,我们应该使用assert
而不是std::assert
,也不需要为assert
提供using
声明。和预处理变量一样,宏名字在程序内必须唯一。含有cassert
头文件的程序不能再定义名为assert
的变量、函数或者其他实体。assert
宏常用于检查“不能发生”的条件。assert
的行为依赖于一个名为NDEBUG
的预处理变量的状态。如果定义了NDEBUG
,则assert
什么也不做。默认状态下没有定义NDEBUG
,此时assert
将执行运行时检查。我们可以使用一个#define
语句定义NDEBUG
,从而关闭调试状态。定义NDEBUG
能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert
应该仅用于验证那些确实不可能发生的事情。我们可以把assert
当成调试程序的一-种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。编译器为每个函数都定义了
_ _func_ _
,它是const char
的一个静态数组,用于存放函数的名字。除了 C++编译器定义的_ _func_ _
之外,预处理器还定义了另外 4 个对于程序调试很有用的名字:_ _FILE_ _
,存放文件名的字符串字面值;_ _LINE_ _
,存放当前行号的整型字面值;_ _TIME_ _
,存放文件编译时间的字符串字面值;_ _DATE_ _
,存放文件编译日期的字符串字面值。
函数匹配分三步:
- 第一步,选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见;
- 第二步,考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。如果没找到可行函数,编译器将报告无匹配函数的错误;
- 第三步,从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
最佳实践:调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
分析函数调用前,我们应该知道小整型一般都会提升到
int
类型或更大的整数类型。假设有两个函数,一个接受int
、另一个接受short
,则只有当调用提供的是short
类型的值时才会选择short
版本的函数。所有算术类型转换的级别都一样。例如,从
int
向unsigned int
的转换并不比从int
向double
的转换级别高。如果重载函数的区别在于它们的引用类型的形参是否引用了
const
,或者指针类型的形参是否指向const
,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。切记,不能把普通引用绑定到const
对象上,即常量引用的初值只能是左值。如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:如果实参是指向常量的指针,调用形参是const*
的函数:如果实参是指向非常量的指针,调用形参是普通指针的函数。函数指针指向的是函数而非对象。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
1
2// pf 指向一个函数,该函数的参数是两个 const string 的引用,返回值是 bool
bool (*pf)(const string&, const string&); // 未初始化当我们把函数名作为一个值使用时,该函数自动地转换成指针。 假设存在一个名为 lengthCompare 函数,则下述的赋值语句是等价的:
1
2pf = lengthCompare; // pf 指向名为 lengthCompare 的函数
pf = &lengthCompare; // 等价的赋值语句:取地址符是可选的可以直接使用指向函数的指针调用该函数,无须提前解引用指针:
1
2
3bool bl = pf("he1lo","goodbye"); // 调用 lengthCompare 函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用在指向不同函数类型的指针间不存在转换规则。但是和往常一样,我们可以为函数指针赋一个
nullptr
或者值为 0 的整型常量表达式,表示该指针没有指向任何一个函数。当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。
类型别名和
decltype
能让我们简化使用了函数指针的代码:1
2
3
4
5
6// Func 和 Func2 是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // 等价的类型
// FuncP 和 FuncP2 是指向函数的指针
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; // 等价的类型需要注意的是,
decltype
返回函数类型,此时不会将函数类型自动转换成指针类型。(类似decltype
对数组的处理方式)和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
1
2using F = int(int*, int); // F 是函数类型,不是指针
using PF = int(*)(int*, int); // PF 是指针类型必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指定为指针:
1
2
3PF f1(int); // 正确:PF 是指向函数的指针,f1 返回指向函数的指针
F f1(int); // 错误:F 是函数类型,f1 不能返回一个函数
F *f1(int); // 正确:显式地指定返回类型是指向函数的指针还可以使用尾置返回类型的方式声明一个返回函数指针的函数:
1
auto f1(int)->int(*)(int*, int);
如果我们明确知道返回的函数是哪一个,就能使用
decltype
简化书写函数指针返回类型的过程。编写函数的声明,令其接受两个
int
形参并且返回类型也是int
;然后声明一个vector
对象,令其元素是指向该函数的指针。1
2int func(int, int);
vector <decltype(func)*> v;一些关键术语:
- 二义性调用(ambiguous call) 是一种编译时发生的错误,造成二义性调用的原因是在函数匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配;
- 最佳匹配(best match) 从一组重载函数中为调用选出的一个函数。如果存在最佳匹配,则选出的函数与其他所有可行函数相比,至少在一个实参上是更优的匹配,同时在其他实参的匹配上不会更差。
C++ Primer - 第 6 章 函数
猜你喜欢
Thank you for your donate!
- 本文链接: https://blog.shipengx.com/archives/fb22e988.html
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!