导入
我们都知道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
,其中 7
表示 MyClass
的字符数,6
表示 method
的字符数,E
代表函数结束,d
表示参数类型为 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文件,包含符号表,因此无需额外的导入库。