为什么在 C + + 中需要外部“ C”{ # include < foo.h > } ?

为什么我们需要使用:

extern "C" {
#include <foo.h>
}

具体来说:

  • 我们什么时候用?

  • 在编译器/链接器级别发生了什么需要我们使用它?

  • 在编译/链接方面,这如何解决需要我们使用它的问题?

92410 次浏览

这与不同编译器执行名称混淆的方式有关。C + + 编译器会以一种与 C 编译器完全不同的方式破坏从头文件导出的符号的名称,所以当你尝试链接时,你会得到一个链接器错误,说有缺少的符号。

为了解决这个问题,我们告诉 C + + 编译器在“ C”模式下运行,因此它执行名称篡改的方式与 C 编译器相同。这样做之后,连接器错误就被修复了。

这用于解决名称混淆问题。ExternC 意味着这些函数是在一个“扁平的”C 样式的 API 中。

C + + 编译器创建的符号名与 C 编译器不同。因此,如果你试图调用一个驻留在 C 文件中的函数,编译成 C 代码,你需要告诉 C + + 编译器,它试图解析的符号名称看起来与默认值不同; 否则链接步骤将失败。

C 和 C + + 表面上是相似的,但是每个都编译成一组非常不同的代码。当使用 C + + 编译器包含头文件时,编译器期望使用 C + + 代码。然而,如果它是一个 C 头文件,那么编译器期望头文件中包含的数据被编译成某种格式ーー C + + “ ABI”,或者“应用二进制接口”,所以链接器会卡壳。这比将 C + + 数据传递给需要 C 数据的函数要好。

(为了深入了解真正的细节,C + + 的 ABI 通常会“混淆”它们的函数/方法的名称,所以调用 printf()时不会将原型标记为 C 函数,C + + 实际上会生成调用 _Zprintf的代码,并在最后添加额外的废话。)

因此: 在包含 c 头时使用 extern "C" {...}ーー就是这么简单。否则,编译后的代码就会不匹配,链接器就会卡住。然而,对于大多数头文件,您甚至不需要 extern,因为大多数系统 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 + + 对于符号名称有不同的规则。符号是连接器如何知道对编译器生成的一个对象文件中的函数“ openBankAccount”的调用是对由同一(或兼容)编译器从不同源文件生成的另一个对象文件中的函数“ openBankAccount”的引用。这允许您使用多个源文件编写程序,这在处理大型项目时是一种解脱。

在 C 语言中,规则非常简单,符号都在同一个名称空间中。因此,整数“ ocks”被存储为“ ocks”,而 count _ ocks 函数被存储为“ count _ ocks”。

使用这个简单的符号命名规则,为 C 和其他语言(如 C)构建了链接器。所以链接器中的符号只是简单的字符串。

但是在 C + + 语言中,您可以使用名称空间、多态性和其他各种与这样一个简单规则相冲突的东西。称为“ add”的所有六个多态函数都需要有不同的符号,否则其他对象文件将使用错误的符号。这是通过“破坏”(这是一个技术术语)的名称的符号。

当将 C + + 代码链接到 C 库或代码时,你需要外部“ C”任何用 C 编写的东西,比如 C 库的头文件,来告诉你的 C + + 编译器这些符号名不能被损坏,而你的 C + + 代码的其余部分当然必须被损坏,否则它不会工作。

我们什么时候用?

当您将 C 库链接到 C + + 对象文件时

发生了什么 需要我们的编译器/链接器级别 来使用它?

C 和 C + + 使用不同的符号命名方案。这告诉链接器在给定库中链接时使用 C 的方案。

在编译/链接方面如何 这能解决问题吗 要求我们使用它?

使用 C 命名方案允许您引用 C 样式的符号。否则链接器将尝试 C + + 风格的符号,这将无法工作。

extern "C" {}构造指示编译器不要对大括号中声明的名称执行错误处理。通常,C + + 编译器“增强”函数名,以便它们编码有关参数和返回值的类型信息; 这称为 名字弄错了extern "C"结构可以防止损坏。

它通常在 C + + 代码需要调用 C 语言库时使用。当向 C 客户机公开 C + + 函数时(例如,从 DLL) ,也可以使用它。

Extern“ C”确定生成的对象文件中的符号应该如何命名。如果函数的声明没有外部“ C”,那么对象文件中的符号名将使用 C + + 名称处理。举个例子。

给定的测试 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 + + 调用它,那么通常会在 exter“ C”{}中包含一个头。当您这样做时,您告诉编译器头中的所有声明都将使用 C 链接。当您链接代码时,您的。O 文件将包含对“ foo”的引用,而不包含对“ _ Z3foobra”的引用,这些引用希望能够匹配您链接到的库中的任何内容。

大多数现代图书馆会在这样的标题周围设置警卫,以便用正确的链接声明符号。例如,在许多标准标题中,你会发现:

#ifdef __cplusplus
extern "C" {
#endif


... declarations ...


#ifdef __cplusplus
}
#endif

这样可以确保当 C + + 代码包含头部时,目标文件中的符号与 C 库中的符号相匹配。如果 C 头很旧,而且还没有这些保护,那么只需要在它周围放上额外的“ C”{}。

在 C + + 文件中使用的 C 编译器编译的文件中包含定义函数的头文件时,应该使用 exter“ C”。(许多标准 C 库可能在其头部包含这个检查,以使开发人员更加简单)

例如,如果您有一个包含3个文件 util.c、 util.h 和 main.cpp 的项目,并且。C 和。Cpp 文件是用 C + + 编译器(g + + ,cc 等)编译的,那么它就不是真正需要的,甚至可能导致链接器错误。如果构建过程对 util.c 使用常规的 C 编译器,那么在包含 util.h 时需要使用外部“ C”。

正在发生的是,C + + 将函数的参数编码在它的名称中。这就是函数重载。对于 C 函数来说,所有可能发生的事情就是在名称的开头加上一个下划线(“ _”)。如果不使用外部“ C”,当函数的实际名称是 _ DoSomething ()或者只是 DoSomething ()时,链接器将寻找一个名为 DoSomething@@ int@float ()的函数。

使用 extern“ C”解决了上面的问题,它告诉 C + + 编译器应该寻找一个遵循 C 变数命名原则的函数,而不是遵循 c + + 的函数。

反编译一个 g++生成的二进制文件,看看发生了什么

要理解为什么 extern是必要的,最好的方法是通过一个示例来详细了解目标文件中发生了什么:

Main.cpp

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

解释

我们看到:

  • efeg存储在与代码相同名称的符号中

  • 其他的符号都被破坏了,我们来解开它们:

    $ c++filt _Z1fv
    f()
    $ c++filt _Z1hv
    h()
    $ c++filt _Z1gv
    g()
    

Conclusion: both of the following symbol types were not mangled:

  • defined
  • declared but undefined (Ndx = UND), to be provided at link or run time from another object file

So you will need extern "C" both when calling:

  • C from C++: tell g++ to expect unmangled symbols produced by gcc
  • C++ from C: tell g++ to generate unmangled symbols for gcc to use

Things that do not work in extern C

It becomes obvious that any C++ feature that requires name mangling will not work inside 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 函数只有一个可能的非损坏符号,因此不需要额外的工作。

Main.cpp

#include <cassert>


#include "c.h"


int main() {
assert(f() == 1);
}

C.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并没有产生。

GitHub 上的示例。

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;
}

Cpp.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

Cpp.cpp

#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无法找到的混乱的符号。

GitHub 上的示例。

在 Ubuntu 18.04中测试。