extern "C" 本质论——符号和符号修饰

来源:互联网 发布:html入门书籍推荐知乎 编辑:程序博客网 时间:2024/06/04 23:20

在编写C语言代码的时候,经常在代码中会看到如下所示的代码:

#ifdef __cplusplus

#if __cplusplus

extern "C"{

#endif

#endif /* __cplusplus */

 

#ifdef __cplusplus

#if __cplusplus

}

#endif

#endif /* __cplusplus */

 

上面代码中的extern "C"__cplusplus是干什么用的?

 

先从extern "C"说起,估计100个人里面会有99个人马上告诉我答案:extern "C"是为了CC++兼容。对,是为了CC++兼容,可是是兼容什么呢?如果你还不知道,并且对这个问题感兴趣,就请继续浏览这篇文章。

 

要弄清楚这个问题,首先要弄清楚一个概念:符号。

当然要弄清楚这个概念,还有其他一些东西要搞清楚,让我为各位慢慢道来。

 

符号的概念

我们知道,用编译器编译一个源代码后,会生成一个目标文件,该目前文件就是我们通常所说的以.o.obj为后缀结尾的文件。多个目标文件进行链接最终形成一个可执行文件(在Windows下通常是exe文件)或者生成一个库文件(在Unix下通常是.soa文件,在Windows下通常是.dll)文件。

现在看下面两段源代码a.cb.c

a.c的内容:

int globalA = 0;

void funA() { // do something}

 

b.c的内容:

extern int globalA

extern void funA()

void funB()

{

     funA();

     pirntf(“%d:, globalA);

}   

上面的代码中,b.c引用了a.c中的全局变量globalA和函数funA。而链接的本质是什么?链接的本质就是将多个目标文件拼接成一个可执行文件或库文件,而拼接的过程就是目标文件之间对地址的引用,即对函数和变量地址的引用。

问题来了,b.c在参与链接的时候,它怎么知道它引用道的globalAfunA的地址在哪里呢?也就是说它到哪里去找到globalAfunA,到哪里去引用它?

答案是:

编译器编译生成的目标文件,会产生一个对应的符号表(Symbol Table),这个表中记录了目标文件所使用的所有符号,包括它自己定义的和它引用的(外部符号)。目标文件中自己定义的符号,有一个对应的值,就是符号值,对于变量和函数来说,符号值就是它们的地址。

C语言中globalA在符号表中的名称就是globalAfunA在符号表中的名称就是funA,它们在a.oa.obj中,会有一个符号对应的地址记录,也就是它们的符号值。

 

对于b.ob.obj而言,funAglobalA属于外部符号。在它参与链接的过程中,简单的说,就是遍历所有的参与链接的文件,寻找存在funAglobalA符号的目标文件,然后引用funAglobalA符号所在目标文件的符号值。

 

大家明白了吗?如果明白了,可能会问这和extern "C"有什么关系。您别着急,要弄清楚,还要看C++中的符号又是怎样的,弄清楚这个,才能知道本质。

C++符号修饰

如前所述,C语言中变量a的符号,就是a,函数func的符号就是func。但是在C++中就不是那么一回事了。

看下面几个函数定义:

int func(int i) { …}

float func(float f ) {… }

C语言中,如果上面的几个函数在不同的模块中,在链接的时候,就会报重复定义错误;如果是在同一个源代码中,就会直接产生编译错误。原因是这几个函数的符号在C语言中都是func。链接时,一个符号在几个地方同时定义,将导致引用到它的地方不知道该引用哪一个,也就是通常所说的重复定义。

 

但是上面的函数定义在C++中完全是合法的,在C++中叫做函数重载。不论是编译还是链接都没有问题!为什么?答案就是,上面两个函数在编译生成目标文件时生成的符号是不一样的!

C++编译器,会给符号加上符号修饰。为了使我们的例子更加丰富一些,我们用下面的例子举例,除了上面两个函数外,我们还加入了一个类,和一个命名空间。

int func(int i);

float func(float f );

 

class C{

       float func(float);

class C2{

       int func(int)

};

};

 

namespace N{

       int func(int);

       class C{

              int func(int);

}

};

不同的编译器的符号修饰方法可能不同,下面分别以Visual C++编译器和GCC编译期为例。

Visual C++编译器:

           函数签名

符号名称

int func(int)

?func@@YAHH@Z

float func(float)

?func@@YAMM@Z

int C::func(int)

?func@C@@AAEHH@Z

int C::C2::func(int)

?func@C2@C@@AAEHH@Z

int N:::func(int)

?func@N@@YAHH@Z

int N::C:::func(int)

?func@C@N@@AAEHH@Z

PS:使用过Visual C++的同学,在编译的时候,也没有碰到过类似”undefined refercnce to ‘?func@@YAHH@Z’”的奇怪打印 ^_^

 

GCC编译期:

           函数签名

符号名称

int func(int)

_Z4funci

float func(float)

_Z4funcf

int C::func(int)

_Z4N1C4funcEi

int C::C2::func(int)

_Z4N1C2C24funcEi

int N:::func(int)

_ZN1N4funcEi

int N::C:::func(int)

_ZN1N1c4funcEi

 

GCC的例子进行简要说明一下:

GCC C++符号修饰中:所有符号都以_Z开头,对于嵌套的名字,后面紧跟N,然后是各个名称空间和类的名字,每个名字前是名字字符串的长度,在以E结尾。对于变量也存在同样的类似规则。

 

哈哈,各位看官,到此有没有看出点门道出来。好吧,现在我们可以言归正转了,extern “C”到底是什么?

 

extern “C”本质

还是以b.c为例子,只不过我们假设b.c使用C++编译器编译,也就是说,它是C++代码。而a.cC代码,使用C编译器编译。

b.c的内容:

extern int globalA

extern void funA()

void funB()

{

     funA();

     pirntf(“%d:, globalA);

}   

上面的b.cC++编译的,它需要使用一个外部变量globalA和一个外部函数funA。然而由于是C++代码,因此b.c生成目标文件,要引用的外部符号是加上了符号修饰的。以funA,GCC编译期为例,b.c要引用的符号是_Z4funA

可是a.c是一个C代码,funA生成的符号就是funA!因此b.c在进行链接时,根本就找不到funA,因为两者生成的符号根本不一样!

怎么解决这个问题?C++编译器会将extern “C”内的代码当作C语言代码处理。

如下:

extern “C”

{

     int globalA

extern void funA();

}

void funB()

{

     funA();

     pirntf(“%d:, globalA);

}   

这样,funAb.c的目标文件中,要引用的外部符号名称就会是funA,而不是_Z4funA

上面的extern “C”申明也可以是这样的:

extern “C” void funA();

extern “C” int globalA;

 

所以如果一个C++模块引用一个C模块,就必须显示声明extern “C”

 

最后一个问题:__cplusplus是干什么用?

extern “C”C++支持的,C并不支持,__cplusplusC++编译器的预编译宏,如果存在该宏定义,就说明当前编译器是C++编译器。因此下面的代码意思是,如果是C++编译器,就在代码中包含extern “C”声明,否则不包含(也就是说,C代码根本不会用到extern “C”,因为它不支持,只有C++才会用到)

#ifdef __cplusplus

#if __cplusplus

extern "C"{

#endif

#endif /* __cplusplus */

 

#ifdef __cplusplus

#if __cplusplus

}

#endif

原创粉丝点击