ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

#ifdef __cplusplus
extern "C" {
#endif

在底部,我们写

#ifdef __cplusplus
}
#endif

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

如果我有一个包含 C 头文件 Bh 的 C++ 文件 A.hh,包含另一个 C 头文件 Ch,这是如何工作的?我认为当编译器进入 Bh 时,会定义 __cplusplus,因此它将用 extern "C" 包装代码(并且不会在此块内定义 __cplusplus)。因此,当它进入 Ch 时,__cplusplus 不会被定义,代码也不会被包裹在 extern "C" 中。这个对吗?用 extern "C" { extern "C" { .. } } 包装一段代码有什么问题吗?第二个外部“C”会做什么?我们不会将此包装器放在 .c 文件周围,而只是 .h 文件。那么,如果一个函数没有原型会发生什么?编译器是否认为它是 C++ 函数?我们还使用了一些用 C 编写的第三方代码,并且没有这种包装器。每当我包含该库中的标头时,我都会在#include 周围放置一个外部“C”。这是处理这个问题的正确方法吗?最后,这是一个好主意吗?还有什么我们应该做的吗?在可预见的未来,我们将混合 C 和 C++,我想确保我们覆盖了所有的基础。

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

V
VLL

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++ 时,结构可能具有不同的大小,当然还有名称修饰,以及可能的其他差异。
M
Martin G

extern "C" 不会改变 __cplusplus 宏的存在与否。它只是改变了包装声明的链接和名称修饰。您可以非常愉快地嵌套 extern "C" 块。如果您将 .c 文件编译为 C++,则任何不在 extern "C" 块中且没有 extern "C" 原型的内容都将被视为 C++ 函数。如果将它们编译为 C,那么当然一切都是 C 函数。是的,您可以通过这种方式安全地混合 C 和 C++。


如果您将 .c 文件编译为 C++,那么所有内容都将编译为 C++ 代码,即使它位于 extern "C" 块中。 extern "C" 代码不能使用依赖于 C++ 调用约定(例如运算符重载)的功能,但函数的主体仍然编译为 C++,所有这些都需要。
A
Andy Dent

一些陷阱是 Andrew Shelansky 出色答案的补充,并且有点不同意并没有真正改变编译器读取代码的方式

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

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

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

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


这是一个神秘的原因,把我的头发拉了出来。确实需要张贴在某个地方。
B
Bo Zhou

这是关于 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++ 编译器,则用于接口函数。