0. 什么是函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded) 函数。
分析上面的定义:
- 同一作用域内,强调函数重载的范围。若两个不相干的文件、或两个不相干的类具有同名函数,那么不构成重载。
- 名字相同但形参列表不同,这是重载的充分条件。若同一作用域内两个函数的返回值不同,不构成重载。
发明函数重载,只是为了减轻我们在定义函数时起名字的负担。例如:
void print(const char *p);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);
上面的函数构成了函数重载,不论何时我们想输出一些内容,只要调用print
就行了。
注意:main函数不能重载。
关于重载,我们需要了解:
- 声明&定义重载函数
- 调用重载函数
- 成员函数的重载
- 运算符重载
1. 声明&定义重载函数
声明&定义重载函数要从两方面入手:
- 逻辑上是否可以构成重载
- C++语言层面上如何定义重载
1.1 逻辑层面
尽管函数重载可以减轻我们为函数起名字的负担,但是最好只重载那些确实非常相似的函数。例如:
void move_home(); // 移动光标到本行开头
void move_abs(int, int);
void move_rel(int, int, const string &dir);
似乎可以把他们统一为:
void move();
void move(int, int);
void move(int, int, const string &dir);
但是使用起来,丢失了名字中本来的信息。使用的时候,也明显感觉非重载的版本让人读起来更顺畅:
myScreen.moveHome();
myScreen.move();
1.2 定义重载
经过逻辑层面的分析,若可以使用重载函数减轻命名负担,则可以按照以下步骤定义重载:
- 确定函数的名字
- 确定每个函数的参数,确保不一致
确定函数的名字很简单,确保函数的参数不一致有些困难。
1.2.1 参数数量不一致
1.2.1.1 含有默认实参
某些函数有这样一种形参,在函数的很多次调用中都被赋予一个相同的值,此时我们把这个反复出现的值成为函数的默认实参(default argument) 。含有默认实参的函数,可以包含该实参,也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
调用时,省略想要使用的默认实参即可。
screen(30); // 等价于screen(30, 80, ' ');
若定义了类似于如下的重载函数:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
string screen(sz ht = 24, sz wid = 80);
虽然参数列表不一样,编译重载函数也不会出错,但是在调用时不恰当,如只传递两个参数screen(30, 90)
,会出现二义性错误。
1.2.1.2 含有可变形参
使用initializer_list
定义的可变形参,可以定义一组数量未知、但是全部实参类型相同的可变形参。
void error_msg(initializer_list<string>);
initializer_list
实际上是标准库的一个类型,因此定义含有该类参数的重载函数,和其他函数没什么区别。
还有一种定义可变形参的方法,可变参数模板,请参考【C++深陷】之“可变参数”。
1.2.1.2 含有省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。
省略符形参只能出现在形参列表的最后一个位置:
// 两种形式
void func(params, ...);
void func(...);
含有省略符形参的函数,若省略符前面的参数和其他函数参数一样,虽然编译重载函数不会出错,但是在调用的时若没有给省略符形参传递参数,会出现二义性错误。
1.2.2 参数类型不一致
以下讨论的前提是参数数量一致。
1.2.2.1 省略形参名
声明函数时可以省略形参名,因此以下两种声明是等价的,不构成重载:
void print(int);
void print(int a); // 和上面声明等价
1.2.2.2 顶层const与底层const
顶层const对参数区分无影响,例如下面的声明,都是重复声明:
void print(int);
void print(const int); // 和上面的声明是等价的
void print(int *);
void print(int * const); // 这是顶层const
但是指针和引用,使用底层const是可以区分的:
void print(int &);
void print(const int &);
void print(int *);
void print(const int *);
1.2.2.3 类型别名
我们可以使用typedef
和using
声明一个类型的别名,若使用别名定义了一系列重载函数,字面上看不出区别,但实际上是一种函数:
typedef Phone Telno;
Record lookup(const Phone &);
Record lookup(const Telno &); // 和上面的定义等价
1.2.2.4 数组与指针
C++中的数组通常会被转换成指向首元素的指针,因此如下示例的3个函数,是等价的:
void print(const int *);
void print(const int []);
void print(const int [10]);
1.2.3 返回值不是重载条件
如定义,我们再次强调,两个函数名相同、参数列表相同,但是返回值不同的函数,无法构成重载。
2. 调用重载函数
调用一组重载函数,是函数匹配做的事情。
函数匹配,也称作重载确定,它是一个过程,主要任务是找到一个与实参最佳匹配的函数。
若重载的函数参数差别很大,可以很容易区分,但是遇到如下面这种情况:两个重载的函数参数数量相同,且相对位置的参数可以互相类型转换,这就麻烦了。
函数匹配的结果,无非是确定了一个最优调用方案、无匹配和二义性错误。
关于函数匹配的内容,请参考【深度C++】之“函数匹配”。
3. 成员函数的重载
成员函数重载条件与普通的函数一样,只需要注意构成重载的函数只在一个类内。因为成员函数有一个隐式的this指针作为第一个参数。
4. 运算符重载
运算符重载可以给类类型重新定义该运算符的含义。明智地使用运算符重载能令我们的程序更易于编写和阅读。
与定义函数重载一样,第一步要从逻辑层面保证运算符的本身的逻辑与内置类型差不多。
第二步,根据每个运算符的规则,定义属于每个类类型自己的运算符重载函数,详细参考【C++深陷】之“运算符重载”。
5. 总结
函数重载(overload) 是同一作用域内的几个函数名字相同但形参列表不同的函数。
只有返回值不一样,不能构成重载。
函数重载只是为了减轻我们在定义函数时起名字的负担,帮助我们的程序更易于编写和阅读。
逻辑层面保证需要重载技术,然后再根据C++语言的规则定义重载函数。要注意确保函数名一致,参数数量、参数类型不一致。常见的问题如默认实参、const、数组等情况。
调用重载的函数,要遵循C++的函数匹配规则,确定一个最优调用方案,或者是出现无匹配和二义性错误。