extern "C"
需要预先了解:
- extern、static与inline关键字总结
- 函数
1. extern “C” 的使用
extern “C” 的作用是告诉 C++ 编译器:这个名字(函数或变量)按 C 语言的规则处理链接。它解决的是 C++ 程序调用 C 语言库的问题。
1.1 为什么需要 extern “C”
现实中有大量优秀的 C 库——OpenGL、SDL、zlib、sqlite 等。C++ 程序直接 #include 它们的头文件并调用函数时,链接阶段往往会失败,报出 undefined reference 错误。原因在于:C 编译器和 C++ 编译器给同一个函数生成的符号名不一样(详见本篇后半部分)。
extern "C" 就是给 C++ 编译器的一条指令:“这个函数的符号,按 C 的方式生成。”
1.2 基本用法
1. 单个声明:
extern "C" int add(int a, int b);
2. 代码块(多个声明一次搞定):
extern "C"
{
int add(int a, int b);
const char* get_version();
}
3. 带定义——函数体仍然是 C++ 代码,只是链接规则不同:
extern "C"
{
int add(int a, int b)
{
return a + b; // 函数体照常写 C++
}
}
4. 变量也可以:
extern "C" int errno; // 引用 C 库导出的全局变量
1.3 头文件双模式(最常用)
C 库的头文件通常写成"双模式",让 C 和 C++ 都能直接包含。它的关键是宏 __cplusplus——只有 C++ 编译器才定义这个宏,C 编译器不定义:
// mylib.h
#ifdef __cplusplus
extern "C"
{
#endif
int add(int a, int b);
const char* get_version();
#ifdef __cplusplus
}
#endif
C 编译器包含这个头文件时,extern "C" 部分被跳过,看到的是普通声明;C++ 编译器包含时,所有声明被包进 extern "C" 块,符号按 C 规则生成。这样同一个头文件对两种语言都成立。
1.4 注意事项
- extern “C” 函数不能重载——重载靠名称修饰区分,而 extern “C” 恰恰关闭了名称修饰,同名函数只能有一个:
extern "C" int f(int a) { return a; }
extern "C" double f(double a) { return a; }
编译这段代码,编译器直接报错:
error: conflicting declaration of C function 'double f(double)'
2 | extern "C" double f(double a) { return a; }
| ^
note: previous declaration 'int f(int)'
1 | extern "C" int f(int a) { return a; }
| ^
- 只影响链接,不影响语义——
extern "C"块里的函数体依然是 C++ 代码,可以用默认参数、函数重载之外的一切 C++ 特性:
extern "C" int add(int a, int b = 10); // 默认参数是 C++ 特性,仍然可用
- 语言链接是函数类型的一部分——
extern "C" void f(int)和void f(int)是不同的函数类型,赋值给函数指针时不能混用:
extern "C" void f(int);
void (*fp1)(int) = f; // 错误!类型不匹配(C 链接 vs C++ 链接)
extern "C" void (*fp2)(int) = f; // 正确
- 对类成员函数无效——标准规定:类成员名字的语言链接不受
extern "C"影响,所以它只用于自由函数和全局变量。
2. 底层原理:名称修饰
2.1 链接器如何找到函数
回顾extern、static与inline关键字总结中的内容:每个翻译单元编译后产生一个目标文件,里面有一张符号表(symbol table),登记了本文件定义的符号和引用的符号。链接器的工作就是把"引用"和"定义"对上。
所以"调用一个函数"在底层是:编译阶段生成对该函数符号的引用,链接阶段由链接器解析。符号名不匹配,链接就失败。
2.2 C 的名称修饰:几乎没有
C 语言不支持函数重载,函数名全局唯一,所以 C 编译器几乎不做修饰——符号名就是函数名本身。实测一个 C 文件:
int c_add(int a, int b) { return a + b; }
编译后用 nm 查看符号表:
0000000000000000 T c_add
符号就是 c_add,干干净净。(某些平台会加前缀,如 Windows x86 的 _c_add、macOS 的 _c_add,但规则简单且固定。)
2.3 C++ 的名称修饰:编码参数类型
C++ 支持函数重载:add(int, int) 和 add(double, double) 可以共存,链接器必须能区分它们。于是 C++ 编译器把函数名连同参数类型一起编码进符号,这个过程叫名称修饰(name mangling)。
GCC / Clang / MinGW 遵循 Itanium ABI 的修饰规则,格式是 _Z + 名字长度 + 名字 + 参数类型编码:
int add(int a, int b); // _Z3addii
double add(double a, double b); // _Z3adddd
_Z— 修饰前缀3add— 名字 “add”,长度 3i—int;d—double;f—float;v—void
用 nm 实测:
0000000000000000 T _Z3addii
0000000000000014 T _Z3adddd
用 c++filt 可以还原:
_Z3addii -> add(int, int)
_Z3adddd -> add(double, double)
成员函数和命名空间里的函数用更复杂的嵌套编码(_ZN 开头),如 _ZN4Base1fEv 是 Base::f(),_ZN12StaticHolder5countE 是 StaticHolder::count。
注:MSVC 用的是另一套完全不同的修饰规则(如
?add@@YAHHH@Z)。这也是为什么 C++ 的二进制库通常不跨编译器混用——同一个类,GCC 和 MSVC 生成的符号名不同,链接会失败。
2.4 extern “C” 做了什么
extern "C" 就是关闭名称修饰:声明的符号直接用原始名字,与 C 编译器生成的一致。实测:
extern "C" int c_add(int a, int b) { return a + b; }
符号表里是:
000000000000002e T c_add
与 C 版本完全相同——链接器自然就能把 C++ 的引用和 C 库的定义对上了。
2.5 一个真实的链接错误
把开头的问题完整走一遍。C 库导出 c_add,C++ 代码忘记加 extern "C":
// use.cpp — 忘记 extern "C"
extern int c_add(int a, int b);
int main() { return c_add(1, 2); }
编译后查看符号:
U _Z5c_addii // C++ 引用的是修饰后的名字
C 库目标文件里只有 c_add,链接器找不到 _Z5c_addii,报错:
use.o:use.cpp:(.text+0x18): undefined reference to `c_add(int, int)'
collect2.exe: error: ld returned 1 exit status
注意报错信息把 _Z5c_addii 还原成了 c_add(int, int)——这正是名称修饰存在的证据。改成 extern "C" int c_add(int a, int b); 后,引用符号变成 U c_add,与库符号一致,链接成功。
注:
undefined reference在 C++ 里还有一种常见来源——只声明了函数但忘记链接对应的库(如忘了-lxxx)。区分方法:如果报错名带(int, int)这类参数,说明是名称修饰层面的不匹配;如果修饰后的名字和你的声明一致,多半是库没链接上。


