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

150

我们为什么需要使用:

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

具体来说:

  • 我们应该在什么情况下使用它?

  • 编译器/链接器层面上发生了什么需要我们使用它?

  • 从编译/链接的角度来看,这如何解决需要使用它的问题?

11个回答

140

C和C++在表面上看起来很相似,但它们编译成的代码集合却大为不同。当你使用C++编译器包含头文件时,编译器期望的是C++代码。然而,如果这是一个C头文件,则编译器期望头文件中的数据被编译成特定格式的C++ 'ABI'或'应用程序二进制接口',因此链接器会出现问题。这比将C++数据传递给需要C数据的函数更可取。

(要深入了解细节,C++的ABI通常会对其函数/方法的名称进行"名称修饰",因此如果没有将原型标记为C函数就调用printf(),C++实际上会生成调用_Zprintf及其后面的一些额外东西的代码。)

所以:在包含C头文件时,请使用extern "C"{...},就这么简单。否则,您的编译代码将不匹配,并且链接器会出问题。对于大多数头文件,您甚至不需要extern,因为大多数系统C头文件已经考虑到它们可能被C++代码包含,并已经extern "C"了它们的代码。


2
你能详细解释一下“大多数系统的C头文件已经考虑到可能被C++代码包含并且已经使用extern关键字声明了它们的代码”吗? - anon
11
@BulatM. 它们包含类似于以下内容:#ifdef __cplusplus extern "C" { #endif因此,当从C++文件中引用时,它们仍然被视为C头文件。 - Calmarius

116

extern "C" 用于确定生成的目标文件中符号的命名方式。如果一个函数没有使用 extern "C" 进行声明,那么在目标文件中的符号名称将会使用 C++ 的名字重载机制(name mangling)。下面是一个例子:

给定如下 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头文件是旧的并且没有这些保护,则只需要在其周围放置extern "C" {}。


24

在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”的多态函数都需要具有不同的符号,否则其他对象文件将使用错误的函数。这通过对符号名称进行“mangling”(这是一个技术术语)来完成。

当将C ++代码链接到C库或代码时,您需要extern "C"任何写入C的内容,例如C库的头文件,以告诉C ++编译器这些符号名称不应进行名称混淆,而您的其余C ++代码当然必须进行名称混淆,否则它将无法工作。


11

什么时候应该使用它?

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

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

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

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

使用C命名方案允许您引用C风格的符号。否则,链接器会尝试使用C++风格的符号,这不起作用。


7

C++编译器和C编译器创建符号名称的方式不同。因此,如果您尝试调用存储在以C代码编译的C文件中的函数,则需要告诉C++编译器它正在尝试解析的符号名称看起来与默认值不同;否则,链接步骤将失败。


7
每当您在C++文件中使用由C编译器编译的文件定义的函数时,都应该使用extern "C"。如果许多标准C库在其头文件中包含此检查以使开发人员更加简便。

例如,如果您有一个项目包含3个文件util.c、util.h和main.cpp,并且.c和.cpp文件都使用C++编译器(g ++、cc等)进行编译,则实际上不需要使用它,甚至可能会导致链接器错误。如果构建过程使用常规C编译器进行util.c,则在包括util.h时需要使用extern "C"。

发生的情况是C++在函数名中编码了函数的参数。这就是函数重载的工作原理。对于C函数来说,通常只会在名称前面添加下划线(“_”)。如果不使用extern "C",则链接器将寻找名为DoSomething@@int@float()的函数,而函数的实际名称是_DoSomething()或DoSomething()。

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

6
extern "C" {} 结构指示编译器不对大括号内声明的名称进行名称重整。通常,C++编译器会“增强”函数名称,以便它们编码关于参数和返回值的类型信息;这称为“名称重整”。extern "C" 结构可以防止名称重整。
当C++代码需要调用C语言库时,通常会使用它。在向C客户端公开C++函数(例如从DLL中)时也可以使用它。

5

这是用于解决名称混淆问题的。extern C 意味着函数在“平面”的 C 风格 API 中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接