我们为什么需要使用:
extern "C" {
#include <foo.h>
}
具体来说:
我们应该在什么情况下使用它?
编译器/链接器层面上发生了什么需要我们使用它?
从编译/链接的角度来看,这如何解决需要使用它的问题?
我们为什么需要使用:
extern "C" {
#include <foo.h>
}
具体来说:
我们应该在什么情况下使用它?
编译器/链接器层面上发生了什么需要我们使用它?
从编译/链接的角度来看,这如何解决需要使用它的问题?
C和C++在表面上看起来很相似,但它们编译成的代码集合却大为不同。当你使用C++编译器包含头文件时,编译器期望的是C++代码。然而,如果这是一个C头文件,则编译器期望头文件中的数据被编译成特定格式的C++ 'ABI'或'应用程序二进制接口',因此链接器会出现问题。这比将C++数据传递给需要C数据的函数更可取。
(要深入了解细节,C++的ABI通常会对其函数/方法的名称进行"名称修饰",因此如果没有将原型标记为C函数就调用printf()
,C++实际上会生成调用_Zprintf
及其后面的一些额外东西的代码。)
所以:在包含C头文件时,请使用extern "C"{...}
,就这么简单。否则,您的编译代码将不匹配,并且链接器会出问题。对于大多数头文件,您甚至不需要extern
,因为大多数系统C头文件已经考虑到它们可能被C++代码包含,并已经extern "C"
了它们的代码。
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
#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++编译器会对每个函数进行名称修饰和重载。而C编译器则不会这样做。此外,不同的C++编译器可能使用不同的方法进行名称修饰。
extern "C"
告诉C++编译器不要对花括号内的代码进行名称修饰,从而允许你从C++中调用C函数。
这与不同编译器执行名称重整的方式有关。C++编译器会以与C编译器完全不同的方式重整从标头文件导出的符号的名称,因此当您尝试进行链接时,您将收到链接器错误,表示缺少符号。
为了解决这个问题,我们告诉C++编译器以"C"模式运行,因此它以与C编译器相同的方式执行名称重整。这样做后,链接器错误就被修复了。
C和C ++对符号名称有不同的规则。符号是链接器知道编译器生成的一个对象文件中调用函数“openBankAccount”的引用,是指向另一个从不同源文件生成的对象文件中调用名为“openBankAccount”的函数的方式。这使您可以将多个源文件组成程序,在处理大型项目时非常方便。
在C中,规则非常简单,符号都在单个名称空间中。因此,整数“socks”存储为“socks”,函数count_socks存储为“count_socks”。
链接器是为C和其他类似C的语言构建的,具有这种简单的符号命名规则。因此,链接器中的符号只是简单的字符串。
但是,在C ++中,该语言允许您具有命名空间、多态和其他与这种简单规则冲突的东西。您的六个称为“add”的多态函数都需要具有不同的符号,否则其他对象文件将使用错误的函数。这通过对符号名称进行“mangling”(这是一个技术术语)来完成。
当将C ++代码链接到C库或代码时,您需要extern "C"任何写入C的内容,例如C库的头文件,以告诉C ++编译器这些符号名称不应进行名称混淆,而您的其余C ++代码当然必须进行名称混淆,否则它将无法工作。
什么时候应该使用它?
当您将C库链接到C ++对象文件中时。
编译器/链接器级别上会发生什么,需要我们使用它?
C和C++在符号命名方案方面使用不同的方法。这告诉链接器在链接给定库时使用C的方案。
在编译/链接方面,这如何解决需要我们使用它的问题?
使用C命名方案允许您引用C风格的符号。否则,链接器会尝试使用C++风格的符号,这不起作用。
C++编译器和C编译器创建符号名称的方式不同。因此,如果您尝试调用存储在以C代码编译的C文件中的函数,则需要告诉C++编译器它正在尝试解析的符号名称看起来与默认值不同;否则,链接步骤将失败。
extern "C" {}
结构指示编译器不对大括号内声明的名称进行名称重整。通常,C++编译器会“增强”函数名称,以便它们编码关于参数和返回值的类型信息;这称为“名称重整”。extern "C"
结构可以防止名称重整。这是用于解决名称混淆问题的。extern C 意味着函数在“平面”的 C 风格 API 中。
#ifdef __cplusplus extern "C" { #endif
因此,当从C++文件中引用时,它们仍然被视为C头文件。 - Calmarius