笔记由来:《程序员的自我修养》
约在20世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字一样。比如一个汇编源码里包含一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中的相对应的符号名也是foo。当后来Unix平台和C语言发明时,已经存在相当多汇编写的库和目标文件。这时会产生一个问题,哪就算C程序使用这些库文件的话,C语言就不能使用这些库中定义的汉斯和变量的名字作为符号名否则就会与现有的目标文件冲突。为了防止类似的符号名冲突,Unix下C语言就规定,C语言源代码中所有全局变量和函数经过编译以后,相对应的符号名钱加下划线"_",比如C语言函数“foo”那么编译以后的符号名就是“_foo".随着时间的推移,很多操作系统和编译器被完全重写的好几遍,在现在Linux 下的GCC编译器中,默认已经去掉在C语言符号前加"_"。Gcc编译可以通过参数选项-fleading-underscore 或 -fno-leading-underscore 打开和关闭是否在C语言符号前加下划线。
C++符号修饰由于C++拥有类 继承 虚机制 重载 名称空间(吐槽下:C++ 应该作为一门新语言,而不是在新语言上增加对C的支持,很容易造成与C语言体系的混淆)
回到正题:
简单的例子,
两个相同名字的函数
func(int) 和 func(double)
尽管函数名相同,但参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过中如何区别两个函数呢,聪明的地球人发明了符号修饰(Name Decoration) 的机制.
函数签名 | 修饰后的名称(符号名) | |
---|---|---|
int func(int) | _Z4funci | |
int func(int) | _Z4funci | |
int func(float) | _Z4funcf | |
int C::func(int) | _ZN1C4funci | |
int N::func(int) | _ZN1N4funci |
上述func 函数为在不类和命名空间中带不同参数时经过编译器将C++源码编译成目标文件时(gcc -c xx)
通过名称修饰所的结果对照.修饰规则自行查阅资料。名称修饰也被用来防止静态变量的名字冲突。
比如:
main()函数里面有一个静态变量叫foo,而func()函数里面也有一个静态变量叫foo
为了区分者两个变量,gcc会将他们的符号名修饰成两个不同的名字_ZZ4mainE3foo 和_ZZ4funcvE3foo;
这样就区分了这两个变量。
主题:
extern "C"
C++ 为了与C兼容,在符号管理上,C++有一个用来声明或者定义一个C的符号的“extern C” 关键字用法:
extern "C" { int func(int); int var; }
C++ 编译器会将在extern "C" 的大括号内部的代码当作C语言代码来处理。所以C++ 的名称修饰机制将不会起作用。小实验:
#include namespace myname{ int var = 43; } extern "C" double _ZN6myname3varE; int main() { printf("%d\n",_ZN6myname3varE); return 0; }
$g++ test.cpp -o test
$./test
42
很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言或者C++代码包含。比如我们C语言函数中的string.h 中声明了memset 这个函数,他的原型如下:
void *memset(void*, int , size_t);
如果不加处理,当我们的C语言包含string.h的时候,并且用带了memset这个函数,编译器将memset符号引用正确处理; 但是在C++语言中,编译器会认为这是一个memset函数是一个C++函数,将memset符号修饰成 :
_Z6memsetPvii
这样连接器就无法与C语言库中的memset符号进行链接。所以对C++来树,必须使用extern "C"来声明。但是C语言又不支持extern "C" 语法,如果为了兼容C语言和C++语言定义两套头文件,未免太麻烦。为此,我们使用C++宏定义"__cplusplus".C++编译器会在编译C++程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码。
#ifdef __cplusplus extern "C" { #endif void *memset(void *, int , size_t); #ifdef __cplusplus } #endif
至此结束。
想继续深究的同学可参阅 《程序员的自我修养》 。