在编写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"是为了C与C++兼容。对,是为了C与C++兼容,可是是兼容什么呢?如果你还不知道,并且对这个问题感兴趣,就请继续浏览这篇文章。
要弄清楚这个问题,首先要弄清楚一个概念:符号。
当然要弄清楚这个概念,还有其他一些东西要搞清楚,让我为各位慢慢道来。
符号的概念
我们知道,用编译器编译一个源代码后,会生成一个目标文件,该目前文件就是我们通常所说的以.o或.obj为后缀结尾的文件。多个目标文件进行链接最终形成一个可执行文件(在Windows下通常是exe文件)或者生成一个库文件(在Unix下通常是.so或a文件,在Windows下通常是.dll)文件。
现在看下面两段源代码a.c和b.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在参与链接的时候,它怎么知道它引用道的globalA和funA的地址在哪里呢?也就是说它到哪里去找到globalA和funA,到哪里去引用它?
答案是:
编译器编译生成的目标文件,会产生一个对应的符号表(Symbol Table),这个表中记录了目标文件所使用的所有符号,包括它自己定义的和它引用的(外部符号)。目标文件中自己定义的符号,有一个对应的值,就是符号值,对于变量和函数来说,符号值就是它们的地址。
在C语言中globalA在符号表中的名称就是globalA,funA在符号表中的名称就是funA,它们在a.o或a.obj中,会有一个符号对应的地址记录,也就是它们的符号值。
对于b.o或b.obj而言,funA和globalA属于外部符号。在它参与链接的过程中,简单的说,就是遍历所有的参与链接的文件,寻找存在funA和globalA符号的目标文件,然后引用funA和globalA符号所在目标文件的符号值。
大家明白了吗?如果明白了,可能会问这和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.c是C代码,使用C编译器编译。
b.c的内容:
extern int globalA;
extern void funA()
void funB()
{
funA();
pirntf(“%d:, globalA);
}
上面的b.c是C++编译的,它需要使用一个外部变量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);
}
这样,funA在b.c的目标文件中,要引用的外部符号名称就会是funA,而不是_Z4funA。
上面的extern “C”申明也可以是这样的:
extern “C” void funA();
extern “C” int globalA;
所以如果一个C++模块引用一个C模块,就必须显示声明extern “C”
最后一个问题:__cplusplus是干什么用?
extern “C”是C++支持的,C并不支持,__cplusplus是C++编译器的预编译宏,如果存在该宏定义,就说明当前编译器是C++编译器。因此下面的代码意思是,如果是C++编译器,就在代码中包含extern “C”声明,否则不包含(也就是说,C代码根本不会用到extern “C”,因为它不支持,只有C++才会用到)
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif /* __cplusplus */
#ifdef __cplusplus
#if __cplusplus
}
#endif