文章目录
6.6 函数匹配
当几个重载函数的形参数量相等以及某些形参的类型可以由其它类型转换得到时,往往很难确定某次调用应该选择哪种函数,这时编译器就需要在进行函数调用前进行 函数匹配。例如:
void f();
void f(int);
void f(int, int);
void f(double, double=3.14);
f(6.28); // 调用void f(double, double)
【函数匹配的过程】
以上面的例子为例,函数匹配的过程一般分为几步:
(1)确定候选函数
选定本次调用对应的重载函数集,集合中的函数称为 候选函数。候选函数有两个特性:
A. 与被调用的函数同名;
B. 其声明在调用点可见。
在上面的例子中,有4个名为 f 的候选函数。
(2)确定可行函数
考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为 可行函数 。可行函数也有两个特征:
A. 形参数量与本次调用提供的实参数量相等;
B. 每个实参的类型与对应的形参类型相同,或者可以转换成形参的类型。
在上面的例子中,我们可以根据实参的数量从候选函数中排除掉2个:
void f(); // 排除:形参为0个
void f(int); // 可行:实参double可以转换成int
void f(int, int); // 排除:形参有2个
void f(double, double=3.14);// 可行:第二个形参提供了默认值,第一个形参与实参的类型相同
f(6.28); // 只提供了1个实参
(3)寻找最佳匹配(如果有的话)
从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
上例中,调用只提供了1个实参,类型为double,在可行函数中寻找最佳匹配:
void f(int); // 如果调用它,则实参得从double转成int
void f(double, double=3.14); // 与实参精确匹配
f(6.28); // 精确匹配比需要类型转换的匹配更好,所以调用的是void f(double, double)
【含有多个实参的函数匹配】
当实参的数量有2个或更多时,函数匹配就比较复杂了。
在这种情况下,选择可行函数的方法 和只有一个实参时是一样的,编译器选择那些 形参数量满足且实参类型和形参类型能匹配的函数。 接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配:
(1)如果有且只有一个函数满足下列条件,则 匹配成功:
A. 该函数每个实参的匹配都不劣于其它可行函数需要的匹配;
B. 至少有一个实参的匹配优于其他可行函数提供的匹配。
(2)如果检查了所有实参之后,没有任何一个函数脱颖而出,则该调用是 错误 的,编译器将报告 二义性调用 的信息。
例如:
void f();
void f(int);
void f(int, int);
void f(double, double=3.14);
f(42, 6.28);
// 对于第1个参数来说,f(int, int) 优于 f(double, double)
// 对于第2个参数来说,f(double, double) 优于 f(int, int)
// 编译器最终将因为这个调用具有二义性而拒绝其请求
6.6.1 实参类型转换
【需要类型提升和算术类型转换的匹配】
首先,我们要明确一点,小整形一般都会提升到 int 类型或更大的整数类型。例如,假设有两个函数,一个接受 int、另一个接受 short,那么只有当调用提供的是 short 类型的值时才会选择 short 版本的函数:
void func(short);
void func(int);
func('a'); // char提升为int,调用func(int)
【注意】所有算术类型转换的级别都一样。例如从 int 向 unsigned int 的转换并不比从 int 向 double的转换高级:
void func(long);
void func(float);
func(3.14); // 错误:3.14的类型是double,它既能转成long,也能转成float,这时存在二义性调用
【函数匹配和const实参】
如果重载函数的区别在于它们的引用类型的形参是否引用了 const,或者指针类型的形参是否指向了 const,则当调用发生时,编译器通过实参是否是常量来判断选择哪个函数:
void func(const int&); // 参数是常量引用
void func(int&); // 参数是int的引用
const int i;
int j;
func(i); // 调用func(const int&)
func(j); // 调用func(int&)
指针类型的形参也类似。如果两个函数的唯一区别是 它的指针形参指向常量或非常量,则编译器通过实参是否是常量决定选用哪个函数;
(1)如果两个函数的唯一区别是 它的指针形参指向常量或非常量,则编译器通过实参是否是常量决定选用哪个函数;
(1)如果实参是指向常量的指针,调用形参是 const* 的函数;
(2)如果实参是指向非常量的指针,调用形参是普通指针的函数。
6.7 函数指针
**函数指针指向的是函数而非对象。**和其它指针一样,函数指针指向 某种特定类型。函数的类型由它的 返回类型和形参类型 共同决定,与函数名无关。例如:
bool cmp(const string &i, const string &j);
// 该函数的类型为 bool(const string &i, const string &j)
要想声明一个可以指向该函数的指针,只需用指针替换函数名即可:
// ptr指向一个函数,该函数的两个参数为const string的引用,返回类型为bool
bool (*ptr)(const string &i, const string &j); // 未初始化
【使用函数指针】
当我们把一个函数名作为一个值使用时,该函数自动转换成指针。例如:
bool cmp(const string &i, const string &j);
bool (*ptr)(const string &i, const string &j);
// 下面两条语句等价
ptr = cmp; // ptr指向名为cmp的函数
ptr = &cmp; // 等价的赋值语句:取地址符可选
此外,我们可以 直接使用指向函数的指针调用该函数,无需提前解引用指针:
bool cmp(const string &i, const string &j);
bool (*ptr)(const string &i, const string &j);
ptr = cmp;
bool b1 = ptr("hi", "hellow"); // 调用cmp函数
bool b2 = (*ptr)("hi", "hellow"); // 一个等价调用
bool b3 = cmp("hi", "hellow"); // 另一个等价调用
我们可以为函数指针赋一个 nullptr 或者值为 0 的整型常量表达式,表示该指针没有指向任何一个函数。
【重载函数的指针】
当使用重载函数时,如果定义了指向重载函数的指针,指针的类型必须与重载函数中的一个精确匹配:
void func(int*);
void func(char);
void (*ptr)(int*) = func; // 正确:ptr指向函数func(int*)
void (*ptr1)(int) = func; // 错误:没有任何一个func与该形参列表匹配
double (*ptr2)(int*) = func; // 错误:返回类型不匹配
【返回指向函数的指针】
我们可以返回函数类型的指针,但要注意的是,编译器不会自动将函数返回类型当成对应的指针类型处理,因此需要把返回类型写成指针形式。
要声明一个返回函数指针的函数,最简单的方法是使用 类型别名:
using F = int(int*, int); // F是函数类型,不是指针
using FP = int (*)(int*, int); // FP是指针类型
FP f1(int); // 正确:FP是指向函数的指针,函数f1返回指向函数的指针
F f1(int); // 错误:F是函数类型,f1不能返回函数类型
F *f1(int); // 正确:显示地指定返回类型是指向函数的指针
我们也可以利用如下形式来直接声明 f1:
int (*f1(int))(int*, int);
按照从内而外的顺序阅读这条声明语句:
(1)f1 有形参列表,所以 f1 是一个函数;
(2)f1 前面有*,说明它的返回值是一个指针;
(3)进一步观察,指针的类型本身也包含形参列表,因此该指针指向函数;
(4)最后,可以直到该函数的返回类型是 int。
当然,我们也可以用尾置返回类型的方式来声明一个返回函数指针的函数:
auto f1(int) -> int (*)(int*, int);