Part I: The Basics
Chapter 6. Functions
6.4 函数重载
重载(overloaded):在同一作用域内,函数有相同的名字,但有不同的形参列表。
main 函数不能重载。
定义重载函数
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
判断两个形参的类型是否相同
有时两个形参列表看起来不同,但实际上是一样的:
// 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
重载和 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*)
如果形参是某种类型的指针或引用,则区分其指向的是 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 对象(或指向 const 的指针)传递给 const 形参。
因为 非const 能转换为 const,所以上面的 4 个函数都能作用于非const 对象或指向 非const 的指针。
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));
return const_cast<string&>(r);
}
调用重载函数
函数匹配(function matching):是将某个函数调用与一组重载函数中的特定函数相关联的过程,也称为重载解析(overloaded resolution)。
编译器通过比较调用函数的实参与重载集合中每个函数的形参,来确定要调用的函数。
调用重载函数时,有 3 种结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
- 没有函数具有与调用的实参匹配的形参,在这种情况下,编译器会发出一条无匹配(no match)的错误信息。
- 有多个函数可以匹配,且没有一个匹配是明显最佳的。这种情况是错误的,是二义性调用(ambiguous call)。
重载与作用域
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("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
}
在 C++语言中,名字查找发生在类型检查之前。
void print(const string &);
void print(double); // overloads the print function
void print(int); // another overloaded instance
void fooBar2(int ival) {
print("Value: "); // calls print(const string &)
print(ival); // calls print(int)
print(3.14); // calls print(double)
}
6.5 特殊用途语言特性
默认实参
调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
typedef string::size_type sz; // typedef see § 2.5.1 (p. 67)
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
默认实参被指定为形参列表中形参的初始值。
可以为一个或多个形参定义默认值。但是,如果一个形参具有默认实参,则其后的所有形参也必须具有默认实参。
调用含有默认实参的函数
string window;
window = screen(); // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ')
window = screen(66, 256); // screen(66,256,' ')
window = screen(66, 256, '#'); // screen(66,256,'#')
调用的实参按位置解析。 默认参数用于调用的尾部的(最右边)实参。
例如,要覆盖 backgrnd
的默认值,必须提供 ht
和 wid
的实参:
window = screen(, , '?'); // error: can omit only trailing arguments
window = screen('?'); // calls screen('?',80,' '); '?' has the hexadecimal value 0x3F, which is decimal 63
设计具有默认实参的函数时,其中一部分任务是设计形参的顺序,以使那些不太可能使用默认值的形参出现在前面,而那些最可能使用默认值的形参出现在后面。
默认实参声明
在给定的作用域中,一个形参只能被赋予一次默认实参。因此,任何后续声明都只能为先前未指定默认值的形参添加默认值。当然,只有在右侧的所有形参都已经具有默认值的情况下,才可以指定默认值。
string screen(sz, sz, char = ' '); // no default for the height or width parameters
string screen(sz, sz, char = '*'); // error: redeclaration
string screen(sz = 24, sz = 80, char); // ok: adds default arguments
通常,应该在合适的头文件中的函数声明中,指定默认实参。
默认实参初始值
局部变量不能作为默认实参。
// the declarations of wd, def, and ht must appear outside a function
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // calls screen(ht(), 80, ' ')
在函数声明的范围内解析用作默认参数的名称。这些名称代表的值在调用时进行计算:
void f2() {
def = '*'; // changes the value of a default argument
sz wd = 100; // hides the outer definition of wd but does not change the default
window = screen(); // calls screen(ht(), 80, '*')
}
内联函数和 constexpr 函数
inline 函数避免函数调用的开销
在每次调用时,通常将指定为 inline
的函数“内联”展开。
// inline version: find the shorter of two strings
inline const string & shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
将 shorterString
定义为 inline
,调用
cout << shorterString(s1, s2) << endl;
(很可能)在编译过程中展开成为类似下面的这种形式:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
从而消除了 shorterString
函数的运行时开销。
注意:inline
规范只是向编译器发出的一个请求。编译器可以选择忽略这个请求。
通常,inline
机制旨在优化规模较小、流程直接、调用频繁的函数。许多编译器不支持内联递归函数。一个 75 行的函数几乎肯定不会内联扩展。
constexpr 函数
constexpr
函数是可用于常量表达式的函数。
constexpr
函数的定义与其他函数类似,但必须满足某些限制:return
类型和每个形参的类型必须是字面值类型,且函数主体必须有且只有一条 return
语句。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // ok: foo is a constant expression
编译器可以在编译时确认 new_sz()
调用返回一个常量表达式,所以可以使用 new_sz()
初始化 constexpr
变量 foo
。当可以初始化时,编译器将对 constexpr
函数的调用替换成其结果值。为了能够立即展开该函数,constexpr 函数是隐式内联的。
constexpr
函数主体可以包含其他语句,只要这些语句在运行时不执行任何动作。例如,空语句,类型别名,using
声明。
允许 constexpr 函数的返回值不是一个常量:
// scale(arg) is a constant expression if arg is a constant expression
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
如果 scale
函数的实参是一个常量表达式,函数返回一个常量表达式,反之则不然:
int arr[scale(2)]; // ok: scale(2) is a constant expression
int i = 2; // i is not a constant expression
int a2[scale(i)]; // error: scale(i) is not a constant expression
把 inline 函数和 constexpr 函数放在头文件中
与其他函数不同,inline
函数和 constexpr
函数可以在程序中定义多次。毕竟,为了展开代码,编译器需要定义,而不仅仅是声明。但是,给定的 inline
函数或 constexpr
函数的所有定义必须完全匹配。因此,inline 函数和 constexpr 函数通常定义在头文件中。
调试帮助
思想:程序将包含仅在程序开发时才执行的调试代码。当应用程序完成并准备好交付时,调试代码将关闭。这种方法使用两种预处理器工具:assert
和 NDEBUG
。
assert 预处理宏
assert
是一个预处理器宏 (preprocessor macro)。预处理器宏是一个预处理器变量,它的行为有点类似于内联函数。assert
宏使用一个表达式作为条件:
assert(expr);
对 expr 求值,如果表达式为 false,那么 assert
输出信息并终止程序。如果表达式为 true,那么 assert
什么也不做。
assert
宏定义在 cassert
头文件中。预处理名字由预处理器管理,而不是编译器管理。
和预处理变量一样,宏的名字在程序中必须唯一。含有 cassert
头文件中的程序不能再定义名为 assert
的变量、函数或其他实体。即使没有包含 cassert
头文件,最好也不要使用 assert
名字。
assert
宏通常用于检查“不能发生”的条件。它可作为调试程序的辅助手段,但不能代替程序的运行时逻辑检查或程序应包含的错误检查。
例,程序要求 word
的长度总是大于 threshold
。
assert(word.size() > threshold);
NDEBUG 预处理变量
assert
的行为取决于 NDEBUG
预处理变量的状态。若定义了 NDEBUG
,则 assert
什么也不做。
默认情况下没有定义 NDEBUG
,所以,默认 assert
执行运行时检查。
很多编译器提供了一个命令行选项来定义预处理变量:
$ CC -D NDEBUG main.C # use /D with the Microsoft compiler
上条命令等价于在 main.C 文件的开头写
#define NDEBUG
表示关闭调试状态。
还可以使用 NDEBUG
编写自己的条件调试代码:
void print(const int ia[], size_t size) {
#ifndef NDEBUG
// _ _func_ _ is a local static defined by the compiler that holds the function's name
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
// ...
预处理器还定义了 5 个对于调试很有用的名字:
__func__
:存放当前函数名的字符串字面值__FILE__
:存放文件名的字符串字面值__LINE__
:存放当前行号的整型字面值__TIME__
:存放文件编译时间的字符串字面值__DATE__
:存放文件编译日期的字符串字面值
if (word.size() < threshold)
cerr << "Error: " << _ _FILE_ _
<< " : in function " << _ _func_ _
<< " at line " << _ _LINE_ _ << endl
<< " Compiled on " << _ _DATE_ _
<< " at " << _ _TIME_ _ << endl
<< " Word read was \"" << word
<< "\": Length too short" << endl;
6.6 函数匹配
以下面这组函数及其调用为例:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // calls void f(double, double)
确定候选函数和可行函数
候选函数 (candidate function):与调用函数同名的函数,且其声明在调用点可见。在这个例子中,有 4 个名为 f
的候选函数。
可行函数 (viable function):从候选函数中选出的函数,其形参的数目与调用的实参数目相等,每个实参的类型必须匹配或者可以转换为对应形参的类型。在这个例子中,f(int)
和 f(double, double=3.14)
是可行的。
如果没找到可行函数,编译器将报告无匹配函数。
如果有,找到最佳匹配
基本思想:实参类型与形参类型越接近,匹配越好。
精确匹配比需要类型转换的匹配更好。因此,编译器将调用 f(5.6)
解析为函数 f(double, double=3.14)
的调用。
含有多个形参的函数匹配
分析调用:f(42, 2.56);
此例中,可行函数是 f(int, int)
和 f(double, double)
。
如果有且仅有一个函数满足以下条件,则匹配成功:
①每个实参的匹配都不劣于其他可行函数要求的匹配;
②至少有一个实参的匹配优于其他可行函数提供的匹配。
如果检查了每个实参,没有哪个函数最优,那么调用是错误的。编译器报告调用是二义性的。
编译器拒绝调用 f(42, 2.56);
,因为它是二义性的:每个可行函数在调用的一个实参上匹配比其他的匹配更优。
在设计良好的系统中,不需要对实参进行强制类型转换。
调用重载函数应该不需要强制类型转换。需要强制转换表明形参集合设计不佳。
实参类型转换
为了确定最佳匹配,编译器对每个实参类型到其对应形参类型的转换进行排名:
- 精确匹配,包括:
实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型。
向实参添加顶层 const 或从实参中删除顶层 const。 - 通过 const 转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
需要类型提升或算术类型转换的匹配
小整型总是提升到 int 类型或更大的整数类型。
void ff(int);
void ff(short);
ff('a'); // char promotes to int; calls f(int)
所有算术类型转换的级别都一样。
void manip(long);
void manip(float);
manip(3.14); // 3.14 is a double; error: ambiguous call
函数匹配和 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(a); // calls lookup(const Account&)
lookup(b); // calls lookup(Account&)
在第一个调用中,可行函数只有 lookup(const Account&)
。
在第二个调用中,两个函数都是可行的。然而,非常量对象初始化对常量的引用需要转换,具有非常量形参的函数与 b
精准匹配。
指针形参与之类似。
6.7 函数指针
函数指针指向的是函数而不是对象。
函数的类型由它的返回类型和形参类型决定,与函数名无关。
// compares lengths of two strings
bool lengthCompare(const string &, const string &);
该函数的类型是 bool(const string&, const string&)
。声明指向函数的指针,用指针替换函数名:
// pf points to a function returning bool that takes two const string references
bool (*pf)(const string &, const string &); // uninitialized
使用函数指针
当使用函数名作为一个值时,函数自动转换为指针。
pf = lengthCompare; // pf now points to the function named lengthCompare
pf = &lengthCompare; // equivalent assignment: address-of operator is optional
还可以直接使用指向函数的指针调用该函数,无需解引用指针:
bool b1 = pf("hello", "goodbye"); // calls lengthCompare
bool b2 = (*pf)("hello", "goodbye"); // equivalent call
bool b3 = lengthCompare("hello", "goodbye"); // equivalent call
在指向不同函数类型的指针之间不存在转换。可以给函数指针赋值 nullptr
或值为 0 的整数常量表达式,表示该指针不指向任何函数。
string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0; // ok: pf points to no function
pf = sumLength; // error: return type differs
pf = cstringCompare; // error: parameter types differ
pf = lengthCompare; // ok: function and pointer types match exactly
指向重载函数的指针
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1 points to ff(unsigned)
编译器使用指针类型确定使用的是哪个重载函数。指针类型必须与重载函数中的某一个精确匹配:
void (*pf2)(int) = ff; // error: no ff with a matching parameter list
double (*pf3)(int*) = ff; // error: return type of ff and pf3 don't match
函数指针形参
不能定义函数类型形参,但形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上是当成指针使用:
// third parameter is a function type and is automatically treated as a pointer to function
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// equivalent declaration: explicitly define the parameter as a pointer to function
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
当传入函数作为实参时,它将自动转换为指针:
// automatically converts the function lengthCompare to a pointer to function
useBigger(s1, s2, lengthCompare);
类型别名和 decltype 可以简化使用函数指针的代码
// Func and Func2 have function type
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // equivalent type
// FuncP and FuncP2 have pointer to function type
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; // equivalent type
注意,decltype
返回函数类型,不会自动转换成指针。要想返回指针,必须加上 *
。
// equivalent declarations of useBigger using type aliases
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);
返回指向函数的指针
不能返回函数类型,但可以返回指向函数类型的指针。声明一个返回指向函数的指针的函数,最简单的方式时使用类型别名:
using F = int(int*, int); // F is a function type, not a pointer
using PF = int(*)(int*, int); // PF is a pointer type
注意:与函数类型的形参不同,返回类型不会自动转换为指针。必须显式地将返回类型指定为指针类型:
PF f1(int); // ok: PF is a pointer to function; f1 returns a pointer to function
F f1(int); // error: F is a function type; f1 can't return a function
F *f1(int); // ok: explicitly specify that the return type is a pointer to function
直接声明 f1
:
int (*f1(int))(int*, int);
使用尾置返回的方式简化返回指向函数的指针的函数的声明:
auto f1(int) -> int (*)(int*, int);
将 decltype 和 auto 用于函数指针类型
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
// depending on the value of its string parameter,
// getFcn returns a pointer to sumLength or to largerLength
decltype(sumLength) *getFcn(const string &);