声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
本文主要包括:重载函数的定义、调用和作用域,以及寻找最佳函数匹配的具体说明。
目录
重载函数(overloaded functions):在同一作用域内,几个函数有相同的名字,不同的形参列表(不同的形参数量或形参类型)。
1 定义重载函数
// 创建几个不同的函数,分别根据账户号、电话、名字信息查找记录
Record lookup(const Account &); // find by Account
Record lookup(const Phone &); // find by Phone
Record lookup(const Name &); // find by Name
// 编译器根据实参的类型确定应该调用哪一个函数
Account acct;
Phone phone;
Record r1 = lookup(acct); // call version that takes an Account
Record r2 = lookup(phone); // call version that takes a Phone
// 不允许两个函数除了返回类型外其他所有的要素都相同
Record lookup(const Account &);
bool lookup(const Account &); // error: only the return type is different
1.1 相同的形参
有时候两个形参列表看起来不一样,但实际上是相同的:
- 省略 vs 不省略形参的名字
- 类型 vs 类型别名
// each pair declares the same function
Record lookup(const Account &acct);
Record lookup(const Account &); // parameter names are ignored
typedef Phone Telno;
Record lookup(const Phone &);
Record lookup(const Telno &); // Telno and Phone are the same type
1.2 顶层 const
和重载
顶层 const
作用于对象本身,当用实参初始化形参时,会忽略掉顶层 const
。所以,一个拥有顶层 const
的形参无法和另一个没有顶层 const
的形参进行区分。
// 在这两组函数声明中,每一组的第二个声明和第一个声明是等价的
Record lookup(Phone);
Record lookup(const Phone); // redeclares Record lookup(Phone)
Record lookup(Phone *);
Record lookup(Phone *const); // redeclares Record lookup(Phone*)
1.3 底层 const
和重载
如果形参是某种类型的指针或引用,则通过区分其指向的是 const
对象还是 非const
对象可以实现函数重载。
在调用过程中,编译器可以通过实参是否是常量来推断应该调用哪个函数:
- 只能把
const
对象(或指向const
的指针)传递给const
形参; - 可以把 非
const
对象(或指向 非const
的指针)传递给所有形参,但会优先选用 非const
形参。
// functions taking const and nonconst references or pointers have different parameters
// declarations for four independent, overloaded functions
Record lookup(Account &); // function that takes a reference to Account
Record lookup(const Account &); // new function that takes a const reference
Record lookup(Account *); // new function, takes a pointer to Account
Record lookup(const Account *); // new function, takes a pointer to const
关于顶层
const
和底层const
(指针:本身是一个对象,又可以指向另外一个对象)顶层
const
(top-level const)
- 表示指针本身是常量;
- 扩展表示任意的对象是常量。
底层
const
(low-level const)
- 表示指针所指的对象是常量;
- 扩展表示引用等复合类型的基本类型部分是常量。
指针类型:既可以是顶层
const
,又可以是底层const
。
2 const_cast
和重载
最常应用 const_cast
的情境是在重载函数中:
// return a reference to the shorter of two strings
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));
// 安全地将 const string & 类型的对象 r 转回其原本的 string & 类型
return const_cast<string &>(r);
}
3 调用重载的函数
函数匹配(function matching,也叫做重载确定 overloaded resolution):把函数调用与一组重载函数中的某一个关联起来。
在调用重载函数时,有三种可能的结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码;
- 编译器找不到任何一个函数与调用的实参匹配,此时发出无匹配(no match)的错误信息;
- 编译器找到多个函数可以匹配,但是没有一个明显的最佳选择,此时发出二义性调用(ambiguous call)的错误信息。
函数匹配的步骤包括:
- 根据函数名,选定本次调用对应的候选函数(重载函数集合);
- 根据参数数量和类型,从候选函数中选出可行函数;
- 根据参数类型匹配情况,从可行函数中选择与本次调用最匹配的函数。
下文将以这组函数及其调用为例进行说明:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // calls void f(double, double)
3.1 确定候选函数
候选函数(candidate function),具备两个特征:
- 与被调用的函数同名;
- 其函数声明在调用点可见。
在示例代码中,对于函数调用 f(5.6)
,有 4 个名为 f
的候选函数。
3.2 确定可行函数
可行函数(viable function),具备两个特征:
- 其形参数量与本次调用提供的实参数量相等;
- 每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
如果没找到可行函数,编译器将报告无匹配函数(no match)的错误。
在示例代码中,对于函数调用 f(5.6)
,有 2 个可行函数,f(int)
和 f(double, double = 3.14)
。
3.3 寻找最佳匹配
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,由高到低排序如下:
- 精确匹配,包括:
- 实参类型和形参类型相同;
- 实参从数组类型或函数类型转换成对应的指针类型;
- 向实参添加顶层
const
或者从实参中删除顶层const
。
- 通过
const
转换实现的匹配; - 通过类型提升实现的匹配;
- 通过算术类型转换、指针转换实现的匹配;
- 通过类类型转换实现的匹配。
类类型转换参考后面章节:C++ 语言专题(一.07)操作重载与类型转换 => 9 重载、类型转换与运算符,
其它类型转换回顾:C++ 语言专题(一.02)指针与引用 => 3.1 隐式转换。
在示例代码中,对于函数调用 f(5.6)
,实参 5.6
的类型是 double
,与可行函数 f(double, double)
的形参类型精确匹配,因此,编译器解析成对 f(double, double = 3.14)
的调用。
3.3.1 需要类型提升的匹配
小整型一般都会提升到 int
类型或更大的整数类型。
void ff(int);
void ff(short);
ff('a'); // char promotes to int; calls f(int)
3.3.2 需要算数类型转换的匹配
所有算术类型转换的级别都一样。
void manip(long);
void manip(float);
manip(3.14); // 3.14 is a double; error: ambiguous call
3.3.3 函数匹配和 const
实参
如果重载函数的区别在于,它们的引用类型的形参是否引用了 const
,或者指针类型的形参是否指向了 const
,则当调用发生时,编译器通过实参是否是 const
来决定选择哪个函数。
Record lookup(Account &); // function that takes a reference to Account
Record lookup(const Account &); // new function that takes a const reference
const Account a;
Account b;
// 可行函数只有 lookup(const Account&)
lookup(a); // calls lookup(const Account&)
// 两个函数都是可行函数
// 接受非常量形参的函数与 b 精确匹配,匹配程度最高
// 用非常量对象初始化常量引用需要类型转换,即通过 const 转换实现的匹配,匹配程度相对低
lookup(b); // calls lookup(Account&)
3.4 含有多个形参的函数匹配
当实参的数量有两个或更多时,编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且仅有一个函数满足下列条件,则匹配成功:
- 每个实参的匹配程度,都不劣于其他可行函数 与相应实参的匹配(需要的匹配?);
- 至少有一个实参的匹配程度,优于其他可行函数 与此实参的匹配(提供的匹配?)。
如果没有任何一个函数脱颖而出,编译器将报告二义性调用(ambiguous call)的错误。
分析函数调用:f(42, 2.56);
在示例代码中,可行函数包括:f(int, int)
和 f(double, double)
。
对于第 1 个实参 42
,f(int, int)
精确匹配;
对于第 2 个实参 2.56
,f(double, double)
精确匹配。
因为每个可行函数各自在一个实参上实现了更好地匹配,从整体上无法判断孰优孰劣,所以编译器最终将因为这个调用具有二义性而报告错误。
4 重载与作用域
重载对作用域的一般性质并没有改变:
- 如果在内层作用域中声明名字,那么它将隐藏外层作用域中声明的同名实体;
- 在不同的作用域中无法重载函数名。
string read();
void print(const string &);
void print(double); // overloads the print function
void fooBar(int ival)
{
bool read = false; // new scope: hides the outer declaration of read
string s = read(); // error: read is a bool variable, not a function
// bad practice: usually it's a bad idea to declare functions at local scope
void print(int); // new scope: hides previous instances of print
// 在调用 print 函数时,编译器首先寻找对该函数名的声明,找到的是接受 int 值的局部声明
// 一旦找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体
// 剩下的工作就是检查函数调用是否有效(在 C++ 语言中,名字查找发生在类型检查之前)
print("Value: "); // error: print(const string &) is hidden
print(ival); // ok: print(int) is visible
print(3.14); // ok: calls print(int); print(double) is hidden
}
参考
- [美] Stanley B.Lippman著.C++ Primer 中文版(第5版).电子工业出版社.2013.
- [美] Stephen Prata著.C++ Primer Plus(第6版)中文版.人民邮电出版社.2012.
宁静以致远,感谢 Vico 老师。