为什么 C++ 中需要 extern “C”{ #include <foo.h> }?
-
09-06-2019 - |
题
为什么我们需要使用:
extern "C" {
#include <foo.h>
}
具体来说:
我们什么时候应该使用它?
编译器/链接器级别发生了什么需要我们使用它?
在编译/链接方面,这如何解决需要我们使用它的问题?
解决方案
C 和 C++ 表面上很相似,但各自编译成一组非常不同的代码。当您使用 C++ 编译器包含头文件时,编译器需要 C++ 代码。然而,如果它是一个 C 头文件,那么编译器期望头文件中包含的数据被编译为某种格式——C++“ABI”或“应用程序二进制接口”,因此链接器会阻塞。这比将 C++ 数据传递给需要 C 数据的函数更好。
(为了深入了解本质,C++ 的 ABI 通常会“破坏”其函数/方法的名称,因此调用 printf()
如果不将原型标记为 C 函数,C++ 将实际生成调用的代码 _Zprintf
, ,最后加上额外的废话。)
所以:使用 extern "C" {...}
当包含一个 c 头文件时——就这么简单。否则,编译的代码将不匹配,并且链接器将被阻塞。然而,对于大多数标头,您甚至不需要 extern
因为大多数系统 C 头文件已经考虑到它们可能包含在 C++ 代码中并且已经 extern
他们的代码。
其他提示
extern "C" 确定生成的目标文件中的符号应如何命名。如果函数声明时没有使用 extern "C",则目标文件中的符号名称将使用 C++ 名称修饰。这是一个例子。
给定 test.C 像这样:
void foo() { }
编译并列出目标文件中的符号给出:
$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
U __gxx_personality_v0
foo 函数实际上称为“_Z3foov”。该字符串包含返回类型和参数等的类型信息。如果您像这样编写 test.C:
extern "C" {
void foo() { }
}
然后编译并查看符号:
$ g++ -c test.C
$ nm test.o
U __gxx_personality_v0
0000000000000000 T foo
你得到了 C 链接。目标文件中“foo”函数的名称只是“foo”,并且它没有来自名称修改的所有花哨类型信息。
如果附带的代码是用 C 编译器编译的,但您尝试从 C++ 调用它,则通常会在 extern "C" {} 中包含标头。当您执行此操作时,您是在告诉编译器标头中的所有声明都将使用 C 链接。当您链接代码时,您的 .o 文件将包含对“foo”的引用,而不是“_Z3fooblah”,它希望与您链接的库中的任何内容相匹配。
大多数现代库都会在此类标头周围放置防护装置,以便使用正确的链接声明符号。例如在许多标准标头中,您会发现:
#ifdef __cplusplus
extern "C" {
#endif
... declarations ...
#ifdef __cplusplus
}
#endif
这可以确保当 C++ 代码包含标头时,目标文件中的符号与 C 库中的符号相匹配。如果 C 标头是旧的并且还没有这些防护,则只需在 C 标头周围放置 extern "C" {} 即可。
在 C++ 中,您可以拥有共享名称的不同实体。例如,这里是所有命名的函数列表 富:
A::foo()
B::foo()
C::foo(int)
C::foo(std::string)
为了区分它们,C++ 编译器将在称为名称修饰或装饰的过程中为每个名称创建唯一的名称。C 编译器不这样做。此外,每个 C++ 编译器可能会以不同的方式执行此操作。
extern "C" 告诉 C++ 编译器不要对大括号内的代码执行任何名称修改。这允许您从 C++ 中调用 C 函数。
它与不同编译器执行名称修改的方式有关。C++ 编译器将以与 C 编译器完全不同的方式破坏从头文件导出的符号名称,因此当您尝试链接时,您会收到链接器错误,指出缺少符号。
为了解决这个问题,我们告诉 C++ 编译器以“C”模式运行,因此它以与 C 编译器相同的方式执行名称修改。这样做之后,链接器错误就被修复了。
我们什么时候应该使用它?
当您将 C 库链接到 C++ 目标文件时
需要我们使用它的编译器/链接器级别发生了什么?
C 和 C++ 使用不同的符号命名方案。这告诉链接器在给定库中链接时使用 C 的方案。
在汇编/链接方面,这如何解决需要我们使用它的问题?
使用 C 命名方案允许您引用 C 样式符号。否则,链接器将尝试 C++ 样式的符号,但这是行不通的。
C 和 C++ 对于符号名称有不同的规则。符号是链接器如何知道编译器生成的一个目标文件中对函数“openBankAccount”的调用是对由相同(或兼容)的不同源文件生成的另一个目标文件中称为“openBankAccount”的函数的引用编译器。这允许您从多个源文件中创建一个程序,这在处理大型项目时是一种解脱。
在 C 中,规则非常简单,无论如何,符号都在一个名称空间中。因此,整数“socks”存储为“socks”,函数 count_socks 存储为“count_socks”。
链接器是使用这个简单的符号命名规则为 C 和其他语言(如 C)构建的。所以链接器中的符号只是简单的字符串。
但在 C++ 中,该语言允许您拥有命名空间、多态性以及与如此简单的规则相冲突的各种其他内容。称为“add”的所有六个多态函数都需要具有不同的符号,否则其他目标文件将使用错误的符号。这是通过“修改”(这是一个技术术语)符号名称来完成的。
将 C++ 代码链接到 C 库或代码时,您需要 extern "C" 任何用 C 编写的内容(例如 C 库的头文件),以告诉 C++ 编译器这些符号名称不会被破坏,而其余的符号名称将被破坏。当然,你的 C++ 代码必须被破坏,否则它将无法工作。
只要包含定义位于由 C 编译器编译的文件中的函数的标头(在 C++ 文件中使用),就应该使用 extern "C"。(许多标准 C 库可能会在其标头中包含此检查,以使开发人员更简单)
例如,如果您有一个包含 3 个文件的项目,util.c、util.h 和 main.cpp,并且 .c 和 .cpp 文件都是使用 C++ 编译器(g++、cc 等)编译的,那么它就不是并不是真正需要的,甚至可能导致链接器错误。如果您的构建过程对 util.c 使用常规 C 编译器,则在包含 util.h 时需要使用 extern "C"。
发生的情况是 C++ 将函数的参数编码在其名称中。这就是函数重载的工作原理。C 函数通常会在名称开头添加下划线(“_”)。如果不使用 extern "C",当函数的实际名称为 _DoSomething() 或只是 DoSomething() 时,链接器将查找名为 DoSomething@@int@float() 的函数。
使用 extern "C" 通过告诉 C++ 编译器它应该查找遵循 C 命名约定而不是 C++ 命名约定的函数来解决上述问题。
C++ 编译器创建符号名称的方式与 C 编译器不同。因此,如果您尝试调用驻留在 C 文件中并编译为 C 代码的函数,则需要告诉 C++ 编译器它尝试解析的符号名称与默认名称不同;否则链接步骤将失败。
这 extern "C" {}
构造指示编译器不要对大括号内声明的名称执行重整。通常,C++ 编译器“增强”函数名称,以便它们对有关参数和返回值的类型信息进行编码;这就是所谓的 损坏的名字. 。这 extern "C"
构造可以防止损坏。
它通常在 C++ 代码需要调用 C 语言库时使用。当向 C 客户端公开 C++ 函数(例如,来自 DLL)时,也可以使用它。
这用于解决名称修改问题。extern C 意味着这些函数位于“扁平”C 风格的 API 中。
反编译一个 g++
生成的二进制文件看看发生了什么
我将这个答案移至: C++ 中 extern "C" 的作用是什么? 因为这个问题被认为是这个问题的重复。
主程序
void f() {}
void g();
extern "C" {
void ef() {}
void eg();
}
/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }
使用 GCC 4.8 Linux 编译 极低频 输出:
g++ -c main.cpp
反编译符号表:
readelf -s main.o
输出包含:
Num: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 6 FUNC GLOBAL DEFAULT 1 _Z1fv
9: 0000000000000006 6 FUNC GLOBAL DEFAULT 1 ef
10: 000000000000000c 16 FUNC GLOBAL DEFAULT 1 _Z1hv
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z1gv
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND eg
解释
我们看到:
ef
和eg
存储在与代码同名的符号中其他符号都被破坏了。让我们来解开它们:
$ c++filt _Z1fv f() $ c++filt _Z1hv h() $ c++filt _Z1gv g()
结论:以下两种符号类型都是 不是 损坏:
- 定义的
- 已声明但未定义 (
Ndx = UND
),在链接或运行时从另一个目标文件提供
所以你需要 extern "C"
两者都在调用时:
- 来自 C++ 的 C:告诉
g++
期望由以下方式生成未损坏的符号gcc
- 来自 C 的 C++:告诉
g++
生成未损坏的符号gcc
使用
在 extern C 中不起作用的东西
很明显,任何需要名称修改的 C++ 功能都无法在内部工作 extern C
:
extern "C" {
// Overloading.
// error: declaration of C function ‘void f(int)’ conflicts with
void f();
void f(int i);
// Templates.
// error: template with C linkage
template <class C> void f(C i) { }
}
来自 C++ 示例的最小可运行 C
为了完整起见以及对于新手来说,另请参阅: 如何在C++项目中使用C源文件?
从 C++ 调用 C 非常简单:每个 C 函数只有一个可能的非重整符号,因此不需要额外的工作。
主程序
#include <cassert>
#include "c.h"
int main() {
assert(f() == 1);
}
ch.h
#ifndef C_H
#define C_H
/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif
#endif
抄送
#include "c.h"
int f(void) { return 1; }
跑步:
g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out
没有 extern "C"
链接失败并显示:
main.cpp:6: undefined reference to `f()'
因为 g++
期望找到一个破损的 f
, , 哪个 gcc
没有产生。
来自 C 示例的最小可运行 C++
从以下位置调用 C++ 有点困难:我们必须手动创建我们想要公开的每个函数的未损坏版本。
这里我们说明如何向 C 公开 C++ 函数重载。
主程序
#include <assert.h>
#include "cpp.h"
int main(void) {
assert(f_int(1) == 2);
assert(f_float(1.0) == 3);
return 0;
}
程序文件h
#ifndef CPP_H
#define CPP_H
#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif
#endif
程序文件
#include "cpp.h"
int f(int i) {
return i + 1;
}
int f(float i) {
return i + 2;
}
int f_int(int i) {
return f(i);
}
int f_float(float i) {
return f(i);
}
跑步:
gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out
没有 extern "C"
它失败了:
main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'
因为 g++
生成损坏的符号 gcc
找不到。
在 Ubuntu 18.04 中测试。