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

为什么我们需要使用:

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

特别:

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

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

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

C和C ++在表面上是相似的,但是每个编译成一组非常不同的代码。 当您使用C ++编译器包含头文件时,编译器期待C ++代码。 但是,如果它是一个C头文件,那么编译器期望包含在头文件中的数据被编译成某种格式 – C ++“ABI”或“应用程序二进制接口”,这样链接器就会窒息起来。 这比将C ++数据传递给期望C数据的函数更可取。

(为了进入真正的本质,C ++的ABI通常会“破坏”它们的函数/方法的名字,所以调用printf()而不把原型标记为C函数,C ++实际上会生成调用_Zprintf代码,再加上额外的废话最后。)

所以:使用extern "C" {...}; 当包括交stream标题 – 这是很简单的。 否则,在编译的代码中会出现不匹配,链接器将会窒息。 然而,对于大多数头文件,甚至不需要extern因为大多数系统C头文件已经说明了它们可能被C ++代码包含并且已经存在于其代码中。

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”。 该string包含返回types和参数的types信息等等。 如果你写这样的test.C:

 extern "C" { void foo() { } } 

然后编译并查看符号:

 $ g++ -c test.C $ nm test.o U __gxx_personality_v0 0000000000000000 T foo 

你得到C连接。 对象文件中的“foo”函数的名称只是“foo”,并没有来自名称修饰的所有奇特types信息。

如果与它一起编译的代码是用C编译器编译的,但是您试图从C ++调用它,那么通常在extern“C”{}中包含一个头文件。 当你这样做的时候,你要告诉编译器,头文件中的所有声明都将使用C链接。 当你链接你的代码时,你的.o文件将包含对“foo”的引用,而不是“_Z3fooblah”,它有望匹配你连接的库中的任何东西。

大多数现代图书馆都会把这些标题放在旁边,这样符号就可以通过正确的链接进行声明。 比如在很多标准的头文件中你会发现:

 #ifdef __cplusplus extern "C" { #endif ... declarations ... #ifdef __cplusplus } #endif 

这确保了当C ++代码包含头文件时,目标文件中的符号与C库中的符号相匹配。 你只需要在你的C头部周围放置extern“C”{},如果它已经老了,并且已经没有这些防护。

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

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

为了区分它们,C ++编译器将在名为mangling或decorating的进程中为每个名称创build唯一名称。 C编译器不这样做。 而且,每个C ++编译器都可以这样做。

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

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

为了解决这个问题,我们告诉C ++编译器以“C”模式运行,所以它会像C编译器一样执行名称修改。 这样做,链接器错误是固定的。

C和C ++对符号名称有不同的规则。 符号是链接器如何知道在编译器生成的一个目标文件中调用函数“openBankAccount”是对另一个由不同源文件生成的另一个目标文件中的“openBankAccount”函数的引用,编译器。 这使您可以从一个以上的源文件中创build一个程序,这对于处理大型项目来说是一种解脱。

在C中,规则非常简单,无论如何,符号都在一个单一的名字空间中。 所以整数“socks”存储为“socks”,函数count_socks存储为“count_socks”。

这个简单的符号命名规则为C和C等其他语言构build了链接器。 所以链接器中的符号只是简单的string。

但是在C ++中,这种语言可以让你拥有命名空间,多态以及与这样一个简单规则相冲突的其他各种事物。 所有称为“add”的六个多态函数都需要具有不同的符号,否则错误的函数将被其他目标文件使用。 这是通过“捣毁”(这是一个技术术语)符号的名字来完成的。

将C ++代码链接到C库或代码时,需要使用C语言编写的任何东西(如C库的头文件),以告诉C ++编译器这些符号名称不会被破坏,而其余的当然你的C ++代码必须被破坏,否则将无法工作。

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

将C库编译为C ++对象文件时

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

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

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

使用C命名scheme可以引用C风格的符号。 否则,链接器会尝试C ++风格的符号,这将无法正常工作。

您应该在任何时候使用extern“C”来定义驻留在由C编译器编译的文件中的函数,这些函数在C ++文件中使用。 (许多标准的C库可能会在头文件中包含这个检查,使开发人员更简单)

例如,如果您有一个包含3个文件的项目,util.c,util.h和main.cpp以及.c和.cpp文件都使用C ++编译器(g ++,cc等)进行编译,那么它不是'真的需要,甚至可能导致链接器错误。 如果您的构build过程对util.c使用常规C编译器,那么在包含util.h时将需要使用extern“C”。

发生什么事是C ++在其名称中编码函数的参数。 这是函数重载的工作原理。 所有倾向于在C函数中发生的事情是在名称的开头添加一个下划线(“_”)。 如果不使用extern“C”,当函数的实际名称是_DoSomething()或DoSomething()时,链接器将查找名为DoSomething @@ int @ float()的函数。

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

C ++编译器创build的符号名称与C编译器不同。 因此,如果您要调用驻留在C文件中的函数(编译为C代码),则需要告诉C ++编译器它正在尝试parsing的符号名称与默认的符号名称不同; 否则链接步骤将失败。

这用于解决名称修改问题。 extern C意味着这些函数是在一个“扁平”的C风格的API。

extern "C" {}结构指示编译器不要对花括号中声明的名称执行修改。 通常,C ++编译器会“增强”函数名称,以便对参数和返回值的types信息进行编码; 这被称为损坏的名字extern "C"构造防止了损坏。

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