365

我正在开发一个包含大量遗留C代码的项目。我们已经开始用 C++ 编写,目的是最终也转换遗留代码。我对C和 C++ 的交互方式有点困惑。我知道通过用 C++ 编译器包装C代码extern "C"不会破坏C代码的名称,但我不完全确定如何实现这一点。

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

#ifdef __cplusplus
extern "C" {
#endif

在底部,我们写

#ifdef __cplusplus
}
#endif

在两者之间,我们拥有所有的包含、类型定义和函数原型。我有几个问题,看看我是否理解正确:

  1. 如果我有一个包含C头文件 Bh 的 C++ 文件 A.hh,包含另一个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++,我想确保我们覆盖了所有的基础。

4

4 回答 4

328

extern "C"并没有真正改变编译器读取代码的方式。如果你的代码在 .c 文件中,它将被编译为 C,如果它在 .cpp 文件中,它将被编译为 C++(除非你对你的配置做了一些奇怪的事情)。

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

anextern "C"中的代码仍然是 C++ 代码。在外部“C”块中可以做的事情是有限制的,但它们都是关于链接的。您不能定义任何无法使用 C 链接构建的新符号。例如,这意味着没有类或模板。

extern "C"块很好地嵌套。extern "C++"如果您发现自己被困在区域内,也extern "C"有可能,但从清洁的角度来看,这不是一个好主意。

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

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

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

extern "C"要回答您的问题 #3:如果没有原型的函数在 .cpp 文件中而不是在块内,则它们将具有 C++ 链接。不过这很好,因为如果它没有原型,它只能被同一个文件中的其他函数调用,然后你通常不关心链接是什么样的,因为你不打算拥有那个函数无论如何都会被同一编译单元之外的任何东西调用。

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

5:这种混合是使用的常见原因extern "C",我认为这样做没有任何问题——只要确保你明白你在做什么。

于 2010-09-24T17:32:50.127 回答
51
  1. extern "C"不会改变__cplusplus宏的存在与否。它只是改变了包装声明的链接和名称修饰。

  2. 您可以非常愉快地嵌套extern "C"块。

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

  4. 是的

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

于 2010-09-24T17:08:01.300 回答
25

与 Andrew Shelansky 的出色答案相得益彰的几个陷阱,并且有点不同意并没有真正改变编译器读取代码的方式

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

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

链接器错误将在稍后发生,因为如果您没有围绕声明的extern "C"包装器并且标头包含在 C 和 C++ 源代码的混合中,则似乎找不到您的函数。

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

于 2014-08-26T08:15:16.780 回答
5

这是关于 ABI,为了让 C 和 C++ 应用程序都可以毫无问题地使用 C 接口。

由于 C 语言非常简单,对于不同的编译器,例如 GCC、Borland C\C++、MSVC 等,代码生成多年来都是稳定的。

虽然 C++ 变得越来越流行,但必须将很多东西添加到新的 C++ 域中(例如,最后 Cfront 在 AT&T 被放弃,因为 C 无法涵盖它需要的所有功能)。比如模板特性、编译时代码生成等,过去不同的编译器厂商实际上是分别进行了C++编译器和链接器的实际实现,实际的ABI与不同平台的C++程序完全不兼容。

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

于 2018-12-14T10:04:21.867 回答