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"的代码,最后加上额外的废话。
所以:使用
外部"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++的函数来解决上述问题。
它通常在C++代码需要调用C语言库时使用。当将C++函数(例如,从DLL)暴露到C客户端时,也可以使用它。
C++编译器与C编译器不同地创建符号名称。因此,如果您试图调用一个驻留在C文件中的函数,编译为C代码,则需要告诉C++编译器,它试图解析的符号名称看起来与其默认值不同,否则链接步骤将失败。
这用于解决名称管理问题。extern c意味着函数位于"平面"C风格的API中。