【问题标题】:Combining C++ and C - how does #ifdef __cplusplus work?结合 C++ 和 C - #ifdef __cplusplus 如何工作?
【发布时间】:2011-04-16 21:40:42
【问题描述】:

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

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

#ifdef __cplusplus
extern "C" {
#endif

在底部,我们写

#ifdef __cplusplus
}
#endif

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

  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++ 在可预见的未来,我 想确保我们覆盖所有 我们的基地。

【问题讨论】:

标签: c++ c c-preprocessor extern-c


【解决方案1】:

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

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

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

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

【讨论】:

  • 当您的 C++ 标头/代码被困在某些 C 代码中时,很适合提及 extern "C++"
  • 我写了一个简单的C程序。在其中我添加了 #ifdef __cplusplus 块并添加了 printf("__cplusplus defined\n");在里面。如果我用 gcc 编译它,“__cplusplus defined”就不会打印出来,但是如果我用 g++ 编译它,它就会打印出来。所以我认为 __cplusplus 意味着编译器是 C++ 编译器(你说过)。这不是正确的吗? (因为我看到你说'__cplusplus 应该在外部“C”块中定义'。我们可以明确定义 __cplusplus 吗?
  • 虽然您应该能够(几乎)定义任何您想要的东西,但__cplusplus 的全部意义在于确定是否使用C++C,因此手动/显式定义它违背了它的目的......
  • extern "C" 确实不是关于编译器如何查看源文件,而是它如何查看头文件。当编译为 C 与 C++ 时,结构可能具有不同的大小,当然还有名称修饰,以及可能的其他差异。
【解决方案2】:
  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++,所有这些都需要。
【解决方案3】:

与 Andrew Shelansky 的出色回答相得益彰的几个陷阱以及对 的一些不同意见并没有真正改变编译器读取代码的方式

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

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

链接器错误将在稍后发生,因为您的函数似乎找不到包含在 C 和 C++ 源代码的混合中。

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

【讨论】:

  • 这是一个神秘的原因,把我的头发拉了出来。确实需要张贴在某个地方。
【解决方案4】:

这是关于 ABI,为了让 C 和 C++ 应用程序都使用 C 接口没有任何问题。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-18
    • 2021-03-01
    • 2011-11-23
    • 2021-11-12
    • 2012-03-18
    相关资源
    最近更新 更多