一亩三分地

一份辛勤,一份收获,愿与大家共享,互相勉励!

   :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

      在上世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量及函数的名称是一样的。比如一个代码里面包涵了一个foo函数,那么编译成目标文件以后,其中对应的符号名也是foo。当后来UNIX平台和C语言发明时,已经存在了相当多这样的库和目标文件。这就产生了一个问题,如果一个C程序要使用这些库的话,它就不可以使用这些库中定义的函数和变量做为符号名,否则就会和现有的目标文件冲突。比如有个用汇编编写的库中定义了一个函数或变量叫做main,那其他所有调用这个库的代码里就不能再定义main了。

      为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言源代码文件中所有的全局变量和函数经过编译后,要在相应的符号名前加上下划线"_",而Fortran则要求在所有符号名前加上"_",后面也加上"_"。比如foo函数,C语言编译后的符号名是"_foo",而Fortran的则是"_for_"。这种简单而原始的方法的确能暂时减少多种语言的目标文件之间符号冲突的概率,但并没有从根本上解决此问题。比如同一种语言编写的目标文件还是有可能会产生符号冲突,当程序很大尤其是由不同部门或个人开发时,也有可能导致冲突。当然,随着时间的推移,很多操作系统和编译器被完全重写了好几遍。UNIX也分化成了很多种,整个环境也发生了很大的变化,上面提到的C语言与Fortran、汇编库的符号冲突问题也已经不是那么明显了。现在的Linux GCC编译器,默认情况下已经去掉了在C语言符号名前加"_"的方式,在Windows平台下的编译器还保持着这样的传统。

      C++这样的后来设计的语言开始考虑到这个问题,增加了名称空间(Namespace)的方法来解决多模块的符号冲突问题,但是强大而复杂的C++拥有的类、继承、虚机制、重载等特性无疑又使符号管理更为复杂。举个简单的例子,两个相同名称的函数func(int)和func(double),函数名相同但参数列表不通,这是C++里函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?下面我们引入一个术语叫函数签名,它包含了一个函数的信息:函数名、参数类型、所在的类和名称空间及其他信息。函数签名用于识别不同的函数,而函数的名称只是函数签名的一部分。由于上例中两个同名函数的参数类型不同,我们可以认为它们的函数签名不同而认为是不同的函数。在编译器及链接器处理符号时,它们则使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称。编译器在将C++源代码编译成目标文件时,会将函数和变量的名称进行修饰,形成符号名,目标文件中所使用的符号名就是修饰后名称,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。

如下面6个函数的函数签名在GCC编译器下获得的修饰后名称如下:

     签名和名称修饰机制不光被使用在函数上,C++中的全局变量和静态变量也有同样的机制。不同的编译器厂商的名称修饰方法可能不同,这里就不详细描述其细节及异同了。

     相信说到这里,大家都明白为什么在跨平台的C++代码中经常可以见到extern "C"的写法了吧?显然C++编译器会将在extern "C"大括号内部的代码当做C语言代码处理,C++的名称修饰机制不会对此起作用。上文说到对于一个函数foo,Linux版本的GCC不会将foo函数修饰成_foo,而Visual C++却会将C语言代码修饰成_foo。而在很多时候,我们会碰到有些头文件声明了一些C语言的函数和全局变量,但这个头文件可能会被C或C++代码包含。比如C语言库函数string.h中的memset函数,原型如下:

     void*memset(void *, int , size_t);

     如果不加任何处理,当C语言程序包含string.h并用到memset函数,编译器能正常处理memset符号;但在C++语言中,编译器会将memset函数签名修饰成_Z6memsetPvii,这样链接器就无法和C语言库中的memset符号进行链接了。所以对于C++来说,必须使用extern "C"来声明此函数,针对C和C++代码的不同,编译器使用宏"__cplusplus"来区分。

posted on 2014-03-30 22:01  斯米戈l  阅读(428)  评论(0编辑  收藏  举报