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


136

我们为什么需要使用:

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

特别:

  • 我们什么时候应该使用它?

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

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

Answers:


122

C和C ++在表面上是相似的,但是每个都编译成非常不同的代码集。在C ++编译器中包含头文件时,编译器需要C ++代码。但是,如果它是C头,则编译器希望将头文件中包含的数据编译为某种格式-C ++“ ABI”或“应用程序二进制接口”,因此链接器将阻塞。这比将C ++数据传递给期望C数据的函数更好。

(要想真正了解细节,C ++的ABI通常会``弄混''它们的函数/方法的名称,因此在调用时printf()不会将原型标记为C函数,C ++实际上会生成代码调用_Zprintf,最后加上额外的废话。 )

因此:extern "C" {...}包括ac标头时使用-就这么简单。否则,编译后的代码将不匹配,链接器将阻塞。但是,对于大多数头文件,您甚至都不需要,extern因为大多数系统C头文件已经说明了C ++代码可能已经包含了它们,并且已经包含了extern它们的代码。


1
您能否详细说明“大多数系统C头文件已经考虑到它们可能已包含在C ++代码中并已经终止其代码的事实”。
Bulat M.

7
@BulatM。它们包含以下内容: #ifdef __cplusplus extern "C" { #endif 因此,当从C ++文件中包含它们时,它们仍被视为C标头。
Calmarius

111

extern“ C”确定应如何命名生成的目标文件中的符号。如果声明的函数不带外部“ 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编译器编译的,则通常在extern“ C” {}中包含标头,但是您尝试从C ++调用它。当您这样做时,您是在告诉编译器头文件中的所有声明都将使用C链接。当您链接代码时,您的.o文件将包含对“ foo”的引用,而不是对“ _Z3fooblah”的引用,这有望与您要链接的库中的内容匹配。

大多数现代库都会在此类标头周围加保护,以便使用正确的链接声明符号。例如,在许多标准标题中,您会发现:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

这样可以确保当C ++代码包含标头时,目标文件中的符号与C库中的符号匹配。如果C标头很旧并且还没有这些保护,则只需要在ex标头周围加上extern“ C” {}即可。


22

在C ++中,可以有不同的实体共享一个名称。例如,下面是所有名为foo的函数的列表:

  • A::foo()
  • B::foo()
  • C::foo(int)
  • C::foo(std::string)

为了区分它们,C ++编译器将在一个称为名称修饰或修饰的过程中为每个名称创建唯一的名称。C编译器不这样做。此外,每个C ++编译器可能以不同的方式执行此操作。

extern“ C”告诉C ++编译器不要对花括号内的代码执行任何名称处理。这使您可以从C ++中调用C函数。


14

它与不同的编译器执行名称处理的方式有关。C ++编译器将以与C编译器完全不同的方式来处理从头文件导出的符号的名称,因此,当您尝试链接时,会出现链接器错误,指出缺少符号。

为了解决这个问题,我们告诉C ++编译器以“ C”模式运行,因此它以与C编译器相同的方式执行名称处理。这样做后,链接器错误已修复。


11

C和C ++关于符号名称的规则不同。符号是链接器如何知道对由编译器生成的一个目标文件中的函数“ openBankAccount”的调用是对在相同(或兼容)不同源文件中生成的另一目标文件中称为“ openBankAccount”的函数的引用编译器。这使您可以使用多个源文件制作一个程序,这在处理大型项目时很轻松。

在C语言中,规则非常简单,符号始终都在单个名称空间中。因此,整数“ socks”存储为“ socks”,而函数count_socks存储为“ count_socks”。

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

但是在C ++中,该语言允许您拥有名称空间,多态性以及与这种简单规则相冲突的其他各种功能。您称为“ add”的所有六个多态函数都必须具有不同的符号,否则其他目标文件将使用错误的符号。这是通过“破坏”(这是一个技术术语)符号名称来完成的。

将C ++代码链接到C库或代码时,您需要用C编写的任何东西都加“ C”,例如C库的头文件,以告诉您的C ++编译器这些符号名将不被篡改,而其余的当然,您的C ++代码必须经过修改,否则将无法正常工作。


11

我们什么时候应该使用它?

将C库链接到C ++目标文件时

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

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

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

使用C命名方案可以引用C样式符号。否则,链接器将尝试不起作用的C ++风格的符号。


7

每当您在包含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函数易于发生的事情是在名称的开头加上下划线(_)。如果函数的实际名称是_DoSomething()或仅是DoSomething(),则链接器将在不使用外部“ C”的情况下查找名为DoSomething @@ int @ float()的函数。

使用extern“ C”通过告诉C ++编译器它应寻找遵循C命名约定而不是C ++命名的函数来解决上述问题。


7

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


6

extern "C" {}构造指示编译器不要对花括号内声明的名称进行修饰。通常,C ++编译器会“增强”函数名称,以便它们对有关参数和返回值的类型信息进行编码。这被称为损坏的名称。该extern "C"构造可防止变形。

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



0

反编译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 ELF输出:

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()

结论:以下两种符号类型均未损坏:

  • 定义的
  • 已声明但未定义(Ndx = UND),将在链接或运行时从另一个目标文件提供

因此,extern "C"在调用时,您将同时需要两者:

  • C ++中的C:告诉g++您期望由产生的未破坏符号gcc
  • C语言中的C ++:告诉g++生成未损坏的符号以gcc供使用

在外部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函数只有一个可能的非混合符号,因此不需要额外的工作。

main.cpp

#include <cassert>

#include "c.h"

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

ch

#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。

main.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中测试。


1
感谢您解释下降投票,现在一切都有意义。
西罗Santilli郝海东冠状病六四事件法轮功
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.