为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,所以C++可以允许同名函数的存在,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成相似或又不同的功能。此时就出现了函数重载。
例:
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
long Add(long a, long b)
{
return a + b;
}
int main()
{
Add(10 , 20 );
Add(10.0, 20.0);
Add(10L , 20L );
return 0;
}
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当调用一个重载函数时,编译器通过把你所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策或重载确定。
一、函数重载基础
函数重载有时候也叫静态多态或者编译时多态,这里先给出函数重载的特点:
- 函数名相同
- 函数参数个数不同
- 如果参数个数相同,那么参数类型不能相同
- 如果参数个数相同,并且类型也相同,那么必须是顺序不同
- 函数重载与函数返回值无关
总结就是:函数的参数个数不同 或者 函数的参数类型不同 或者 参数类型顺序不同,且与函数返回值无关。
二、函数重载的调用机制
关于调用,那么问题来了,C++是如何进行准确的调用的呢?调用的是多个函数中的哪一个呢?
进一个例子:
1 #include <iostream>
2
3 using namespace std;
4
5 int add(int a, int b)
6 {
7 return a + b;
8 }
9
10 int add(int a, int b, int c = 1)
11 {
12 return a + b + c;
13 }
14
15 double add(double a, double b)
16 {
17 return a + b;
18 }
19
20 int max(int a, int b)
21 {
22 return a > b ? a : b;
23 }
24
25 int main()
26 {
27 add(1, 2);
28 return 0;
29 }
编译器判断重载函数的第一步是确定该调用中所考虑的重载函数的集合,该函数集合被称为候选函数(candidant function)。所谓候选函数就是与被调用函数同名的函数。上面的前三个函数都可以成为候选函数(当然可以是多个),而唯有max ( int , int ) 被排除在外了。
编译器判断重载函数的第二步分为两动作。第一个动作是编译器从第一步选出的候选函数中调出可行函数(viable function)。可行函数的函数参数个数与调用的函数参数个数相同(如add ( int, int )),或者可行函数的参数可以多一些,但是多出来的函数参数都要有相关的缺省值(如 add (int, int , int = 1 );) 第二个动作是根据参数类型的转换规则将被调用的函数实参转换(conversion)成候选函数的实参。这里本着充分利用参数类型转换的原则,换句话说,尽可能的使用上参数类型转换。当然转换要以候选函数为转换的目标。上面的函数中三个都是可行函数,它们分别是add ( int,int ); add ( int,int , int = 1 ); add ( double , double );。
如果依照参数转换规则没有找到可行函数,则该调用就是错误的,则说没有函数与调用匹配,属于无匹配情况(no match function)。
编译器判断重载函数的第三步是从第二步中选出的可行函数中选出最佳可行函数(best match situation)。在最佳可行函数的选择中,从函数实参类型到相应可行函数参数所用的转化都要划分等级,根据等级的划分(ranked),最后选出最佳可行函数。
补充:重载确定是一个非常复杂的话题,引用《C++ Primer》中对于重载确定的隐式类型转换等级的说明:
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加顶层const或从实参中删除顶层const
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
三、函数重载调用的更深层机制
这也是一个比较深层的问题:具体要考虑的情况有很多,涉及函数调用的隐式类型转换等等。
这里先简单介绍C++编译过程中的函数调用的名字修饰部分,这也就解释了一个问题:
为什么C++支持函数重载,而C语言不支持函数重载呢?
我们知道在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
源文件需要经过编译器依次生成各级文件,最后经过链接器,最终生成可执行程序。
首先要了解,实际上,同名函数就会发生命名冲突,解决命名冲突得方法除了利用上篇的命名空间,就是进行编译时的函数编译机制了。
编译机制在各个不同的编译器下会有所不同,由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字
可以看到,在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
至于命名的映射规则,在查找资料后得到答案是这样的:_Z后面的数字代码各种返回类型、函数名之后加的字母是参数列表的第一个代表字母,不同编译器下规则可能不同,但大体规则应该一致:“返回类型+函数名+参数列表”。
既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析函数调用时,独立于上下文(不依赖于上下文),这里就不举例了。
extern “C”
那就剩最后一个问题了,我们都知道C++是兼容C语言的,有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
extern "C" int add(int left, long right);
int main()
{
add(1,2);
return 0;
}
但此时就会报错,所以
链接时报错:error LNK2019: 无法解析的外部符号_Add,该符号在函数 _main 中被引用