关于链接:为什么我们需要extern“C”{#include< foo.h>

Why do we need extern “C”{ #include <foo.h> } in C++?

我们为什么需要使用:

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

明确地:

  • 我们什么时候用?

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

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


C和C++表面上相似,但每个编译成一组非常不同的代码。当用C++编译器包含头文件时,编译器期望C++代码。但是,如果它是一个C报头,那么编译器就希望将头文件中包含的数据编译成某种格式的c++‘abi’,或者"应用二进制接口",这样链接器就哽住了。这最好是将C++数据传递给期望C数据的函数。

(为了进入真正的本质,C++的ABI通常会对它们的函数/方法的名称进行格式化,因此调用EDCOX1×0"而不将原型标记为C函数,C++实际上会生成调用EDCOX1"1"的代码,最后加上额外的废话。

所以:使用extern"C" {...}当包含一个c头时,就这么简单了。否则,编译后的代码将不匹配,链接器将阻塞。然而,对于大多数报头,您甚至不需要EDCOX1 OR 3 },因为大多数系统C报头将已经解释了它们可能被C++代码和已经EDCOX1 3代码所包含的事实。


外部"c"决定如何命名生成的对象文件中的符号。如果一个函数被声明为没有外部的"C",则对象文件中的符号名称将使用C++名称修改。这是一个例子。

给定的test.c类似于:

1
void foo() { }

编译和列出对象文件中的符号可以得到:

1
2
3
4
$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
                 U __gxx_personality_v0

foo函数实际上被称为"z3foov"。此字符串包含返回类型和参数的类型信息等。如果您改为这样编写test.c:

1
2
3
extern"C" {
    void foo() { }
}

然后编译并查看符号:

1
2
3
4
$ g++ -c test.C
$ nm test.o
                 U __gxx_personality_v0
0000000000000000 T foo

你得到C连杆。对象文件中"foo"函数的名称只是"foo",它没有所有来自名称管理的奇特类型信息。

通常,在外部"C"{}中包含一个头,如果与它一起使用的代码是用C编译器编译的,但您试图从C++调用它。当您这样做时,您将告诉编译器头中的所有声明都将使用C链接。当您链接代码时,.o文件将包含对"foo"的引用,而不是"z3fooblah",希望与您链接的库中的任何内容相匹配。

大多数现代图书馆都会在这些标题周围设置保护装置,以便用正确的链接来声明符号。例如,在许多标准标题中,您会发现:

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern"C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

这确保了当C++代码包含标题时,目标文件中的符号与C库中的符号匹配。你只需要把外部的"c"放在你的c头上,如果它是旧的,并且没有这些守卫。


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

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

为了区分它们,C++编译器将在名字命名或修饰的过程中为每个名称创建唯一的名称。C编译器不会这样做。此外,每个C++编译器都可以用不同的方式来实现这一点。

ExtEnter"C"告诉C++编译器不要在括号内的代码上执行任何名称修改。这允许您从C++内部调用C函数。


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

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


When should we use it?

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

What is happening at the
compiler/linker level that requires us
to use it?

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

How in terms of compilation/linking
does this solve the problems which
require us to use it?

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


C和C++对符号的名称有不同的规则。符号是链接器如何知道在编译器生成的一个对象文件中对函数"openbankaccount"的调用是对您在同一(或兼容)编译器从不同源文件生成的另一个对象文件中称为"openbankaccount"的函数的引用。这允许您从多个源文件中生成一个程序,这在处理大型项目时是一种解脱。

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

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

但是在C++中,语言允许你拥有命名空间,以及多态性和与这样一个简单规则相冲突的各种其他事物。所有六个名为"add"的多态函数都需要有不同的符号,否则其他对象文件将使用错误的符号。这是通过"破坏"(这是一个技术术语)符号的名称来实现的。

当将C++代码链接到C库或代码时,需要用C语言编写的"外部""C",例如C库的头文件,告诉C++编译器这些符号名称不会被篡改,当然C++代码的其余部分必须被篡改,否则将无法工作。


您应该使用ExtEnter"C",当您包含定义在C编译器编译的文件中的函数的头时,该文件在C++文件中使用。(许多标准C库可能在其头中包含此检查,以使开发人员更简单)

例如,如果您有一个带有3个文件的项目,UTI.C、UTI.H和MIN .CPP以及.C和.CPP文件都是用C++编译器(G++、CC等)编译的,那么它就不需要了,甚至可能导致链接器错误。如果您的构建过程使用了一个针对util.c的常规C编译器,那么在包含util.h时,您将需要使用extern"c"。

正在发生的是C++以其名称对函数的参数进行编码。这就是函数重载的工作原理。对于C函数,只需在名称的开头添加一个下划线("uuu")。如果不使用extern"c",链接器将在函数的实际名称为"dosomething()或dosomething()时查找名为dosomething@@int@float()的函数。

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


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

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


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


这用于解决名称管理问题。extern c意味着函数位于"平面"C风格的API中。