参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
6.4 函数重载(P206)
在同一作用域内的几个函数名字相同但形参列表不同,称之为重载(overloaded)函数:
void print(const char *str);
void print(const int *beg, const int *end);
void print(const int arr[], size_t size);
函数重载可以缓解程序员起名字、记名字的负担。
main
函数不能重载
定义重载函数
不允许两个函数除了返回类型外其他要素都相同:
int func(int);
double func(int); // 错误
判断两个形参的类型是否相异
有时候两个形参列表看起来不同,但实际上却是相同的:
int func(int i);
int func(int);
using ll = long long;
int func(long long i);
int func(ll i);
重载和const
形参
顶层 const
不能重载,底层 const
可以重载:
void func(int *p);
void func(int *const p); // 错误
void func(int *p);
void func(const int *p); // 正确
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<const string&>(s2));
return const_cast<string&>(r);
}
调用重载函数
函数匹配(function matching)又称重载确定(overload resolution),指把函数调用和一组重载函数中的某一个关联起来的过程。调用重载函数可能有三种结果:
- 编译器找到一个最佳匹配函数,并生成调用该函数的代码
- 找不到任何一个函数可以与实参匹配,编译器发出无匹配的错误信息
- 有多个函数可以匹配,但每一个都不是明显的最佳选择,产生二义性调用
6.4.1 重载与作用域(P210)
void func(int p) {
cout << "int";
}
void func(double) {
cout << "double";
}
void func(int *p){
cout << "int*";
}
int main() {
void func(double);
int i = 0;
func(i); // 输出为double,因为内层func屏蔽了外面的func
int *p = &i;
func(p); // 错误
return 0;
}
在 C++ 中,名字查找发生在类型检查之前。在上面的代码中,执行 func(p)
首先寻找该函数的声明,找到的是 void func(double)
的局部生命,并自动忽略外层作用域中的同名实体,由于 int*
无法转换成 double
,所以这个调用是错误的。
6.5 特殊用途语言特性(P211)
6.5.1 默认实参(P211)
在调用含有默认实参的函数时,可以包含该实参,也可以省略该实参:
void func(int height = 24, int width = 80, char background = ' ');
使用默认实参调用函数
默认实参负责填补函数调用缺少的尾部实参:
func(); // 等价于func(24, 80, ' ')
func(66); // 等价于func(66, 80, ' ')
func(, , '#'); // 错误,只能省略尾部实参
func('?'); // 等价于func('?', 80, ' ')
在设计含有默认实参的函数时,应该尽可能把默认实参安排在后面。
默认实参声明
在同一个作用域对同一个函数的多次声明中,每个形参只能被赋予一次默认实参:
void func(int, int, char = ' ');
void func(int, int, char = '*'); // 错误
void func(int = 24, int, char); // 正确
默认实参初始值
局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需类型,该表达式就能作为默认实参:
int ht = 24;
char def = ' ';
int wd();
void func(int = ht, int = wd(), char = def);
用作默认实参的名字在函数声明的作用域内解析,这些名字的求值过程发生在函数调用时:
int main(){
def = '*';
int ht = 30; // 屏蔽外层ht
func(); // 等价于func(24, wd(), '*');
}
6.5.2 内联函数和constexpr
函数(P213)
使用内联函数可避免函数调用额外开销
将函数指定为内联函数(inline),就是将它在每个调用点上展开:
inline const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1 : s2;
}
inline
只是想编译器发出一个请求,编译器可以忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、调用频繁的函数。
constexpr
函数
constexpr
函数指能用于常量表达式的函数。定义 constexpr
是需要遵循几项约定:函数的返回值和所有形参必须为字面值类型,函数体中必须有且仅有一条 return
语句(一般而言,整个函数体就只有一条语句,即 return
语句):
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();
在执行 foo
的初始化任务时,编译器把对 new_sz
的函数调用替换为其结果值。constexpr
函数被隐式指定为内联函数。
constexpr
函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作,如空语句、类型别名、using
声明等。
我们允许 constexpr
函数的返回值不是常量:
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
对于上述 scale
函数,当我们传入一个形如字面值 2
的常量表达式时,它的返回类型也是常量表达式,并且编译器会用相应的结果替换对 scale
的调用;如果我们传入的不是常量表达式,则返回值也不是常量表达式。
应当把
constexpr
也当作一种请求。
把内联函数和constexpr
函数放在头文件
内联函数和 constexpr
函数允许多次定义(因为这两种函数往往在编译器就要进行一些操作,仅有函数声明是不够的),但每次定义必须完全相同,通常把它们定义在头文件中。
6.5.3 调试帮助(P215)
程序可以包含是一些用于调试的代码,这些代码只在开发程序时使用,当程序真正发布时,需要屏蔽调试代码。这种方法需要用到两项预处理功能:assert
和 NDEBUG
。
assert
预处理宏
assert
宏使用一个表达式作为它的条件:
assert(expr);
如果表达式为假,assert
输出信息并终止程序运行;如果表达式为真则什么都不做。
assert
宏定义在 cassert
头文件中,常用于检查“不能发生”的条件:
assert(word.size() > threshold);
NDEBUG
预处理变量
assert
的行为依赖于一个名为 NDEBUG
的预处理变量,如果定义了 NDEBUG
( 默认不定义),则 assert
什么也不做。
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 函数匹配(P217)
确定候选函数和可行函数
函数匹配的过程:
- 选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:与被调用函数同名、其声明在调用点可见。
- 考察本次函数调用的实参,从候选函数中选出可行函数(viable function)。可行函数具备两个特征:形参与实参数量相等(如果函数含有默认参数的话可能不同)、实参能转换成形参的类型。
- 在可行函数中寻找最佳匹配。如果有且仅有一个函数满足下列条件,则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数的匹配。
- 至少有一个实参的匹配优于其他可行函数。
如果没有找到可行函数,编译器报告无匹配函数的错误;如果没有找到最佳匹配,编译器将报告二义性调用。
6.6.1 实参类型转换(P219)
编译器将实参到形参的类型转换划分成了几个等级:
- 精确匹配:
- 实参与形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 添加或删除实参中的顶层
const
。
- 通过
const
转换实现的匹配。 - 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
需要类型提升和算术类型转换的匹配
假设两个同名函数,一个接受 int
、另一个接受 short
,即使我们在调用函数时提供一个很小的整数,由于它会被提升成 int
,所以实际调用的是接受 int
的函数。
所有算术类型转换的级别都一样,signed
转换成 unsigned
并不比 int
转换成 double
级别高。
函数匹配和const
实参
void func(int&);
void func(const int&);
const int a = 0;
int b = 0;
func(a); // 调用void func(const int&);
func(b); // 调用void func(int&);
6.7 函数指针(P221)
函数的类型由它的返回类型和形参类型共同决定,想要声明一个指向函数的指针,只需要用指针替换函数名即可:
bool lengthCompare(const string &, const string &);
bool (*pf)(const string &, const string &) = lengthCompare;
使用函数指针
当我们把函数名当作值使用时,该函数名自动转换成指针:
pf = lengthCompare;
pf = &lengthCompare; // 等价的赋值语句,取地址符是可选的
我们也可以直接使用指针调用函数,无需解引用指针:
bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye"); // 等价的调用
指向不同函数类型的指针不存在转换规则(注意,精确匹配中有些情况看似属于类型转换,但实际上不是。)。
重载函数指针
void func(int *const);
void func(unsigned int);
void (*pf1)(unsigned int) = func; // 指向void func(unsigned int);
void (*pf2)(int) = func; // 错误
void (*pf3)(int*) = func; // 指向void func(int *const);
函数指针形参
和数组类似,我们不能定义函数类型的形参,但形参可以是指向函数的指针:
void useBigger(const string&, const string&,
bool pf(const string &, const string &));
void useBigger(const string&, const string&,
bool (*pf)(const string &, const string &)); // 等价声明
我们可以直接把函数当作实参使用。
使用类型别名和 decltype
可以简化代码。
返回指向函数的指针
最好使用类型别名、尾置返回类型简化代码。
将auto
和decltype
用于函数指针类型
string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
decltype(sumLength) *getFcn(const string &);
由于将 decltype
作用于某个函数时,将得到函数类型而非指针类型,所以我们必须显式地加上 *
以表明我们需要返回指针。