最新消息:20210816 当前crifan.com域名已被污染,为防止失联,请关注(页面右下角的)公众号

[学习记录]extern “C“ 的深层次含义

工作和技术 crifan 1835浏览 0评论

在C++代码中,如果加上类似于:

extern &quot;C&quot; {<br />  int func(int);<br />  int var;<br />}

意思是告诉编译器,上面extern “C”中的这段代码,要当作C 语言处理。
但是具体深层含义,我自己是从来没去关注过,知道看了《程序员的自我修养 –链接,装载与库》,才更加清楚一点,记录如下(后半部分内容主要摘抄自原文):
1。首先你要了解,代码编译后的,关于符号(symbol)的知识
2。其次,你要大概知道代码从源码到可执行文件所要经历的步骤,及其每一步的作用。
3。此处的extern "C"就是,不论你的C++代码被编译成什么类型的库(dll,lib,.a,.so),所用到的编译器都会将源码里面的函数,变量等,根据C++编译器自己(目前主要分GNU GCC和Visual C++)的名称修饰规则,将原先名称的变量,变成另外一系列的名称,便于之后的连接器的链接。
书中给出一个例子:
[关于C++的名称修饰的细节]

int func(int);<br />float func(float);
class C {<br />int func(int);<br />class C2 {<br />int func(int);<br />};<br />};
namespace N {<br />int func(int);<br />class C {<br />int func(int);<br />};<br />}
中的这6个func函数,经过名称修饰后,就变成:<br /><strong>(1)对于GUN C++:</strong>

函数签名

修饰后名称(符号名)

int func(int)

_Z4funci

float func(float)

_Z4funcf

int C::func(int)

_ZN1C4funcEi

int C::C2::func(int)

_ZN1C2C24funcEi

int N::func(int)

_ZN1N4funcEi

int N::C::func(int)

_ZN1N1C4funcEi

GNU C++名称修饰规则示例

GCC的基本C++名称修饰方法如下:所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N",然后是各个名称空 间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。对于一个函数来 说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为 _ZN1N1C4funcEi。更为具体的修饰方法我们在这里不详细介绍,有兴趣的读者可以参考GCC的名称修饰标准。幸好这种名称修饰方法我们平时程序 开发中也很少手工分析名称修饰问题,所以无须很详细地了解这个过程。binutils里面提供了一个叫"c++filt"的工具可以用来解析被修饰过的名 称,比如:

$ c++filt _ZN1N1C4funcEi
N::C::func(int)

<strong>(2)对于Visual C++:</strong>

函数签名

修饰后名称

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

Visual C++名称修饰规则示例

Visual C++的名称修饰规则并没有对外公开,当然,一般情况下我们也无须了解这套规则。此处只是学习,了解一下即可。

作者以int N::C::func(int)这个函数签名来猜测Visual C++的名称修饰规则:

修饰后名字由"?"开头,接着是函数名由"@"符号结尾的函数名;后面跟着由"@" 结尾的类名"C"和名称空间"N",再一个"@"表示函数的名称空间结束;第一个"A"表示函数调用类型为"__cdecl"(函数调用类型我们将在第4 章详细介绍),接着是函数的参数类型及返回值,由"@"结束,最后由"Z"结尾。

可以看到函数名、参数的类型和名称空间都被加入了修饰后名称,这样编译器 和链接器就可以区别同名但不同参数类型或名字空间的函数,而不会导致link的时候函数多重定义。

但是有时候可能须要将一个修饰后名字转换成函数签名,比如在链接、调试程 序的时候可能会用到。Microsoft提供了一个UnDecorateSymbolName()的API,可以将修饰后名称转换成函数签名。下面这段代 码使用UnDecorateSymbolName()将修饰后名称转换成函数签名:

/* 2-4.c
* Compile: cl 2-4.c /link Dbghelp.lib
* Usage: 2-4.exe DecroatedName
*/
#include <Windows.h>
#include <Dbghelp.h>int main( int argc, char* argv[] )
{
char buffer[256];    if(argc == 2)
{
UnDecorateSymbolName( argv[1], buffer, 256, 0 );
printf( buffer );
}
else
{
printf( "Usage: 2-4.exe DecroatedNamen" );
}    return 0;
}

[extern “C”的具体含义]
所以,如果加上了 extern “C”,那么,C++编译器会将在extern "C" 的大括号内部的代码当作C语言代码处理。也就是,对于上面代码中,C++的名称修饰机制将不会起作用。这就使得,在其后的链接过程中,所能看到的变量,就是C 语言里面中的类似的变量了。在Visual C++平台下会将C语言的符号进行修饰,所以上述代码中的func和var的修饰后符号分别是_func和_var;但是在Linux版本的GCC编译器 下却没有这种修饰,extern "C"里面的符号都为修饰后符号,即前面不用加下划线。如果单独声明某个函数或变量为C语言的符号,那么也可以使用如下格式:

extern &quot;C&quot; int func(int);<br />extern &quot;C&quot; int var;

上面的代码声明了一个C语言的函数func和变量var。

A。关于名称修饰规则的小实验
我们可以使用上述的机制来做一个小实验:

// ManualNameMangling.cpp<br />// g++ ManualNameMangling.cpp -o ManualNameMangling
#include <stdio.h>
namespace myname {<br />int var = 42;<br />}
extern "C" double _ZN6myname3varE;
int main()<br />{<br />printf( &quot;%dn&quot;, _ZN6myname3varE );<br />return 0;<br />}

上 面的代码中,我们在myname的名称空间中定义了一个全局变量var。根据我们所掌握的GCC名称修饰规则,这个变量修饰后的名称 为"_ZN6myname3varE",然后我们手工使用extern "C"的方法声明一个外部符号_ZN6myname3varE,并将其打印出来。我们使用GCC来编译这个程序并且运行它,我们就可以得到程序输出为 42:

$ g++ ManaulNameMangling.cpp -o ManualNameMangling<br />$ ./ManualNameMangling<br />42<br /><br /><strong>B。常见情况示例解析</strong>

很多时候我们会碰到有些头文件声明了一些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"来声明memset这个函数。但是C语言又不支持extern "C"语法,如果为了兼容C语言和C++语言定义两套头文件,未免过于麻烦。幸好我们有一种很好的方法可以解决上述问题,就是使用C++的 宏"__cplusplus",C++编译器会在编译C++的程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码。具体代码如 下:

#ifdef __cplusplus<br />extern &quot;C&quot; {<br />#endif<br /><br />void *memset (void *, int, size_t);
#ifdef __cplusplus<br />}<br />#endif

如果当前编译单元是C++代码,那么memset会在extern "C"里面被声明;如果是C代码,就直接声明。上面这段代码中的技巧几乎在所有的系统头文件里面都被用到。

[总结]
加了extern "C" 后,就是让C++编译器知道,此部分的代码,要用C 语言的名称修饰规则处理。

[引用]
1。程序员的自我修养:链接、装载与库 (含介绍 以及 第三章和第十一章的内容
http://book.51cto.com/art/200904/120986.htm

转载请注明:在路上 » [学习记录]extern “C“ 的深层次含义

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
80 queries in 0.172 seconds, using 22.14MB memory