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


318

我正在一个有很多遗留C代码的项目中。我们也开始使用C ++进行编写,以期最终也转换旧代码。我对C和C ++的交互方式有些困惑。我了解到,用C ++编译器包装C代码extern "C"不会破坏C代码的名称,但是我不确定如何实现这一点。

因此,在每个C头文件的顶部(在包含保护之后),我们有

#ifdef __cplusplus
extern "C" {
#endif

在底部,我们写

#ifdef __cplusplus
}
#endif

在这两者之间,我们拥有所有的include,typedef和函数原型。我有几个问题,看我是否正确理解:

  1. 如果我有一个C ++文件A.hh,其中包括一个C头文件Bh,包括另一个C头文件Ch,那么该如何工作?我认为,当编译器进入Bh时, __cplusplus将被定义,因此它将包装代码extern "C" (并且__cplusplus不会在此块内定义)。因此,当它进入Ch时, __cplusplus将不会被定义,并且代码也不会被包装在中 extern "C"。它是否正确?

  2. 将一段代码包装起来有什么问题 extern "C" { extern "C" { .. } }吗?第二个extern "C" 怎么办?

  3. 我们不会在.c文件中放这个包装器,而在.h文件中放。那么,如果一个函数没有原型怎么办?编译器是否认为这是C ++函数?

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

  5. 最后,这是一个好主意吗?还有什么我们应该做的?在可预见的将来,我们将混合使用C和C ++,我想确保我们涵盖了所有基础知识。



2
简而言之,这是最好的解释:( To ensure that the names declared in that portion of code have C linkage, and thus C++ name mangling is not performed. 我从链接中得到
anhldbk

您不必用粗体显示C语言的名称
Edward Karak

Answers:


290

extern "C"并没有真正改变编译器读取代码的方式。如果您的代码在.c文件中,则将其编译为C;如果在.cpp文件中,则将其编译为C ++(除非您对配置进行了奇怪的操作)。

什么extern "C"会影响链接。编译C ++函数时,其名称将被修改-这就是使重载成为可能的原因。函数名称会根据参数的类型和数量进行修改,因此具有相同名称的两个函数将具有不同的符号名称。

内部的extern "C"代码仍然是C ++代码。在外部“ C”块中可以执行的操作有一些限制,但是所有这些都与链接有关。您无法定义任何无法使用C链接构建的新符号。例如,这意味着没有类或模板。

extern "C"块很好地筑巢。还有extern "C++",如果你发现自己陷入绝望的内extern "C"区域,但它不是从清洁的角度这样一个好主意。

现在,特别是关于您编号的问题:

关于#1:__ cplusplus将在extern "C"块内保持定义。不过,这并不重要,因为这些块应该整齐地嵌套。

关于#2:将为正在通过C ++编译器运行的任何编译单元定义__cplusplus。通常,这意味着.cpp文件以及该.cpp文件包含的任何文件。如果包含不同的编译单元,则相同的.h(或.hh或.hpp或您拥有什么)可以在不同时间解释为C或C ++。如果您希望.h文件中的原型引用C符号名称,那么extern "C"在被解释为C ++时它们必须具有,而extern "C"在被解释为C时它们不应当具有-因此进行#ifdef __cplusplus检查。

要回答您的问题3:如果没有原型的函数位于.cpp文件中,而不位于extern "C"块内,则它们将具有C ++链接。不过,这很好,因为如果没有原型,则只能由同一文件中的其他函数调用,因此通常不必关心链接的外观,因为您不打算拥有该函数无论如何,同一编译单元之外的任何东西都可以调用它。

对于#4,您已完全了解。如果您要包含具有C链接的代码的标头(例如,由C编译器编译的代码),则必须extern "C"具有标头-这样便可以与库链接。(否则,您的链接器将在查找名称类似于_Z1hic您正在查找的函数void h(int, char)

5:使用这种混音是常见的原因extern "C",我认为这样做没有任何问题-只需确保您了解自己在做什么即可。


10
extern "C++"
值得

1
我写了一个简单的C程序。在其中添加了#ifdef __cplusplus块,并添加了printf(“ __ cplusplus defined \ n”); 在里面。如果我使用gcc进行编译,则不会打印“ __cplusplusdefined”,但是如果我使用g ++进行编译,则会进行打印。所以我认为__cplusplus表示编译器是C ++编译器(您说过)。是不是这样 (因为我看到你说'__cplusplus应该在外部“ C”块内定义。”我们可以明确定义__cplusplus吗?
Chan Kim

1
尽管您应该能够(几乎)定义所需的任何内容,但重点__cplusplus是确定是否C++正在使用vs C,因此手动/明确定义它的目的……
nurchi

extern“ C”的确与编译器如何查看源文件无关,而与它如何查看头文件有关。当用C vs C ++编译时,结构的大小可能会有所不同,当然还有名称修饰,也可能存在其他差异。
尼克

39
  1. extern "C"不会更改__cplusplus宏的存在或不存在。它只是更改包装的声明的链接和名称处理。

  2. 您可以extern "C"很高兴地嵌套块。

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

  4. 您可以通过这种方式安全地混合使用C和C ++。


如果将.c文件编译为C ++,则所有内容都将编译为C ++代码,即使它在一个extern "C"块中也是如此。该extern "C"代码不能使用依赖于C ++调用约定的功能(例如,运算符重载),但是该函数的主体仍被编译为C ++,并包含所有这些内容。
David C.18年

21

确实是Andrew Shelansky的出色答案的助手,有些分歧实际上并没有改变编译器读取代码的方式

因为您的函数原型是用C编译的,所以您不能用不同的参数重载相同的函数名称-这是编译器名称处理的关键特征之一。它被描述为一个链接问题,但事实并非如此-编译器和链接器都会出现错误。

如果您尝试使用原型声明的C ++功能(例如重载),则会出现编译器错误。

链接器错误后会发生,因为你的函数会出现无法找到,如果你具备的extern“C”包装周围的声明和头被包含在C和C ++源的混合物。

阻止人们使用C作为C ++设置的原因之一是因为这意味着他们的源代码不再可移植。该设置是项目设置,因此如果将.c文件拖放到另一个项目中,则不会将其编译为c ++。我希望人们花时间将文件后缀重命名为.cpp。


1
这是一个神秘的原因,把我的头发拔了出来。确实需要在某个地方发布。
米切尔·柯里

3

为了使C和C ++应用程序都能毫无问题地使用C接口,这与ABI有关。

由于C语言非常容易,因此对于不同的编译器(例如GCC,Borland C \ C ++,MSVC等),代码生成已稳定多年。

随着C ++越来越流行,必须在新的C ++域中添加很多东西(例如,最终Cfront在AT&T被放弃了,因为C无法满足其所需的所有功能)。例如模板功能和编译时代码生成,过去,不同的编译器供应商实际上是分别执行C ++编译器和链接器的实际实现,实际的ABI与不同平台上的C ++程序完全不兼容。

人们可能仍然想用C ++实现实际程序,但仍然照旧保留旧的C接口和ABI,头文件必须声明extern“ C” {},它告诉编译器生成兼容/旧/简单/简单的C ABI接口函数(如果编译器是C编译器而不是C ++编译器)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.