C++函数重载的背后

导入

我们都知道C语言没有函数重载,C++有函数重载,那么有没有想过这背后是怎么实现的呢?主要其实靠一个技术:符号修饰(name mangling)。

具体机制

符号修饰就是C++会对C++的变量名包括函数名类名等进行符号的修改,比如一个函数声明是这样void func(int a, float b);,那么g++编译器会将函数名修饰成_Z4funcif,其中_Z类似前缀,4是函数名字符数,func一共4个字符,i是int表示第一个参数类型,f是float表示第二个参数类型。再比如一个类成员函数:

class MyClass {
public:
    void method(double d);
};

会被修饰成_ZN7MyClass6methodEd,其中 表示 MyClass 的字符数,表示 method 的字符数,代表函数结束,表示参数类型为 double

如果包含命名空间,那么最开头同样会加上命名空间前缀。再比如静态函数:

static int myStaticFunction()会修饰成_Z18myStaticFunctionv,v 表示该函数没有参数,可以看到static静态函数和普通函数的符号没有区别。

接着再看参数包含引用和指针的情况:

  • int add(int *a)修饰成_Z3addPi ,P就代表指针

  • int add(int &a)修饰成_Z3addOi,O就代表左值引用

  • int add(int &&a)修饰成_Z3addRi,R就代表右值引用

通过上面所有例子,可以总结如下:

  • 函数名和长度以及参数类型会被表示到符号修饰中

  • 返回值符号修饰不考虑,这也是为什么函数重载不受返回值影响,因为不同返回值的符号修饰是一样的,在编译器看来是一个函数定义,发生冲突出错

  • 被命名空间和类包围的函数,会一层层加上外围的特性,这也是为什么不同命名空间和类下可以有相同名字的函数,因为符号修饰前面的部分不一样,编译器能够判断是不同的函数

  • 指针,左值引用,右值引用的符号修饰不一样,因此三个可以同时存在,形成重载。但是有一点要注意,普通值传参和引用传参(包括左值引用和右值引用),虽然符号修饰不一样,但是编译器无法分辨该传给谁,因为左值引用,右值引用,值传递都能接收左值,并且值传递和引用传递之间没有优先级的关系,造成混乱,所以编译器不支持。而左值引用和右值引用同时存在是没问题的,因为虽然这两个都可以接受左值参数,但是有优先级关系,优先给左值引用函数,没有才会传入右值引用函数。

  • 还有一点,例子都是用g++编译器为例,而不同编译器符号修饰规则是自由的,比如MSVC的CL编译器,获取的符号就完全不一样,而且几乎不可读。

查看符号有几种方式:

  • 直接使用g++ -S变成汇编文件,直接打开就可以找到符号

  • objdump -t 查看符号表,windows可以使用visual studio自带的dumpbin /symbols

  • nm -u/-a 查看符号表

额外

提到符号表,就额外说一些,当我们使用上面介绍的方式查看符号表的时候,如果我们是在windows端,可能会查不到任何符号表。这是完全正常的,因为PE文件正常是没有符号表的。如果你了解PE格式,应该知道,PE格式中只有文件末尾的COFF符号表是包含符号的,但是COFF符号表是可选段,并且一般为0。因此才会查不到符号表,windows使用MSVC编译时,如果使用选项/Zi就会生成调试信息,额外产生一个.pdb文件,这个文件中就包含调试信息和符号信息。

而Linux的ELF文件,默认是带.symtab符号表节的,只有当你使用g++编译器的-s选项,或者直接使用strip命令后,才会移除.symtab符号表

再多说一句,这也是为什么windows端链接DLL动态库需要一个导入库,而Linux链接so共享库不需要,因为windows的DLL动态库也属于PE文件,也没有符号表,但是链接DLL需要符号表信息来填充IVT导入地址表,否则无法知道链接了哪些函数。而Linux的so是ELF文件,包含符号表,因此无需额外的导入库。