自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载概念
是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
调用原理:
编译器在编译代码期间,需要对函数的实参类型进行推导,根据推导的结果选择对应合适类型的函数进行调用。
注意:
- 若该函数存在,则直接调用
- 如果不存在类型完全匹配的函数,则编译器会尝试进行隐式类型转换,转换完成后,如果有对应类型的函数,则进行调用,否则编译失败(没有对应类型;调用有二义性)。
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main() {
Add(10, 20);
Add(10.0, 20.0);
Add(10L, 20L);
return 0;
}
下面两个函数属于函数重载吗?
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
不属于函数重载!!!
C++ 允许两个重名函数同时存在,但他们的参数列表不能完全相同,哪怕返回值类型不同也不可以。
void TestFunc(){}
void TestFunc(int a = 100){}
属于函数重载!!!
不过,这样的函数重载存在问题
int main(){
TestFunc(100); // 调用 void TestFunc(int a = 100){}
TestFunc(); // 此时两个函数会有调用歧义
return 0;
}
// call to 'TestFunc' is ambiguous
名字修饰(name Mangling)
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
Name Mangling是一种 在编译 过程中,将 函数、变量的名称重新改编 的机制。简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。比如,对于以下代码,在最后链接时就会出错:
int Add(int left, int right);
int main() {
Add(1, 2);
return 0;
}
在vs2019下,对上述代码进行编译链接,最后编译器报错:
上述Add函数只给了声明没有给定义,因此在链接时就会报错,提示:在main函数中引用的Add函数找不到函数体。从报错结果中可以看到,C语言只是简单的在函数名前添加下划线。因此当工程中存在相同函数名的函数时,就会产生冲突。
由于C++要支持函数重载,命名空间等,使得其修饰规则比较复杂,不同编译器在底层的实现方式可能都有差异。
int Add(int left, int right);
double Add(double left, double right);
int main() {
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
在vs2019下,对上述代码进行编译链接,最后编译器报错:
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字, 被 重新修饰后的名字 中 包含 了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要 参数列表不同 ,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的名字中,就可保证 名字在底层的全局唯一性。
在C++中:
总结:
- 在C语言中,名字修饰只是在函数名前加下划线,所以只要函数名相同,就会导致冲突。
- 在C++中,名字修饰是 ?函数名@域名1@域名2。。。@@参数列表@z 的格式构成的,包含:
A、函数名
B、所在区域
C、参数列表
所以在C++中,以上三个必须完全相同,才会出现冲突这就是函数重载的原理。
C++中函数重载底层是怎么处理的?
编译器调用重载函数的准则:
- 将所有同名函数作为候选者
- 尝试寻找可行的候选函数
- 精确匹配实参
- 通过默认参数能够匹配实参
- 通过默认类型转换匹配实参
- 匹配失败
- 最终寻找到的可行候选函数不唯一,则出现二义性,编译失败
- 无法匹配所有候选者,函数未定义,编译失败。
重载函数的调用匹配
在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
- 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
- 使用标准转换匹配:如int 到double、double到int、double到long double、Derived到Base、T到void、int到unsigned int;
- 使用用户自定义匹配;
- 使用省略号匹配:类似printf中省略号参数
void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);
void h(char c,int i,short s, float f)
{
print(c);//精确匹配,调用print(char)
print(i);//精确匹配,调用print(int)
print(s);//整数提升,调用print(int)
print(f);//float到double的提升,调用print(double)
print('a');//精确匹配,调用print(char)
print(49);//精确匹配,调用print(int)
print(0);//精确匹配,调用print(int)
print("a");//精确匹配,调用print(const char*)
}
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:
- 定义太少或太多的重载函数,都有可能导致模凌两可
void f1(char);
void f1(long);
void f2(char*);
void f2(int*);
void k(int i)
{
f1(i);//调用f1(char)? f1(long)?
f2(0);//调用f2(char*)?f2(int*)?
}
这时侯编译器就会报错,将错误抛给用户自己来处理:通过显示类型转换来调用等等
(如f2(static_cast<int *>(0)
,当然这样做很丑,而且你想调用别的方法时有用做转换)。上面的例子只是一个参数的情况,下面我们再来看一个两个参数的情况:
int pow(int ,int);
double pow(double,double);
void g()
{
double d=pow(2.0,2)//调用pow(int(2.0),2)? pow(2.0,double(2))?
}
编译器是如何解析重载函数调用的?
- 由匹配文法中的函数调用,获取函数名;
- 获得函数各参数表达式类型;
- 语法分析器查找重载函数,符号表内部经过 重载解析 返回最佳的函数
- 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
重载函数解析大致可以分为三步:
- 根据函数名确定候选函数集
- 从候选函数集中选择可用函数集合
- 从可用函数集中确定最佳函数,或由于模凌两可返回错误
extern “C”
使用extern “C”修饰一个语句或者将一段代码包起来那么这段代码会 以C语言的风格进行编译。
extern “C”是C++编译器提供的与C连接交换指定的符号,用来解决名字匹配的问题。
extern "C" int Add(int left, int right); // 这样就可以进行简单修饰
#ifdef __cplusplus // 检测工程是否为C++工程(正规化)
extern "C"
{
#endif
int Add(int left, int right);
int main() {
Add(1, 2);
return 0;
}
#ifdef __cplusplus
}
#endif
在vs2019下,对上述代码进行编译链接,最后编译器报错:
习题
- 哪个操作符不能被重载?
A. , (逗号)
B. ()
C. . (点)
D. []
E. ->
正确答案:
C
答案解析:
c++不能重载的运算符有.(点号),::(域解析符),?:(条件语句运算符),sizeof(求字节运算符),typeid,static_cast,dynamic_cast,interpret_cast(三类类型转换符)。
正确答案: C
答案解析:
重载运算符不能改变原有运算符的优先级, 结合性和操作数个数.
- 有如下类模板定义:()
template<class T> class BigNumber{
long n;
public:
BigNumber(T i):n(i){}
BigNumber operator+(BigNumber b)
{
return BigNumber(n+b.n);
}
};
已知b1,b2是BigNumber的两个对象,则下列表达式中错误的是?
A. 3+3
B. b1+3
C. b1+b2
D. 3+b1
正确答案: D
答案解析:
A.3+3 就是正常的3+3
B.b1+3 是BigNumber(b1)+BigNumber(3)
C.b1+b2 是BigNumber(b1)+BigNumber(b2)
D.3+b1 常数3无法匹配到此模版中的+重载函数,编译器无法识别
如有不同见解,欢迎留言讨论!