「生活可以更简单, 欢迎来到我的开源世界」
  1. 6.1 函数基础
    1. 6.1.1 局部对象
    2. 6.1.2 函数声明
    3. 6.1.3 分离式编译
  2. 6.2 参数传递
    1. 6.2.1. 传值参数
    2. 6.2.2 传引用参数
    3. 6.2.3 const形参和实参
    4. 6.2.4 数组形参
    5. 6.2.5 main:处理命令行选项
    6. 6.2.6 含有可变形参的函数
  3. 6.3 返回类型和return语句
    1. 6.3.1 无返回值函数
    2. 6.3.2 有返回值函数
    3. 6.3.3 返回数组指针
  4. 6.4 函数重载
    1. 6.4.1 重载与作用域
  5. 6.5 特殊用途语言特性
    1. 6.5.1 默认实参
    2. 6.5.2 内联函数和constexpr函数
    3. 6.5.3 调试帮助
  6. 6.6 函数匹配
    1. 6.6.1 实参类型转换
  7. 6.7 函数指针
C++ Primer 第6章 函数
2018-06-16
」 「

函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个返回值。可以重载函数,也就是说一个名字可以对应几个不同的函数。

6.1 函数基础

典型的函数包含:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。

通过调用运算符来执行函数,调用运算符是一对圆括号,作用于一个表达式,该表达式是函数或者指向函数的指针。

函数的调用完成两项工作:

  1. 用实参初始化函数的形参,执行函数的第一步是(隐式地)定义并初始化它的形参
  2. 将控制权转移给调用函数,此时,主调用函数的执行暂时中断,被调函数开始执行

当遇到一条return语句时函数结束执行过程,完成两项工作:

  1. 返回return语句中的值(如果有的话),函数的返回值用于初始化调用表达式的结果
  2. 将控制权从被调函数转移回主调函数

实参是形参的初始值,尽管实参和形参存在对应关系,但并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。实参类型必须与形参类型匹配(可以存在隐式转换),函数有几个形参就必须提供相同数量的实参,因为函数的调用规定实参数量应与形参数量一致,所以形参一定会被初始化。

函数的形参列表可以为空,但是不能省略:

void f1(){/*...*/}	//隐式地定义空形参列表
void f2(void){/*...*/} //显式地定义空形参列表,为了与C语言兼容

形参列表中形参通过逗号隔开,每个形参都有类型声明符,即使两个形参一样,也不可省略。任意两个形参不能同名。

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值,函数返回值不能是数组类型或函数类型,但是可以是指向数组或函数的指针。

6.1.1 局部对象

在C++语言中,名字有作用域,对象有生命周期

函数体是一个块,块构成一个新的作用域。形参和函数体内部定义的变量统称局部变量,仅在函数的作用域内可见,局部变量会隐藏在外层作用域中同名的其他所有声明中。

只存在于块执行期间的对象称为自动对象。形参是一种自动对象,传递给函数的实参初始化形参对应的自动对象,对于举报变量对应的自动对象来说,分两种情况:

  1. 变量定义本身含有初始值,则使用初始值进行初始化
  2. 变量定义本身不含初始值,执行默认初始化。意味着内置类型的未初始化局部变量将产生未定义的值

局部静态变量:在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,通过将变量定义成 static类型得到。如果局部静态变量没有显示的初始值,将执行默认初始化,内置类型的局部静态变量初始化为0。

6.1.2 函数声明

和其他名字一样,函数的名字必须在使用之前声明。函数只能定义一次,但是可以声明多次。如果一个函数永远不会被我们用到,那么它可以只有声明没有定义。

函数的声明和定义非常相似,唯一的区别是函数声明无须函数体,用一个分号代替即可。

因为函数的声明不包含函数体,所以也就无须形参名字。在函数声明中经常省略形参的名字,尽管如此,写上形参名字有助于理解函数功能。

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型

建议变量、函数在头文件中声明,在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(WIndows)或.o(Unix)的文件,后缀名的含义是该文件包含对象代码。

编译器负责把对象文件链接形成可执行文件。

6.2 参数传递

每次调用函数时会重新创建它的形参,并用传入的实参进行初始化。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用

6.2.1. 传值参数

函数对形参所做的所有操作都不会影响实参。

指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针,因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针。

6.2.2 传引用参数

通过使用引用形参,允许函数改变一个或多个实参的值。

使用引用可以避免拷贝:对有些大的类类型对象或者容器对象进行拷贝是比较低效的,甚至有的类型(包括IO类型在内)根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问改类型的对象。

如果函数无须改变引用形参的值,最好将其声明为常量引用。

一个函数每次只能返回一个值,然而有时候函数同时返回多个值,引用形参为我们一次返回多个结果提供了有效途径。

6.2.3 const形参和实参

顶层const作用于对象本身,和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。

在C++语言中,允许我们定义若干具有相同名字的函数,但是不同函数的形参列表应该有明显的区别。

void fcn(const int i){}
void fcn(int i){} //错误:重复定义了fcn(int)

我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。C++允许我们用字面值初始化常量引用。尽量使用常量引用,可以避免一些不经意的修改。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:

  1. 不允许拷贝数组
  2. 使用数组时(通常)会将其转换成指针

因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数,因为数组会转换成指针,所以我们为函数传递一个数组时,实际上传递的是执行数组首元素的指针。

尽管不能以值传递的方式传递数组,但是我们可以把形参写出类似数组的形式:

//尽管形式不同,但这三个printf函数是等价的
//每个函数都有一个const int* 类型的形参
void printf(const int*);
void printf(const int[]); //可以看出函数的意图是作用于一个数组
void printf(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定

当我们传给printf函数一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术:

  1. 使用标记指定数组长度:要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串
  2. 使用标准库规范:传递指向数组首元素和尾后元素的指针,标准库begin和end函数提供所需指针
  3. 显示传递一个表示数组大小的形参

C++语言允许将变量定义成数组的引用,形参也可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上。因为数组大小是构成数组类型的一部分,所以形参声明的数组维度必须与实参的数组维度一致。

C++实际上没有真正的多维数组,和所有数组一样,当多维数组传递给函数时,真正传递的是指向数组首元素的指针。

void print(int (*martix)[10], int rowSize){}	//指向含有10个整数的数组的指针
//等价于
void print(int matrix[][10], int rowSize){}
//第一个维度会被编译器忽略,所以最好不要把它包括在形参列表内

int *matrix[10]; //10个指针构成的数组
int (*matrix)[10]; //指向含有10个整数的数组的指针

6.2.5 main:处理命令行选项

int main(int argc, char *argv[]){}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。

当实参传给main函数后,argv第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

6.2.6 含有可变形参的函数

有时候我们无法提前预知应该向函数传递几个实参,为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  1. 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
  2. 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参,一般只用于与C函数交互的接口程序。

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参,initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,定义在同名头文件中。

image-20200617175355468

和vector一样是模板类型,但是initializer_list对象中的元素永远是常量值,无法改变。如果想向initial

6.3 返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。

6.3.1 无返回值函数

没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求一定要有return语句,因为在这类函数的最后会隐式地执行return。

通常情况,void函数如果想要在函数中间提前退出,可以使用return语句。

一个返回类型是void的函数可以返回一个返回void的函数。强行令void函数返回其它类型的表达式将产生编译错误。

6.3.2 有返回值函数

只要函数的返回类型不是void,则函数内每条return语句必须返回一个值,值的类型必须和返回类型一致,或能隐式地转换。

C++无法确保结果的正确性,但保证每个return语句的结果类型正确。编译器尽量确保具有返回值的函数只能通过一条有效的return语句退出。

在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word, const string &ending){
return (ctr > 1) ? word + ending : word;
}

函数返回类型是string,返回值将被拷贝到调用点,该函数返回word的副本或一个未命名的临时string对象。

同其它引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名:

//挑出两个string对象中较短的那个,返回其引用
const string &shortString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1 : s2;
}

形参和返回类型都是const string的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。

不要返回局部对象的引用或指针,函数完成后,它所占用的存储空间将被释放,函数终止意味着局部变量的引用或指针将指向无效的内存区域。

调用运算符的优先级与点运算符和箭头运算符相同,并且符合左结合律:

auto sz = shortString(s1, s2).size();

函数返回类型决定函数是否是左值:调用一个返回引用的函数得到左值,其它返回类型得到右值。可以像使用其它左值那样来使用返回引用的函数调用:

char &get_val(string &str, string::size_type ix){
return str[ix];
}
int main(){
string s("a value");
cout << s << end;
get_val(s, 0) = 'A';
cout << s << endl;
return 0;
}

如果返回的是常量引用则不能给调用的结果赋值。

C++11新标准规定,函数可以返回花括号包围的值的列表,类似其它返回结果,此列表也用来对表示函数返回的临时量进行初始化,如果列表为空,临时量执行值初始化,否则,返回的值由函数的返回类型决定。

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

如果main函数的返回类型不是void,一般函数必须返回一个值,但是允许main函数没有返回语句直接结束:如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。

main函数的返回值可以看作是状态指示器:返回0表示执行成功,返回其它值表示执行失败,非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,分别表示成功与失败:

int main(){
if(some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}

因为它们是预处理变量,不能在前面加上std::,也不能在using声明中出现。

当一个函数调用了它自身,不管调用是直接的还是间接的,都称该函数为递归函数。在递归函数中,一定有某条路径是不包含递归调用的,否则函数将不断调用它自身直到程序栈空间耗尽为止。有时候会称这种函数含有递归循环。

6.3.3 返回数组指针

因为数组不能被拷贝,所有函数不能返回数组,但可以返回数组的指针或引用。

使用类型别名简化声明:

typedef int arrT[10];//arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];//等价
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
int arr[10];	//含有10个整数的数组
int *p1[10]; //含有10个整型指针的数组
int (*p2)[10] = &arr; //p2是一个指针,指向含有10个整数的数组

因此,声明一个返回数组指针的函数的格式如:

Type (*function(paramcter_list))[dimension]

Type:表示元素的类型

dimension:表示数组的大小

例子:int (*func(int i))[10];

C++11新标准可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,对于返回类型复杂的函数最有效。尾置返回类型跟在形参列表后面以一个->符号开头:

auto func(int i) -> int (*)[10]

如果我们知道函数返回的指针指向哪个数组,就可以使用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; //返回一个指向数组的指针
}

6.4 函数重载

如果一个作用域内的几个函数名字相同但形参列表不同,称之为重载函数。重载函数的形参类型不一样,但是执行的操作非常类似。

函数的名字仅仅是让编译器知道它是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。

main函数不能重载。重载函数应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其它所有的要素都相同。两个函数,形参列表一样但是返回类型不同,那么第二个函数的声明是错误的。

有时候两个形参列表看起来不一样,但实际上是相同的:

//每对声明的是同一函数
Record lookup(const Account &acct);
Record lookup(const Account &);//形参名字仅仅起到帮助记忆的作用,不影响形参列表的内容

typedef Phone Telno;//类型别名,实质相同
Record lookup(const Phone &);
Record lookup(const Telno &);

顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:

Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const); //重复声明了Record lookup(Phone*)

如果形参是某种类型的指针或引用,则通过区分其执行的是常量对象还是非常量对象可以实现函数重载,即底层const可区分:

//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup(Account &);
Record lookup(const Account &);

Record lookup(Account *);
Record lookup(const Account *);

上面四个函数中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其它类型,所以const对象只能传递给const形参;而非常量可以转换成const,但是编译器会优先选择非常量版本的函数。

函数重载最好是重载那些非常相似的操作,可以减轻命名负担。

const_cast在重载函数的情况下最有用:

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<cosnt string&>(s2));
return const_cast<string&>(r);
}
//这样使用是安全的

定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定

调用重载函数的三种可能结果:

6.4.1 重载与作用域

重载对作用域的一些性质并没有发生什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同作用域中无法重载函数名。

string read();
void print(const string &);
void print(double); //重载print函数
void fooBar(int ival);
{
bool read = false; //新作用域:隐藏外层read
string s = read(); //错误:read是一个布尔值
//不好的习惯:通常来说,局部作用域声明函数不是一个好的选择
void print(int); //新作用域:隐藏了之前的print
print("Value: "); //错误:void print(const string &)被隐藏了
print(ival); //正确:当前print(int)可见
print(3.14); //正确:调用局部print(int),隐藏了外层print(double)
}

当我们调用print函数时,编译器先寻找对该函数名的声明,一旦在当前作用域找到了所需的名字,编译器就会忽略外层作用域的同名实体,剩下的工作就是检查函数调用是否有效。

在C++语言中,名字查找发生在类型检查之前。

6.5 特殊用途语言特性

6.5.1 默认实参

某些函数有这样一种形参,在函数的很多次调用它们都被赋予一个相同的值,我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 40, char backgrnd = ' ');

一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

如果我们使用默认实参,只要在调用函数的时候省略该实参就可以了:

string window;
window = screen(); //等价window = screen(24, 80, ' ');
window = screen(66); //等价window = screen(66, 80, ' ');
window = screen(66,256); //等价window = screen(66, 256, ' ');
window = screen(66,256,'#'); //等价window = screen(66, 256, '#');

函数调用时实参按其位置解析,默认参数负责填补函数调用缺少的尾部实参(靠右侧位置)。设计含有默认实参的函数时,一项任务是合理设置形参的顺序,让那些经常使用默认值的形参出现在后面。

函数声明多次是合法的,因此,在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都已经有默认值。即默认实参可以分多次函数声明分别赋值,而且必须从右侧开始。一般一个函数只声明一次,而且声明放在头文件中。

局部变量不能作为默认实参,其它只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。

//wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); //调用screen(ht(), 80, ' ');

void f2(){
def = '*'; //改变默认实参的值
sz wd = 100; //隐藏了外层定义的wd,但是没有改变默认值
window = screen(); //调用screen(ht(), 80, '*')
}

6.5.2 内联函数和constexpr函数

在大多数机器上,一次函数调用包含一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可以避免函数调用的开销,将函数指定为内联函数,通常就是将它在每个调用点上“内联地展开”,消除函数运行时的开销。在函数定义的返回类型之前加上关键字inline即可将它声明为内联函数,但是,内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数是指能用于常量表达式的函数。定义constexpr函数要遵循几项约定:函数的返回类型以及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

constexpr int new_sz(){return 42;}
constexpr int foo = new_sz();

执行初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地定义为内联函数。

constexpr函数体可以包含其它语句,只要这些语句在运行时不执行任何操作就行,如空语句、类型别名、using声明。

允许constexpr函数的返回值并非一个常量:constexpr函数不一定返回常量表达式

constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
//如果arg是常量表达式,则scale(arg)也是常量表达式
//当scale的实参是常量表达式时,它的返回值也是常量表达式,反之则不然
int arr[scale(2)]; //正确:scale(2)是常量表达式
int i = 2; //i不是常量表达式
int a2[scale(i)]; //错误:scale(i)不是常量表达式

和其它函数不一样,内联函数和constexpr函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

6.5.3 调试帮助

C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是:程序包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。

这种方法用到两项预处理功能:assert和NDEBUG

assert是一种预处理宏。预处理宏是一个预处理变量,它的行为类似于内联函数。assert宏使用一个表达式作为它的条件:assert(expr)

对expr求值,如果表达式为假(即0),assert输出信息并终止程序执行;如果为真(即非0),assert什么也不做。

assert宏定义在cassert头文件中。预处理名字由处理器而非编译器管理,使用预处理名字无须提供using声明。和预处理变量一样,宏名字在程序内必须唯一,含有cassert头文件的程序不能再定义名为assert的变量、函数或其它实体。许多头文件都包含了cassert头文件,即使没有直接包含,也可能通过其它途径包含。

assert宏常用于检查“不能发生”的条件。

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

可以使用一个#define语句定义NDEBUG,从而关闭调试状态,也可通过编译器提供的选项定义预处理变量。

assert可以当成调试程序的一种手段,但是不能代替真正运行时的逻辑检查和程序应该包含的错误检查。

NDEBUG除了用于assert外,还可以用于编写条件调试代码:

void print(const int ia[], size_t size){
#ifndef NDEBUG
//__func__是编译器定义的一个局部静态变量,用于存放函数名字
cerr << __func__ << ": array size is " << size << endl;
#endif
//...
}

编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。

C++编译器定义的其它四个对于程序调试很有用的名字:

6.6 函数匹配

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //调用void f(double, double = 3.14)

函数匹配步骤:

  1. 选定本次调用对应的重载函数集,集合中的函数称为候选函数

    • 与被调用的函数同名
    • 其声明在调用点可见
  2. 考察本次调用提供的实参,从候选函数选出可行函数

    • 形参数量与调用提供的实参数量匹配
    • 每个实参的类型与对应的形参类型相同,或者能转换成形参类型
  3. 寻找最佳匹配(如果有的话):实参类型与形参类型越接近,匹配越好

    含有多个形参的函数匹配,编译器依次检查每个实参,如果有且只有一个函数满足条件则匹配成功:

    • 该函数每个实参的匹配都不劣于其它可行函数需要的匹配
    • 至少有一个实参的匹配优于其它可行函数提供的匹配

    即编译器检查的每个实参最匹配的函数是同一个才能匹配。如果不能,编译器最终因为调用具有二义性而聚集请求。看起来可以用强制类型转换来实现函数匹配,但是设计良好的系统中,不应该对实参进行强制类型转换。

6.6.1 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序为:

  1. 精确匹配
    1. 实参类型和形参类型相同
    2. 实参从数组类型或函数类型转换成对应的指针类型
    3. 向实参添加顶层const或者从实参中删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

分析函数调用前,必须知道小整型一般都会提升到int类型或更大的整数类型。假设有两个函数,一个接受int,一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。然而有时候即使实参是一个很小的整数,也会直接提升为int整型,此时用short版本反而会导致类型转换:

void ff(int);
void ff(short);
ff('a'); //char提升为int,调用ff(int)

所有算术类型转换的级别都一样,从int向unsigned int转换并不比从int向double的转换高级:

void manip(long);
void manip(float);
manip(3.14); //错误:二义性调用

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否常量来决定选择哪个函数:

Record lookup(Account&);
Record lookup(const Account&);
const Account a;
Account b;

lookup(a); //调用Record lookup(const Account&);
lookup(b); //调用Record lookup(Account&);

第一个调用中,传入const对象,因为不能把普通引用绑定到const对象上,所以只有一个函数匹配。

第二个调用中,传入非常量对象b,两个函数都是可行函数,然而用非常量对象初始化常量引用需要类型转换,所以接受非常量引用的函数与b精确匹配。

指针类型的形参也类似:如果实参是指向常量的指针,调用形参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。

6.7 函数指针

函数指针指向的是函数而非对象,和其它指针一样,函数指针指向某种特定的类型,函数的类型由返回类型和形参类型共同决定,与函数名无关:

bool lengthCompare(const string &, const string &);

该函数的类型是bool(const string &, const string &)。要声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

bool (*pf)(const string &, const string &);//返回类型是bool
bool *pf(const string &, const string &);//返回类型是bool*

//当我们把函数名作为一个值使用时,该函数自动地转换成指针
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = lengthCompare; //等价的赋值语句:取地址符是可选的

//通过函数指针调用该函数,无须提前解引用指针
bool b1 = pf("hello", "goodbye"); //调用lengthCompare函数
bool b2 = (*pf)pf("hello", "goodbye");//等价的调用:解引用符是可选的
bool b3 = lengthCompare("hello","goodbye");//另一个等价的调用

//可以为函数指针赋值nullptr或者值为0的常量表达式
//表示该指针没有指向任何一个函数
pf = 0;
pf = nullptr;

string::size_type sumLength(const string&, const string&);
bool castringCompare(const char*, const char*);
pf = sumLength; //错误:返回类型不匹配
pf = cstringCompare; //错误:形参类型不匹配
pf = lengthCompare; //正确:函数和指针类型精确匹配

使用重载函数时,上下文应该清晰地界定到底应该使用哪个函数,如果定义了函数指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数在中的某一个精确匹配:

void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)
void (*pf2)(int) = ff; //错误:没有任何一个ff与该形参列表匹配
double (*pf2)(int*) = ff; //错误:ff和pf3的返回类型不匹配

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用:

//第3个参数是函数类型,它会自动转换成指向函数的指针
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 &));

//可以直接把函数作为实参使用,它会自动转换成指针
useBigger(s1, s2, lengthCompare);//自动将函数lengthCompare转换成指向函数的指针

直接使用函数指针类型显得冗长而烦琐,类型别名和decltype能简化使用函数指针:

//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) *Func2; //等价的类型

decltype返回函数类型,不会将函数类型自动转换成指针类型,只有在结果前面加上*才能得到指针。

可以使用如下的形式重新声明useBigger:

//useBigger的等价声明,使用了类型别名
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);

和数组类似,虽然不能返回一个函数,但是能返回一个指向函数类型的指针。然而,我们必须把返回类型写出指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理

要使用一个返回函数指针的函数,最简单的方法是使用类型别名:

using F = int(int*, int);		//F是函数类型,不是指针
using FF = int(*)(int*, int); //FF是指针类型

FF f1(int); //正确:FF是指向函数的指针,发f1返回指向函数的指针
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F *f1(int); //正确:显示地指定返回类型是指向指向函数的指针

int (*f1(int))(int *, int); //最原始的声明方法,由内而外阅读

由内而外的顺序阅读这条声明语句:f1有形参列表,所以f1是一个函数;f1前面有*,所以f1返回一个指针;指针的类型也包含形参列表,因此指针指向函数,该函数的返回类型是int。

除此外,还可使用尾置返回类型的方式声明一个返回函数指针的函数:

auto f1(int) -> int (*)(int*, int);

如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程:

string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
decltype(sumLength) *getFcn(cosnt string &);
<⇧>