C++:函数重载的使用细则


自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”

函数重载概念

是函数的一种特殊情况,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++中函数重载底层是怎么处理的?

编译器调用重载函数的准则:

  1. 将所有同名函数作为候选者
  2. 尝试寻找可行的候选函数
  3. 精确匹配实参
  4. 通过默认参数能够匹配实参
  5. 通过默认类型转换匹配实参
  6. 匹配失败
  7. 最终寻找到的可行候选函数不唯一,则出现二义性,编译失败
  8. 无法匹配所有候选者,函数未定义,编译失败。

重载函数的调用匹配

在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、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))?
}

编译器是如何解析重载函数调用的?

在这里插入图片描述

  1. 由匹配文法中的函数调用,获取函数名;
  2. 获得函数各参数表达式类型;
  3. 语法分析器查找重载函数,符号表内部经过 重载解析 返回最佳的函数
  4. 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上

重载函数解析大致可以分为三步:

  • 根据函数名确定候选函数集
  • 从候选函数集中选择可用函数集合
  • 从可用函数集中确定最佳函数,或由于模凌两可返回错误

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下,对上述代码进行编译链接,最后编译器报错:
在这里插入图片描述

习题

  1. 哪个操作符不能被重载?

A. , (逗号)
B. ()
C. . (点)
D. []
E. ->

正确答案:

C

答案解析:

c++不能重载的运算符有.(点号),::(域解析符),?:(条件语句运算符),sizeof(求字节运算符),typeid,static_cast,dynamic_cast,interpret_cast(三类类型转换符)。

在这里插入图片描述

正确答案: C

答案解析:

重载运算符不能改变原有运算符的优先级, 结合性和操作数个数.

  1. 有如下类模板定义:()
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无法匹配到此模版中的+重载函数,编译器无法识别


如有不同见解,欢迎留言讨论!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值