将C++和C结合使用 - #ifdef __cplusplus如何工作?

393
我正在处理一个项目,其中有很多遗留的C代码。我们已经开始使用C++编程,并打算最终将遗留代码也转换为C++。我对C和C++之间的交互方式有些困惑。我知道通过使用extern "C"来包装C代码,C++编译器不会破坏C代码的名称,但我不太确定如何实现这一点。
所以,在每个C头文件的顶部(include guards之后),我们添加了extern "C"
#ifdef __cplusplus
extern "C" {
#endif

在底部,我们写下:

#ifdef __cplusplus
}
#endif

在这两者之间,我们有所有的包含文件、typedef和函数原型。我有一些问题,想看看我是否理解正确:

  1. 如果我有一个C++文件A.hh,其中包括一个C头文件B.h,又包括另一个C头文件C.h,那么这是如何工作的?我认为当编译器进入B.h时,将定义__cplusplus,因此它将用extern "C"括起代码(并且这个块内部不会定义__cplusplus)。所以当它进入C.h时,__cplusplus将不会被定义,并且代码也不会用extern "C"括起来。这样正确吗?

  2. extern "C" { extern "C" { .. } }把一段代码括起来有什么问题吗?第二个extern "C"会做什么?

  3. 我们不会在.c文件中放置这个包装器,只放在.h文件中。那么,如果一个函数没有原型,会发生什么?编译器会认为它是一个C++函数吗?

  4. 我们还使用了一些第三方的用C编写的代码,并且没有这种包装器。每当我包含该库的标头时,我都会在#include周围放置一个extern "C"。这是处理它的正确方法吗?

  5. 最后,这个设置是一个好主意吗?我们将在可预见的未来混合C和C++,我想确保我们覆盖了所有的基础。


6
简洁明了地说,这是最好的解释: 为了确保在该代码段中声明的名称具有C链接,因此不会执行C ++名称混编。 - anhldbk
相关文章:在同一程序中混合使用C和C++代码 - Sercan Sebetçi
4个回答

352

extern "C"并不会改变编译器读取代码的方式。如果你的代码在.c文件中,它将被编译为C语言;如果在.cpp文件中,则会被编译为C++(除非您对配置进行了奇怪的修改)

extern "C"影响链接。当编译C++函数时,函数名会被修改以实现重载,这就是所谓的名称修饰。函数名称会根据参数类型和数量进行修改,因此具有相同名称的两个函数将具有不同的符号名称。

extern "C"块内部的代码仍然是C++代码。但有一些限制,都与链接相关。例如,不能定义任何无法使用C链接构建的新符号,如类或模板等。

extern "C"块可以嵌套使用。如果你发现自己陷入了extern "C"区域,还可以使用extern "C++",但从清晰性角度来看,这不是一个好主意。

关于你提到的问题:

#1:在extern "C"块内,__cplusplus仍然保持定义。但这并不重要,因为块应该正常嵌套。

#2:__cplusplus将为通过C++编译器运行的任何编译单元定义。通常,这意味着.cpp文件和由该.cpp文件包含的任何文件。如果不同的编译单元在不同时间将.h(或.hh或.hpp等)解释为C或C++,则它们可能会被解释为C或C++。如果您希望.h文件中的原型引用C符号名称,则在将其解释为C++时必须具有extern "C",并且当其被解释为C时,不应该有extern "C",因此需要使用#ifdef __cplusplus进行检查。

回答您的第三个问题:如果没有原型,函数在.cpp文件中且不在extern "C"块内,则具有C++链接。虽然这样做是可以接受的,因为如果没有原型,它只能被同一文件中的其他函数调用,那么您通常不关心链接看起来如何,因为您并不打算让该函数被同一编译单元之外的任何东西调用。
对于问题#4,您已经完全理解了。如果您包含具有C链接(例如由C编译器编译的代码)的代码的头文件,则必须extern "C"标头,以便您可以链接到库。 (否则,当您正在寻找void h(int,char)时,您的链接器将寻找名称为_Z1hic的函数)
第五个问题:这种混合的使用是使用extern "C"的常见原因,我认为这样做没有问题,只要确保您理解自己在做什么即可。

13
当你的C++头文件/代码被嵌入到某些C代码中时,提及“extern"C ++"”是很好的做法。 - deddebme
1
我写了一个简单的C程序。在其中,我添加了一个#ifdef __cplusplus块,并在其中添加了printf("__cplusplus defined\n");。如果我使用gcc编译它,则不会打印“__cplusplus defined”,但是如果我使用g++编译它,则会打印。因此,我认为__cplusplus表示编译器是C ++编译器(您说过)。这不是正确的吗?(因为我看到您说“__cplusplus应定义在extern"C"块内”。我们可以显式定义__cplusplus吗?) - Chan Kim
3
尽管你几乎可以定义任何想要的东西,但__cplusplus 的整个意义在于确定是否使用了 C++ 而不是 C,因此手动或显式地定义它违背了它的目的... - nurchi
extern "C" 实际上与编译器如何查看源代码文件无关,而是与其如何查看头文件有关。在 C 和 C++ 编译时,结构体的大小可能不同,当然还有名称重整等其他差异可能存在。 - Nick

54
  1. extern "C" 不会影响__cplusplus宏的存在或缺失,它只是改变了包装声明的链接和名称重整。

  2. 你可以愉快地嵌套使用 extern "C" 块。

  3. 如果将你的 .c 文件作为 C++ 编译,则没有在 extern "C" 块中且没有 extern "C" 原型的任何内容都将被视为 C++ 函数。如果将它们编译为 C,则所有内容都将是 C 函数。

  4. 是的。

  5. 你可以安全地混合使用 C 和 C++。


3
如果您将 .c 文件编译为 C++,那么所有内容都将被编译为C++代码,即使它在 extern "C" 块中。 extern "C" 代码不能使用依赖于 C++ 调用约定的特性(例如运算符重载),但是函数体仍然作为 C++ 进行编译,具有所有相关的特性。 - David C.

26

在补充Andrew Shelansky的优秀回答的同时,还有一些需要注意的事项。同时也要稍微与“并没有真正改变编译器读取代码的方式”这个说法保持一些不同的看法。

由于您的函数原型是编译为C语言,因此不能使用不同参数重载相同函数名称 - 这是编译器名称重整的一个关键特性。它被描述为链接问题,但这并不完全正确 - 您将从编译器和链接器中都得到错误信息。

如果您尝试使用C++中原型声明的特性如重载,那么编译器会报错。

如果您在包含C和C++源文件的混合环境中没有在声明周围添加“extern "C"”封装,并且头文件已被包含,则链接器将在后续出现找不到函数的错误。

不建议人们使用“将C编译为C++”设置的原因之一是,这意味着他们的源代码不再具有可移植性。该设置是一个项目设置,因此如果将.c文件放入其他项目中,则不会将其编译为c++。我更希望人们花时间将文件后缀名改为.cpp而不是使用该设置。


2
这是一个晦涩难懂的原因,让我抓狂了。真的需要在某个地方发布。 - Mitchell Currie

7

这篇文章讲的是ABI,为了让C和C++应用程序在使用C接口时没有任何问题。

C语言非常简单,多年来已经稳定地为不同编译器(如GCC、Borland C\C++、MSVC等)生成了代码。

随着C++变得越来越流行,许多新功能必须添加到新的C++领域中(例如Cfront被AT&T放弃,因为C无法涵盖它所需的所有功能)。例如模板功能和编译时代码生成,过去,不同的编译器供应商实际上是分别实现C++编译器和链接器的,实际的ABI在不同平台上与C++程序根本不兼容。

人们可能仍然喜欢用C++实现实际程序,但仍像往常一样保留旧的C接口和ABI,头文件必须声明extern "C" {},它告诉编译器为接口函数生成兼容/旧的/简单/易于C ABI,如果编译器是C编译器而不是C++编译器。


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