函数
6.1函数基础
通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用逗号隔开的实参列表,从而用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
形参和实参
尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
6.1.1局部对象
在c++语言中,名字有作用域,对象有生命周期。理解这两个概念非常重要:
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。因此,把只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。 形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。 内置类型的未初始化局部变量将产生未定义的值。
局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static 类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化(只能初始化一次),并且直到程序终止才销毁,在此期间即使对象所在的函数结束执行也不会对它有影响:
size_t count_calls() {
static size_t ctr = 0;
return ++ctr;
}
int main() {
for (size_t i = 0; i != 10; ++i) {
cout << count_calls() << endl;
}
return 0;
}
如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
6.1.2函数声明
和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。唯一的例外是,如果一个函数永远也不会被用到,那么它可以只有声明没有定义。
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
函数声明也称作函数原型。
在头文件中进行函数声明
建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。 如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦想改变函数的接口,只需改变一条声明即可。 定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。 函数函数声明的头文件应该被包含到定义函数的源文件中。
6.1.3分离式编译
随着程序越来越复杂,希望把程序的各个部分分别存储在不同文件中。为了允许编写程序时按照逻辑关系将其划分开来,c++语言支持所谓的分离式编译,允许把程序分割到几个文件中去,每个文件独立编译。如果修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制。
6.2参数传递
形参初始化的机制与变量初始化一样。 和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。 当形参是引用类型时,对应的实参被引用传递。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。
6.2.1传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
void reset(int *ip) {
*ip = 0;
ip = 0;
}
熟悉c的程序员常常使用指针类型的形参访问函数外部的对象。在c++语言中,建议使用引用类型的形参替代指针。
6.2.2传引用参数
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。另一方面,如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为一次返回多个结果提供了有效的途径。
string::size_type find_char(const string &s, char c, string::size_type &occurs) {
auto ret = s.size();
occurs = 0;
for (decltype(ret) i = 0; i != s.size(); ++i) {
if (s[i] == c) {
if (ret == s.size()) {
ret = i;
}
++occurs;
}
}
return ret;
}
6.2.3const 形参和实参
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const 。换句话说,形参的顶层const 被忽略掉了。当形参有顶层const 时,传给它常量对象或者非常量对象都是可以的。但是忽略掉形参的顶层const 也会造成函数重载的一些问题。例如,void fcn(int) 其实和void fcn(const int) 是一样的。
指针或引用形参与const
形参的初始化方式和变量的初始化方式是一样的,可以使用非常量初始化一个底层const 对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
void reset(int &);
void reset(int *);
int main() {
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i);
reset(&ci);
reset(i);
reset(ci);
reset(42);
reset(ctr);
return 0;
}
尽量使用常量引用
把函数不会改变的形参定义成普通的引用是一种比较常见的错误,这么做会产生一种误导,即函数可以修改它的实参的值。此外,使用普通的引用而非常量引用也会极大地限制函数所能接受的实参类型,例如,不能把const 对象、字面值或者需要类型转换的对象传递给普通的引用形参。
6.2.4数组形参
数组的两个特殊性质对定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时通常会将其转换成指针。因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式:
void print(const int*);
void print(const int[]);
void print(const int[10]);
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。 因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
使用标记指定数组长度
管理数组实参的第一种方法是要求数组本身包含一个结束标记,例如c风格字符串(最后一个字符后面跟着一个空字符):
void print(const char *cp) {
if (cp) {
while (*cp) {
cout << *cp++;
}
}
}
这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。
适用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针:
void print(const int *beg, const int *end) {
while (beg != end) {
cout << *beg++ << endl;
}
}
只要调用者能正确地计算指针所指的位置,那么这样的方法就是安全的。
显式传递一个表示数组大小的形参
第三种管理数组实参的方法是专门定义一个表示数组大小的形参:
void print(const int ia[], size_t size) {
for (size_t i = 0; i != size; ++i) {
cout << ia[i] << endl;
}
}
数组引用和形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用:
void print(int (&arr)[10]) {
for (auto elem : arr) {
cout << elem << endl;
}
}
这种用法无形中限制了函数的可用性,只能将函数作用于指定大小的数组(后面会介绍给引用类型的形参传递任意大小的数组)。
传递多维数组
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
void print(int (*matrix)[10], int rowSize) {
}
也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:
void print(int matrix[][10], int rowSize) {
}
形参看起来是一个二维数组,实际上是指向指定大小的数组的指针。
6.2.6含有可变形参的函数
有时无法提前预知应该向函数传递几个实参。为了编写能处理不同数量实参的函数,c++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list 的标准库类型;如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。 C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参,不过这种功能一般只用于与c函数交互的接口程序。
initializer_list 形参
initializer_list 用于表示某种特定类型的值的数组,定义在同名的头文件中。
和vector 一样,initializer_list 也是一种模板类型;和vector 不一样的是,initializer_list 对象中的元素永远是常量值。如果想向initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内:
if (expected != actual) {
error_msg({"functionX", expected, actual});
} else {
error_msg({"functionX", "okay"});
}
含有initializer_list 形参的函数也可以同时拥有其他形参。
省略符形参
省略符形参是为了便于c++程序访问某些特殊的c代码而设置的,这些代码使用了名为varargs 的c标准库功能。通常,省略符形参不应用于其他目的。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。 省略符形参只能出现在形参列表的最后一个位置:
void foo(parm_list, ...);
void foo(...);
6.3返回类型和return 语句
6.3.2有返回值函数
return 语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
函数完全后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域;同样,返回局部对象的指针也是错误的。
引用返回左值
调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用:
char& get_val(string& str, string::size_type ix) {
return str[ix];
}
int main() {
string s("a value");
cout << s << endl;
get_val(s, 0) = 'A';
cout << s << endl;
return 0;
}
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。 如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。
主函数main 的返回值
为了使返回值与机器无关,cstdlib 头文件定义了两个预处理变量:EXIT_SUCCESS 和EXIT_FAILURE 。
6.3.3返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较繁琐,但是通过类型别名可以简化这一任务:
typedef int arrT[10];
using arrT = int[10];
arrt* func(int i);
声明一个返回数组指针的函数
要想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度:Type (*function(parameter_list))[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++11新标准中还有一种可以简化返回数组指针的函数声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。尾置返回类型跟在列表后面并以一个-> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto :auto func(int i) -> int (*)[10] 。
使用decltype
如果知道函数返回的指针将指向哪个数组,就可以使用decltype 关键字声明返回类型:
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even;
}
arrPtr 使用关键字decltype 表示它的返回类型是个指针,并且该指针所指的对象与odd 的类型一致。因为odd 是数组,所以arrPtr 返回一个指向含有5个整数的数组的指针。 有一个地方需要注意:decltype 并不负责把数组类型转换成对应的指针,所以decltype 的结果是个数组,要想表示返回指针还必须在函数声明时加一个* 符号。
6.4函数重载
定义重载函数
不允许两个函数除了返回类型外其他所有的要素都相同。
重载和const 形参
顶层const 不影响传入函数的对象。一个拥有顶层const 的形参无法和另一个没有顶层const 的形参区分开来。另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数的重载,此时的const 是底层的:
Record lookup(Account&);
Record lookup(const Account&);
Record lookup(Account*);
Record lookup(const Account*);
cast_const 和重载
const_cast 在重载函数的情景中最有用,例如:
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
函数的参数和返回类型都是const string 的引用。可以对两个非常量string 实参调用这个函数,但返回结果仍然是const string 的引用。因此,需要一种新的函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast 可以做到这一点:
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载的函数
当调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。
6.4.1重载与作用域
其实,重载对作用域的一般性质并没有什么改变:如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:
string read();
void print(const string&);
void print(double);
void footBar(int ival) {
bool read = false;
string s = read();
void print(int);
print("Value: ");
print(ival);
print(3.14);
}
一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。 在c++中,名字查找发生在类型检查之前。
6.5特殊用途语言特性
6.5.1默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
使用默认实参调用函数
如果想使用默认实参,只要在调用函数的时候省略该实参即可。函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如:
string window;
window = screen(, , '?');
window = screen('?');
当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
默认实参声明
虽然多次声明同一个函数是合法的,但是有一点要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值:
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*');
string screen(sz = 24, sz = 80, char);
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen();
void f2() {
def = '*';
sz wd = 100;
window = screen();
}
6.5.2内联函数和constexpr 函数
内联函数可以避免函数调用的开销
将函数指定为内联函数,通常就是将它在每个调用点上"内联地"展开。在函数的返回类型前面加上关键字inline ,这样就可以将它声明成内敛函数了:
inline const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,并且很多编译器都不支持内联递归函数。
constexpr 函数
constexpr 函数是指能用于常量表达式的函数。定义这样的函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return 语句:
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();
为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。当然,constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行,例如,可以有空语句、类型别名以及using 声明。 允许constexpr 函数的返回值并非一个常量:
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
当函数的实参是常量表达式时,它的返回值也是常量表达式;反之则不然:
int arr[scale(2)];
int i = 2;
int a2[scale(i)];
如果用一个非常量表达式调用constexpr 函数,比如int 类型的对象i,则返回值是一个非常量表达式。
把内联函数和constexpr 函数放在头文件内
和其他函数不一样,内联函数和constexpr 函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr 函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr 函数通常定义在头文件中。
6.5.3调试帮助
C++有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert 和NDEBUG 。
assert 预处理宏
assert 是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件:
assert(expr);
首先对表达式求值,如果表达式为假(即0),则输出信息并终止程序的执行。如果表达式为真(即非0),assert 什么也不做。 和预处理变量一样,宏名字在程序内必须唯一。含有cassert 头文件的程序不能再定义名为assert 的变量、函数或者其他实体。在实际编程过程中,即使没有包含cassert 头文件,也最好不要为了其他目的使用assert 。很多头文件都包含了cassert ,这就意味着即使没有直接包含cassert ,它也很有可能通过其他途径包含到程序中。 assert 宏常用于检查不能发生的条件。
NDEBUG 预处理变量
assert 的行为依赖于一个名为NDEBUG 的预处理变量的状态。如果定义了NDEBUG ,则assert 什么也不做。默认状态下没有定义NDEBUG ,此时assert 将执行运行时检查。 可以使用一个#define 语句定义NDEBUG ,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项可以定义预处理变量。 定义NDEBUG 能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert 应该仅用于验证那些确实不可能发生的事情。可以把assert 当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。 除了用于assert 外,也可以使用NDEBUG 编写自己的条件调试代码。如果NDEBUG 未定义,将执行#ifndef 和#endif 之间的代码;如果定义了NDEBUG ,这些代码将被忽略:
void print(const int ia[], size_t size) {
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif
}
预处理器定义了几个对于程序调试很有用的名字:
__func__ :输出当前调试的函数的名字。__FILE__ :存放文件名的字符串字面值。__LINE__ :存放当前行号的整型字面值。__TIME__ :存放文件编译时间的字符串字面值。__DATE__ :存放文件编译日期的字符串字面值。
6.6函数匹配
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名;二是其声明在调用点可见。 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6);
寻找最佳匹配(如果有的话)
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。基本思想是,实参类型与形参类型越接近,它们匹配得越好。
含有多个形参的函数匹配
f(42, 2.56);
编译器因为函数调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了最好的匹配,从整体式无法判断孰优孰劣。看起来似乎可以通过强制类型转换其中的一个实参来实现函数匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。
6.6.1实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级:
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 向实参添加顶层
const 或者从实参中删除顶层const 。
- 通过
const 转换实现的匹配。 - 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
需要类型提升和算术类型转换的匹配
小整形一般都会提升到int 类型或更大的整数类型。假设有两个函数,一个接受int ,另一个接受short ,则只有当调用提供的是short 类型的值时才会选择short 版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int 类型;此时使用short 版本反而会导致类型转换:
void ff(int);
void ff(short);
ff('a');
所有算术类型转换的级别都一样。例如:从int 向unsigned int 的转换并不比从int 向double 的转换级别高。例如:
void mainip(long);
void mainip(float);
mainip(3.14);
函数匹配和const 实参
如果重载函数的区别在于它们的引用类型的形参是否引用了const ,或者指针类型的形参是否指向const ,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
6.7函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:bool lengthCompare(const string &, const string &); 。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:bool (*pf)(const string &, const string &); 。
使用函数指针
当把函数名作为一个值使用时,该函数自动地转换成指针。例如:
pf = lengthCompare;
pf = &lengthCompare;
此外,还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool p1 = pf("hello", "goodbye");
bool p2 = (*pf)("hello", "goodbye");
bool p3 = lengthCompare("hello", "goodbye");
在指向不同函数类型的指针间不存在转换规则。但是和往常一样,可以为函数指针赋一个nullptr 或者值为0的整型常量表达式,表示该指针没有指向任何一个函数。
重载函数的指针
当使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配:
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff;
void (*pf2)(int) = ff;
double (*pf3)(int*) = ff;
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &);
可以直接把函数作为实参使用,此时它会自动转换成指针。直接使用函数类型显得冗长而繁琐。类型别名和decltype 能简化使用了函数指针的代码:
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;
typedef bool(*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *FuncP2;
返回指向函数的指针
和数组类型,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
using F = int(int*, int);
using PF= int(*)(int*, int);
必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针:
PF f1(int);
F f1(int);
F *f1(int);
当然,也能直接用这样的形式直接声明:int (*f1(int))(int*, int); 。可以看到,f1 有形参列表,所以f1 是个函数;f1 前面有* ,所以f1 返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int 。另一方面,也可以使用尾置返回类型的方式声明一个返回函数指针的函数:auto f1(int) -> int (*)(int*, int); 。
将auto 和decltype 用于函数指针类型
如果明确知道返回的函数是哪一个,就能使用decltype 简化书写函数指针返回类型的过程:
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
decltype(sumLength) *getFcn(const string&);
|