一词多义是我们都熟悉、司空见惯的东西。一个词语在不同的语境中具有不同的意义。正因如此才促成了我们语言丰富性。本实用经验所介绍的函数重载就同一词多义非常类似,函数重载就是自然语言一词多义在编程语言中的映射。
1.What
函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。
说明:函数重载和重复声明的区别
- 两个函数声明的返回类型和形参表完全匹配,第二个函数判定为第一函数的重复声明。
- 两个函数的声明形式参数表完全相同,但是返回类型不同。第二个函数声明判定为错误。
首先来看一个例子,体会一下函数重载的给我们带来的编程舒适感:实现一个打印函数,既可打印int类型,也可打印string类型。
#include<iostream>
using namespace std;
void Print(int nValue) // 在屏幕上打印一个整数
{
cout << "print a integer :" << nValue << endl;
}
void Print(string strValue) // 在屏幕上打印一个字符串
{
cout << "print a string :" << strValue << endl;
}
int main()
{
cout << "print a integer and a string." << endl;
Print(12);
Print("hello girl!");
return 0;
}
上面的代码实现中,Print函数会根据Print()的参数决定是调用Print(int)还是Print (string)函数。Print(12)会调用Print(int)函数。Print(“hello girl”)会调用Print(string)函数。
现在我们对函数的重载有了一个大致的了解了。下面我们看看C++为什么引入函数重载,函数在调用过程中怎么确定如何调用的呢?
2.Why
C++引入函数重载主要有这几个方面的考虑:
(1)编程和阅读代码友好性的角度,试想如果没有重载机制:同一功能的函数你必须取不同的函数名称。如上例,你必须取Print_int,Print_string这样的名称以区别两个函数。
(2)类的构造函数跟类名相同,就是说构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦。
(3)操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等。
注意:警惕函数重载的滥用。并不是所有看似可以用函数重载的地方,选择函数重载具是较佳选择。如下某些情况下使用不同的函数名称可以提供较多的信息,是程序易于理解。此处建议不要滥用函数重载。
3.How
编译器处理函数重载,主要解决函数命名冲突、函数调用匹配两个关键问题。函数命名冲突顾名思义就是重载函数具有相同的函数名称,编译器是如何区分它们的。函数调用匹配即编译器在连接过程中如何判定调用哪个重载函数。我们首先看一下命名冲突,然后再看函数调用匹配。
(1)函数命名冲突。为了更好的讲述函数命名冲突问题,我们首先看一下上述举例代码的汇编代码(Windows 下MingGW编译器):
void Print(int nValue)对应汇编代码:请注意它的函数标签名称为__Z5Printi:
00000012 <__Z5Printi>:
void Print(int nValue) // 在屏幕上打印一个整数
{
12: 55 push %ebp
13: 89 e5 mov %esp,%ebp
15: 83 ec 08 sub $0x8,%esp
cout << "print a integer :" << nValue << endl;
18: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
1f: 00
20: c7 04 24 00 00 00 00 movl $0x0,(%esp)
27: e8 00 00 00 00 call 2c <__Z5Printi+0x1a>
2c: 8b 55 08 mov 0x8(%ebp),%edx
2f: 89 54 24 04 mov %edx,0x4(%esp)
33: 89 04 24 mov %eax,(%esp)
36: e8 00 00 00 00 call 3b <__Z5Printi+0x29>
3b: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
42: 00
43: 89 04 24 mov %eax,(%esp)
46: e8 00 00 00 00 call 4b <__Z5Printi+0x39>
}
4b: c9 leave
4c: c3 ret
void Print(string strValue)函数对应汇编代码:请注意它的函数标签名为__Z5PrintSs。
0000005e <__Z5PrintSs>:
void Print(string strValue) // 在屏幕上打印一个字符串
{
5e: 55 push %ebp
5f: 89 e5 mov %esp,%ebp
61: 53 push %ebx
62: 83 ec 14 sub $0x14,%esp
65: 8b 5d 08 mov 0x8(%ebp),%ebx
cout << "pint a string :" << strValue << endl;
68: c7 44 24 04 4d 00 00 movl $0x4d,0x4(%esp)
6f: 00
70: c7 04 24 00 00 00 00 movl $0x0,(%esp)
77: e8 00 00 00 00 call 7c <__Z5PrintSs+0x1e>
7c: 89 5c 24 04 mov %ebx,0x4(%esp)
80: 89 04 24 mov %eax,(%esp)
83: e8 00 00 00 00 call 88 <__Z5PrintSs+0x2a>
88: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8f: 00
90: 89 04 24 mov %eax,(%esp)
93: e8 00 00 00 00 call 98 <__Z5PrintSs+0x3a>
}
98: 83 c4 14 add $0x14,%esp
9b: 5b pop %ebx
9c: 5d pop %ebp
9d: c3 ret
可以看出Print函数编译之后名称就不是Print了。这样命名冲突问题就解决了,呵呵。但是函数编译时名称会发生变化。变化机制又是怎样的呢?从上面的两个函数的变化规律我想已经猜出一个八九不离十了吧:前面的__Z5可能是返回类型,Print为函数名称,i表示整型int。Ss表示字符串string。事实也是如此。于是我们可以得到下面的结论:全局重载函数名称变化机制映射关系为:返回类型+函数名+参数列表。
我说了这么多,我们重新看一下函数重载的定义。好像到现在我们还从未提及作用域。下面我们看一下C++中更具一般性的函数重载过程。首先将上述代码改成下述形式。
#include<iostream>
#include<string>
using namespace std;
class CPrint
{
public:
void Print(int nValue) // 在屏幕上打印一个整数
{
cout << "print a integer :" << nValue << endl;
}
void Print(string strValue) // 在屏幕上打印一个字符串
{
cout << "pint a string :" << strValue << endl;
}
};
int main(int argc, char* argv[])
{
CPrint printInstance;
cout << "print a integer and a string" << endl;
printInstance.Print(12);
printInstance.Print("hello girl!");
return 0;
}
你可以在MingGW编译器下查看一下上述代码对应的汇编代码。CPrint::Print(int nValue) 函数汇编后对应__ZN6CPrint5PrintEi。CPrint::Print(string strValue) 函数汇编后对应__ZN6CPrint5PrintESs。根据上面的映射猜想,我们可总结出更为准确的映射机制为:作用域+返回类型+函数名+参数列表。
(2)函数调用匹配。现在接着讲述函数调用匹配问题。为了实现最佳的重载函数调用匹配。编译器按照实参类型和相应形参类型的转化等级,将函数调用匹配分为4个等级,他们是: ① 精确匹配,实参和形参类型完全相同。 ② 类型提升实现函数匹配。③ 通过标准转换实现匹配。④ 通过类类型转换实现匹配。
有了函数调用匹配规则后,编译器会通过①根据函数名称选择候选函数集,②从候选函数中选择可用函数,③从可用函数集中确定最佳函数。3个步骤确定重载函数的调用匹配。
注意:
- 内置类型的提升和转换可能会使函数匹配产生意想不到的结果。这个是设计重载函数需注意的东西。
- 在调用重载函数时尽量避免强制类型转换。
- 对应传值调用的函数,形参const与否对重载函数匹配无任何影响,只有在形参为传址或传引用调用时两者才会存在差异。
- 重载函数定义时尽量避免枚举常量。因为枚举类型在提升时依赖于机器,造成你的程序可移植性下降。
请谨记
- 合理利用重载函数会给程序可读性带来好处,但不合理的重载函数不但对代码可读性没有帮助,还会污染名字空间。
- 设计重载函数时尽量避免存在多个函数满足同一个实参匹配,这样会导致重载匹配的二义性问题。