456-C++函数重载机制(汇编层面分析)

1、什么是函数重载(what)?

  • 在同一作用域内,可以有一组相同函数名,不同参数列表的函数,这组函数被称为函数重载。
  • 重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了命名空间的污染,对于程序的可读性有很大的好处。

When two or more different declarations are specified for a single name in the same scope, that name is said to overloaded. By extension, two declarations in the same scope that declare the same name but with different types are called overloaded declarations. Only function declarations can be overloaded; object and type declarations cannot be overloaded.
——摘自《ANSI C++ Standard. P290》

举例: overload.cc

#include<iostream>
using namespace std;
void print(int i)
{
        cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
        cout<<"print a string :"<<str<<endl;
}

int main()
{
        print(12);
        print("hello world!");
        return 0;
}

上面print(12)会去调用print(int),print(“hello world”)会去调用print(string)。

如下面的结果:(先用g++ overload.cc -o overload编译,然后执行)

g++ overload.cc -o overload

在这里插入图片描述

2、为什么需要函数重载(why)?

  • 如果没有函数重载,如在 C 中,你必须要这样去做:为这个 print 函数取不同的名字,如 print_int、print_string。这还只是两种情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印 long 型、char*、各种类型的数字等等。这样做很不友好!
  • 类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实现实例化不同的对象,那是相当的麻烦!
  • 操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如 + 可用于连接字符串等!

3、编译器如何解决命名冲突的?

为了了解编译器是如何处理这些重载函数的,我们反编译下上面我们生成的执行文件,看下汇编代码(全文都是在Linux下面做的实验)。

我们执行命令objdump -d overload > log.txt反汇编并将结果重定向到log.txt 文件中,然后分析 log.txt 文件。

objdump -d overload > log.txt

在这里插入图片描述

1、发现函数void print(int i)编译之后为:(注意它的函数签名变为——_Z5printi)
在这里插入图片描述
2、发现函数void print(string str)编译之后为:(注意它的函数签名变为——__Z5printNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE),其实 basic_string 是一个模板类,string 是模版形参为 charbasic_string 模版类的类型定义。

在这里插入图片描述
我们可以发现编译之后,重载函数的名字变了不再都是 print!这样不存在命名冲突的问题了;

但是又有一个新的问题: 变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?

  • 我的第一反应是:函数名 + 参数列表,因为重载函数取决于参数的类型,个数,而与返回值无关。

但看下面的映射关系

  • void print(int i) – > _Z5printi
  • void print(string str) – > _Z5printNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

进一步猜想:

  • 前面的 Z5中的5 表示函数名长度,print 为函数名,i 表示整型,basic_string 表示字符串;
  • 即映射为 函数名长度+函数名+参数列表
  • 最后在 main 函数中就是通过 这两个名称来调用对应的函数的。

在这里插入图片描述
在这里插入图片描述

我们发现好像 string 有点特殊,我们再写几个重载函数验证一下猜想,如:

  • void print(double l) --> _Z5printd
  • void print(char c) --> _Z5printc

可以发现大概是int->i,double->d,char->c….本上都是用首字母代表;

#include<iostream>
using namespace std;

void print(int i)
{
    cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
    cout<<"print a string :"<<str<<endl;
}


void print(double str)
{
    cout<<"print double :"<<str<<endl;
}


void print(char str)
{
    cout<<"print a char :"<<str<<endl;
}


int main()
{	
    print(12);
    print("hello world!");
    print(1.234);
    print('w');
    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:

#include<iostream>
using namespace std;

int max(int a,int b)
{
        return a>=b?a:b;
}

double max(double a,double b)
{
        return a>=b?a:b;
}
int main()
{
        cout<<"max int is: "<<max(1,3)<<endl;
        cout<<"max double is: "<<max(1.2,1.3)<<endl;
        return 0;
}

在这里插入图片描述

生成反汇编代码:

objdump -d overload2 > log2.txt

在这里插入图片描述
在这里插入图片描述

  • int max(int a,int b) 映射为_Z3maxiidouble max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代表的是函数名的字符个数
  • 上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“函数名长度+函数名+参数列表”。
  • Linux下映射规则:_Z+函数名长度+函数名+类型首字母

4、程序员的自我修养—符号修饰和函数签名

int func(int);

float func(float);

class C {

    int func(int);

    class C2 {

        int func(int);

    };

};

namespace N {

    int func(int);

    class C {

        int func(int);

    };

}

上面的6个函数签名在GCC编译器下,相对应的修饰后名称如下表所示:
在这里插入图片描述
GCC的基本C++名称修饰方法如下:

  • 所有的符号都以“_Z”开头;
  • 对于嵌套的名字(在名称空间或在类里面的),后面紧跟“N”,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以“E”结尾。
  • 对于一个函数来说,它的参数列表紧跟在“E”后面,对于int类型来说,就是字母“i”。

签名名称修饰机制不光被使用到函数上,C++中的全局变量和静态变量也有同样的机制

全局变量:

  • 对于全局变量来说,它跟函数一样都是一个全局可见的名称;
  • 它也遵循上面的名称修饰机制,比如一个名称空间 foo 中的全局变量bar,它修饰后的名字为 :_ZN3foo3barE
  • 注意变量的类型并没有被加入到修饰后名称中,所以不论这个变量是整形还是浮点型甚至是一个全局对象,它的名称都是一样的。

名称修饰机制也被用来防止静态变量的名字冲突

  • 比如 main() 函数里面有一个静态变量叫 foo ,而 func() 函数里面也有一个静态变量叫 foo 。为了区分这两个变量,GCC会将它们的符号名分别修饰成两个不同的名字 _ZZ4mainE3foo 和 _ZZ4funcvE3foo ,这样就区分了这两个变量。

不同的编译器厂商的名称修饰方法可能不同,所以不同的编译器对于同一个函数签名可能对应不同的修饰后名称。

比如上面的函数签名中在Visual C++编译器下,它们的修饰后名称如下表所示。

在这里插入图片描述

我们以 int N::C::func(int) 这个函数签名来猜测Visual C++的名称修饰规则:

  • 修饰后名字由“?”开头,接着是函数名由“@”符号结尾的函数名;
  • 后面跟着由“@”结尾的类名“C”和名称空间“N”,再一个“@”表示函数的名称空间结束;
  • 第一个“A”表示函数调用类型为 “__cdecl ”(函数调用类型我们将在第4章详细介绍),接着是函数的参数类型及返回值,由“@”结束,最后由“Z”结尾。
  • 可以看到函数名、参数的类型和名称空间都被加入了修饰后名称,这样编译器和链接器就可以区别同名但不同参数类型或名字空间的函数,而不会导致link的时候函数多重定义。

由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。

总结:

  • C++的符号修饰在不同的编译器(GCC、Visual C++都有不同的规则),函数修饰后的名称与函数名称、参数名、函数返回值、函数所在的类、函数所在的namespace有关

5、为什么函数返回值不作为重载条件呢?

既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?

  • 当编译器能从上下文中确定唯一的函数的时,如int ret = func(),这个当然是没有问题的。

  • 然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,一个函数为
    void func(int x);另一个为int func(int x); 当我们直接调用func(10),这个时候编译器就不确定调用那个函数。

  • 所以在c++中禁止使用返回值作为重载的条件。

6、形参类型、形参个数完全相同的函数也是可以重载的,只要它的 const/volatiles 属性不同即可

#include <iostream>
using namespace std;
struct foo{
	int bar() const {return 2;} ; //第一个重载函数
	int bar() {return 1;}; //第二个重载函数
};
int main()
{
	const foo a;
	a.bar(); 
	foo b;
	b.bar(); 
} 

在这里插入图片描述
可以看出,是可以重载的!

我们查看一下反编译代码:可以看出这两个 bar 函数的命名并不相同!

objdump -d overload3 > log3.txt

在这里插入图片描述
在这里插入图片描述

我们再查看一下main函数的调用情况:

在这里插入图片描述
我通过单步跟踪,发现a.bar() 调用的是第一个重载函数,b.bar() 调用的是第二个重载函数。

7、为什么 C++支持函数重载,而 C 语言不支持呢?

#include <stdio.h>
void print_int(int i)
{
       printf("%d\n", i);
}
void print_char(char c)
{
       printf("%c\n", c);
}
}
int main()
{
        print(1);
		print('a');
        return 0;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在 C 语言下,映射机制就是直接使用函数名来进行映射的,跟函数的返回值,参数列表无关,所以注定 C 语言不能实现函数重载。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值