第六章 函数
函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。
函数基础
一个典型的函数(function)定义包括以下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表以及函数体。
我们通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行暂时被中断,被调函数(called function)开始执行。
当遇到一条return语句时函数结束执行过程。函数的返回值用于初始化调用表达式的结果。
局部对象
在C++语言中,名字有作用域,对象有生命周期(lifetime),理解这两个概念非常重要:
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量统称为局部变量(local variable)。它们仅在函数的作用域内可见。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。
形参是一种自动对象。函数开始时为形参申请存储空间,函数一旦终止,形参就被销毁。
对于局部变量对应的自动对象来说,如果变量定义本身含有初始值,就用这个初始值进行初始化;否则执行默认初始化(内置类型产生未定义的值)。
局部静态对象
局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
一个例子(其解释见书本p185):
// 统计函数被调用了多少次
size_t count_calls()
{
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ctr;
}
函数返回类型
大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何类型。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
函数声明
函数的名字必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。
函数的声明和定义唯一的区别是声明无须函数体,用一个分号替代即可。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)。
在头文件中进行函数声明
我们建议函数在头文件中声明,在源文件中定义。
这是因为如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需改变一条声明即可。
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
分离式编译
C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。
my note: 这部分内容我比较熟悉了,详细解释见书本p186。
参数传递
Note
形参初始化的机理与变量初始化一样。
如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或函数被传值调用(called by value)。
传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
指针形参
当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
Note
熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针。
传引用参数
对于引用的操作实际上是作用于引用所引的对象上,引用形参也是如此。通过使用引用形参,允许函数改变一个或多个实参的值。
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效。甚至有的类型根本就不支持拷贝操作。此时应该使用引用形参访问该类型的对象。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。那就是通过引用形参并修改它(也就是修改了其引用的对象),从而作为结果传出。
const形参和实参
当形参是const时,必须注意关于顶层const的讨论(p57)。
当用实参初始化形参时会忽略形参的顶层const。即当形参有顶层const时,传递给它常量对象或者非常量对象都是可以的。
忽略形参的顶层const可能产生意想不到的结果:
void fcn(const int i) {}
void fcn(int i) {} // 错误:重复定义
在C++中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表有明显的区别。因为顶层const被忽略了,所以在上面的代码中传入两个fcn函数的参数可以完全一样(从而编译器不知道该调用哪一个)。
指针或引用形参与const
我们可以使用非常量初始化一个底层const,但是反过来不行(不能用一个常量初始化一个非底层const);同时一个普通的引用必须用同类型的对象初始化。
尽量使用常量引用
把函数不会改变的形参定义成(普通的)引用是一种常见错误,这么做给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型(比如无法传入一个常量对象了)。
比如下面这个例子将导致编译错误(p192):
// 不良设计,第一个形参的类型应该是const string&
string::size_type find_char(string &s, char c, string::size_type &occurs);
//...
find_char("Hello World", 'o', ctr); // 无法编译通过
数组形参
当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
Warning
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用技术。
1. 使用标记指定数组长度,如C风格字符串。
2. 使用标准库规范,如传递首元素和尾后元素的指针,来表示一个范围。
3. 显示传递一个表示数组大小的形参。
my note: 以上技术详细解读见书本p194。
数组形参和const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上。
// 正确,形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
但这一用法也限制了print函数的可用性,我们只能将函数作用于大小为10的数组。
传递多维数组
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针,也就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) { /* ... */ }
也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度:
// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }
matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。
main: 处理命令行选项
有时候我们需要给main函数传递实参。一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如:
prog -d -o ofile data0
这些命令行选项通过两个(可选的)形参传递给main函数。
int main(int argc, char *argv[]) { ... }
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个参数argc表示数组中字符串的数量;argc至少为1。
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
以上面的为例,argc应该等于5,argv应该包含如下的C风格字符串:
argv[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的标准库类型;
-
如果实参的类型不同,我们可以编写一种可变参数模板,其细节将在16.4节介绍(p618)。
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。这种功能一般只用于与C函数交互的接口程序。
initializer_list形参
initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。
initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。
Warning
省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo(param_list, ...);
void foo(...);
返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
return语句有两种形式:
return;
return expression;
无返回值函数
没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。
有返回值函数
只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
如果函数返回引用,则该引用仅是它所引对象的一个别名。
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
主函数main的返回值
我们允许main函数没有return语句直接结束,这样编译器将隐式地插入一条返回0的return语句,表示执行成功。
为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,可以用来表示成功与失败:
int main()
{
if (some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}
递归
如果函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。
在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。
main函数不能调用它自己。
返回数组的指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。使用类型别名(p60)可以简化这种返回类型:
typedef int arrT[10]; // arrT是一个类型别名,表示含有10个整数的数组
using arrT = int[10]; // arrT的等价声明
arrT* func(int i); // func返回一个指向含有10个整数的数组的指针
声明一个返回数组指针的函数
形式如下:
Type (*function(param_list))[dimension]
类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*表示返回的是一个指针。)
举一个例子:
int (*func(int i))[10];
可以按照以下的顺序来逐层理解该声明的含义:
-
func(int i)
表示调用func函数时需要一个int类型的实参。 -
(*func(int i))
意味着我们可以对函数的调用结果执行解引用操作。 -
(*func(int i))[10]
表示解引用func的调用将得到一个大小是10的数组。 -
int (*func(int i))[10]
表示数组中的元素是int类型。
使用尾置返回类型
C++新标准提供了另一种简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。
尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
使用decltype
如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型(即获得一个数组类型)。
案例:
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even;
}
my note: decltype并不负责把数组类型转换成对应的指针。
函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。比如:
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);
这些函数接受的形参类型不一样,但是执行的操作非常类似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。
函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。
main函数不能重载。
不允许两个函数除了返回类型以外其他所有的要素都相同。比如:
Record lookup(const Account&);
bool lookup(const Account&); // 错误,与上一个函数相比只有返回类型不同
my note: 返回类型不同的函数,也可以是重载的。只要函数名相同而形参有明显的不同。
重载和const形参
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone); // 重复声明
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
Record lookup(Account&); // 此函数作用于Account的引用
Record lookup(const Account&); // 新函数,作用于常量引用
这种情况下,当我们传递一个非常量对象时,编译器会优先选用非常量版本的函数(尽管传给常量版本的也可以)。
const_cast和重载
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);
}
下面重载的版本中,首先将它的实参强制转换成了对const的引用,然后调用了shorterString函数的const版本。const版本返回对const string的引用,这个引用事实上绑定在一个非常量实参上。因此,可以再将其转换回普通的const&,这显然是安全的。
my note: 传入非常量的实参将调用非常量的版本。
调用重载的函数
定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配(function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来。编译器首先将调用的实参与重载集合中的每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
当调用重载函数时有三种可能的结果:
-
编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
-
找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
-
有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)。
重载与作用域
一般来说,将函数声明置于局部作用域内不是一个明智的选择。
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。对于函数而言也是如此。
my note: 具体例子可见于书本p210。总之,如果在内层作用域声明了一个函数,那么外层的同名的函数都将变得不可见,因此无法找到外层的重载版本。
特殊用途语言特性
默认实参
这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。如:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backrnd = ' ');
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
使用默认实参调用函数
如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。如:
string window;
window = screen(); // 等价于screen(24, 80, ' ');
window = stcreen(66); // 等价于screen(66, 80, ' ');
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。
当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
默认实参初始值
局部变量不可以作为默认实参。另外只要表达式的类型可以转换成形参类型,该表达式就可以作为默认实参。
如:
int g_a = 0;
void f(int a = g_a);
内联函数和constexpr函数
调用普通函数比直接写其语句要慢,这是因为调用函数包含一些额外的工作。
内联函数可以避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。
内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
内联机制用于优化规模小,流程直接,频繁调用的函数。
constexpr函数
是指能用于常量表达式的函数。
函数的返回类型及所有形参都得是字面值类型,且函数体内必须有且只有一条return语句。
如:
constexpr int new_sz() { return 8; }
constexpr int foo = new_sz();
constexpr函数被隐式地指定为内联函数。
把内联函数和constexpr函数放在头文件内
这是因为内联函数和constexpr函数可以多次定义,且必须完全一致。所以把它们都定义在头文件内。
调试帮助
程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。
assert预处理宏
assert是一种预处理宏(preprocessor macro)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:
assert(expr);
首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
assert宏定义在cassert头文件中。
assert宏常用于检查“不能发生”的条件。(即确实不应该发生的事情,发生了就崩溃吧)
NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。或者使用编译器提供的命令行选项定义预处理变量:
$ CC -D NDEBUG main.c
这条命令的作用等价于在main.c文件的一开始写#define NDEBUG。
我们可以把assert当成调试程序的一种辅助手段,但是不能用它代替真正的运行时逻辑检查,也不能代替程序本身应该包含的错误检查。
除了用于assert,也可以使用NDEBUG编写自己的调试代码。
比如:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
// __func__是编译器定义的一个局部静态变量,用于存放函数的名字
cerr << __func__ << "": array size is: " << size << endl;
#endif
// ...
}
编译器为每个函数都定义了__func__
,除此之外,预处理器还定义了4个对于调试程序很有用的名字:
-
__FILE__
, 存放文件名的字符串字面值。 -
__LINE__
, 存放当前行号的整型字面值。 -
__TIME__
, 存放文件编译时间的字符串字面值。 -
__DATA__
, 存放文件编译日期的字符串字面值。
函数匹配
本节(p217)讲述编译器如何确定调用哪个重载函数,并以下述这组函数及其调用为例:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 调用void f(double, double);
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数成为候选函数(candidate function)。候选函数具备两个特征:
1. 与被调用函数同名。
2. 其声明在调用点可见。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:
1. 其形参数量与本次调用提供的实参数量相等。
2. 每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
my note: 如果没有找到可行函数,编译器将报告无匹配函数的错误。
寻找最佳匹配(如果有的话)
第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。
如果有且只有一个函数满足下列条件,则匹配成功:
-
该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
-
至少有一个实参的匹配优于其他可行函数提供的匹配。
my note: 如果编译器检查了每一个可行函数,没有一个能脱颖而出,则会报告二义性调用错误。
实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
1. 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 向实参添加顶层const或者从实参中删除顶层const。
2. 通过const转换实现的匹配(p143)。
3. 通过类型提升实现的匹配(p142)。
4. 通过算数类型转换或指针转换实现的匹配(p142)。
5. 通过类类型转换实现的匹配(参见14.9节,p514)。
my note: 详细案例解析见书本p220。
函数指针
函数指针指向的是函数而非对象。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:
bool lengthCompare(const string&, const string&);
该函数的类型是:bool (const string&, const string&);
要想声明一个指向该函数的指针,只需要将函数名替换成指针即可:
bool (*pf)(const string&, const string&);
使用函数指针
当我们把函数名作为一个值使用的时候,该函数名自动转换成指针(指向该函数的)。
例如,可以这样给把函数地址赋值给指针:
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句,取地址符是可选的
可以直接对指向函数的指针调用该函数,无须解引用指针:
bool b1 = pf("Hello", "Hi");
bool b2 = (*pf)("Hello", "Hi"); // 等价调用
bool b3 = lengthCompare("Hello", "Hi"); // 等价调用
可以给函数指针赋一个nullptr或0,表示没有指向任何函数。
重载函数的指针
当使用了重载函数时,编译器必须确定一个能和指针类型精确匹配的函数,即返回类型和形参列表都要一样。
函数指针形参
不能定义函数类型的形参,但是形参可以是指向函数的指针。
当把函数名作为实参使用,它会自动转换成指针。
定义一个函数(以及指针)类型的方法有:
- typedef
typedef bool Func(int); // Func是函数类型
typedef bool (*FuncP)(int); // FuncP是函数指针类型
- decltype
假如已经有了一个函数:bool Foo(int);
decltype(Foo) Func;
decltype(Foo) *FuncP;
- using
using Func = bool(int);
using FuncP = bool(*)(int);
my note: 关于使用直接声明的方法,以及使用尾置返回类型的方法来确定一个返回类型为函数指针的函数,见书本p223。因为我觉得使用上述别名定义已经能满足这种需求了。