C++符号修饰与extern "C"

Last Updated: 2023-12-22 08:56:34 Friday

-- TOC --

本文介绍C++的符号修饰(name mangling),以及extern "C"这个申明的含义。

C++符号修饰

由于C++支持的抽象层度比C要高,一些高级编程语言特性,如函数重载,类定义,类继承,namespace等等,就需要允许源代码中有名称相同的函数或变量存在。因此C++编译器在编译时,必须要做符号修饰,这是为了区分相同名称的不同对象。C++只能通过符号修饰的方法,来支持那些高级编程语言特性的编译链接过程。一般情况下,程序员不用关心符号修饰的细节。

所谓符号修饰,即C++编译器按照某种规则,对不同对象但名称相同的名称进行修改,或名称变换,以便在编译器内部,可以识别出源代码中相同的名称所对应的不同的符号。

大约在20世纪70年代以前,编译器生成目标文件时,符号名与对应的函数名或变量名是一样的。比如一段汇编代码中包含一个符号名为foo,那么汇编器生成的目标文件,符号的名称也是foo。当后来C语言被发明,Unix被C语言重写时,已经存在相当多的用汇编写的库和目标文件。这就出现了一个问题,如果一个C程序要使用这些库,那么C语言代码中就不可以出现这些库中已经存在符号名,否则链接时就会出现冲突。

为了防止符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局变量和函数,经过编译后,相对应的符号名前加上_。Fortran语言,在编译后,符号名前后都加_。后来随着时间的推移,很多OS和编译器都完全重写了好几遍,UNIX分叉,Linux大行其道,整个环境发生了很大变化。现在Linux下GCC默认已经去掉在符号名前加_的这种方法,但Windows下的编译器还保留这种传统,GCC在Windows下的版本(cygwin,mingw)也会加_这样就可以理解,C++编译器必须使用符号修饰,以支持函数重载,类,namespace等特性。

binutils里面有个工具c++filt,可以用来解析被修饰过的名称。当出现找不到符号的时候,也许看一眼这个符号的真身,就能定位问题:

$ c++filt _ZN18CliprdrFileManager22sigFileNodeDataRequestEPNS_19FileNodeDataRequestE
CliprdrFileManager::sigFileNodeDataRequest(CliprdrFileManager::FileNodeDataRequest*)

不同编译器对符号修饰的规则不同,这是导致不同编译器之间不能互操作的主要原因之一。符号修饰规则没有标准。

extern "C"申明

在C++中链接C风格的接口,需要使用extern "C"关键字在C++中申明C风格的符号。

一般的使用方式如下:

#ifdef __cplusplus
extern "C" {
#endif
...
// your code here
...
#ifdef __cplusplus
}
#endif

在extern "C"范围内申明的符号,不会被C++编译器进行符号修饰,而是采用C语言的风格对符号进行处理。C编译器不会定义__cplusplus这个Macro,因此以上定义方式,可以同时兼容C和C++代码。

这样申明后,因为符号名称没有被修饰过,链接时就能够在对应的目标文件或库文件中找到这些符号。这个技巧几乎在所有系统头文件中被使用。

另一种单行申明的方式:

extern "C" int func(int);
extern "C" int var;

举个例子,C++项目中使用ffmpeg提供的C接口,include头文件,要放在extern "C"范围内:

// in C++ project
extern "C" {
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
}

这样在link时,编译器才能找到这些外部符号。

C代码如何调用C++写的库

很多C++写的库,也会提供C风格的接口,这样C和C++的项目,都可以调用这些库。用C++编写的库,但提供C风格接口,方法依然是使用extern "C"申明一组内部实现的接口。很多用C++编写的库,对外提供C接口,就是用这个技巧。

C风格的ABI接口,成了连接一切的桥梁...

本文链接:https://cs.pynote.net/sf/c/cpp/202111263/

-- EOF --

-- MORE --