139

我最近接受了一次采访,被问到的一个问题是extern "C"C++ 代码中的用途是什么。我回答说是在 C++ 代码中使用 C 函数,因为 C 不使用名称修饰。有人问我为什么 C 不使用名称修饰,老实说我无法回答。

我知道当 C++ 编译器编译函数时,它会给函数一个特殊的名称,主要是因为我们可以在 C++ 中重载必须在编译时解析的同名函数。在 C 中,函数的名称将保持不变,或者可能在其前面带有一个 _。

我的疑问是:允许 C++ 编译器也破坏 C 函数有什么问题?我会假设编译器给它们起什么名字并不重要。我们在 C 和 C++ 中以相同的方式调用函数。

4

9 回答 9

192

上面有点回答了,但我会试着把事情放在上下文中。

首先,C是第一位的。因此,C 所做的就是“默认”。它不会破坏名称,因为它不会。函数名是函数名。全局就是全局,以此类推。

然后 C++ 出现了。C++ 希望能够使用与 C 相同的链接器,并且能够与用 C 编写的代码进行链接。但是 C++ 不能让 C 的“修改”(或缺少)保持原样。查看以下示例:

int function(int a);
int function();

在 C++ 中,这些是不同的函数,具有不同的主体。如果它们都没有被破坏,则两者都将被称为“函数”(或“_function”),并且链接器将抱怨符号的重新定义。C++ 解决方案是将参数类型转换为函数名称。因此,一个被调用_function_int,另一个被调用_function_void(不是实际的重整方案)并且避免了碰撞。

现在我们面临一个问题。如果int function(int a)是在 C 模块中定义的,而我们只是在 C++ 代码中获取它的标头(即声明)并使用它,编译器将生成一条指令给链接器以导入_function_int。当函数被定义时,在 C 模块中,它没有被调用。它被称为_function。这将导致链接器错误。

为避免该错误,在函数声明期间,我们告诉编译器它是一个旨在与 C 编译器链接或由 C 编译器编译的函数:

extern "C" int function(int a);

C++ 编译器现在知道 import_function而不是_function_int,一切都很好。

于 2016-04-14T12:05:52.727 回答
45

并不是说他们“不能”,一般来说,他们不是。

如果你想调用 C 库中名为 的函数foo(int x, const char *y),那么让你的 C++ 编译器将其修改为foo_I_cCP()(或其他什么,只是在这里当场制定一个修改方案)是不好的,因为它可以。

该名称无法解析,该函数位于 C 语言中,并且其名称不依赖于其参数类型列表。所以 C++ 编译器必须知道这一点,并将该函数标记为 C 以避免进行修改。

请记住,所述 C 函数可能位于您没有源代码的库中,您所拥有的只是预编译的二进制文件和标头。所以你的 C++ 编译器不能做“它自己的事情”,它毕竟不能改变库中的内容。

于 2016-04-14T11:38:33.123 回答
32

允许 C++ 编译器也破坏 C 函数有什么问题?

它们不再是 C 函数了。

函数不仅仅是一个签名和一个定义;函数的工作方式很大程度上取决于调用约定等因素。指定在您的平台上使用的“应用程序二进制接口”描述了系统如何相互通信。您的系统使用的 C++ ABI 指定了名称修改方案,以便该系统上的程序知道如何调用库中的函数等。(阅读 C++ Itanium ABI 以获得一个很好的例子。你会很快明白为什么它是必要的。)

这同样适用于您系统上的 C ABI。一些 C ABI 实际上有一个名称修改方案(例如 Visual Studio),所以这不是关于“关闭名称修改”,而是更多关于从 C++ ABI 切换到 C ABI,对于某些功能。我们将 C 函数标记为 C 函数,与 C ABI(而不是 C++ ABI)相关。声明必须与定义匹配(无论是在同一个项目中还是在某个第三方库中),否则声明毫无意义。没有它,您的系统根本不知道如何定位/调用这些功能。

至于为什么平台不将 C 和 C++ ABI 定义为相同并摆脱这个“问题”,这部分是历史性的——原始的 C ABI 不足以用于具有命名空间、类和运算符重载的 C++,所有其中需要以某种计算机友好的方式以符号名称表示——但也有人可能会争辩说,让 C 程序现在遵守 C++ 对 C 社区是不公平的,因为 C 社区将不得不忍受一个更复杂的大规模ABI 只是为了其他一些想要互操作性的人。

于 2016-04-14T11:41:23.147 回答
21

MSVC 实际上确实会破坏 C 名称,尽管是以一种简单的方式。它有时会附加@4或另一个小数字。这与调用约定和堆栈清理的需要有关。

所以这个前提是有缺陷的。

于 2016-04-14T11:38:26.707 回答
13

部分用 C 语言编写,部分用其他语言(通常是汇编语言,但有时是 Pascal、FORTRAN 或其他语言)编写的程序很常见。程序包含由不同人编写的不同组件也很常见,这些人可能没有所有东西的源代码。

在大多数平台上,都有一个规范——通常称为 ABI [应用程序二进制接口],它描述了编译器必须做什么才能生成具有特定名称的函数,该函数接受某些特定类型的参数并返回某些特定类型的值。在某些情况下,ABI 可能会定义多个“调用约定”;此类系统的编译器通常提供一种方法来指示应为特定函数使用哪种调用约定。例如,在 Macintosh 上,大多数 Toolbox 例程都使用 Pascal 调用约定,因此“LineTo”之类的原型将类似于:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

如果一个项目中的所有代码都是使用同一个编译器编译的,那么编译器为每个函数导出什么名称并不重要,但在许多情况下,C 代码需要调用使用其他工具编译的函数,并且不能用当前的编译器重新编译[甚至可能不在 C 中]。因此,能够定义链接器名称对于使用此类函数至关重要。

于 2016-04-14T14:53:16.970 回答
12

我将添加另一个答案,以解决发生的一些切题讨论。

最初调用 C ABI(应用程序二进制接口)是为了以相反的顺序(即 - 从右到左推送)在堆栈上传递参数,调用者还释放堆栈存储空间。现代 ABI 实际上使用寄存器来传递参数,但许多错误的考虑可以追溯到最初的堆栈参数传递。

相比之下,原始的 Pascal ABI 将参数从左向右推送,而被调用者必须弹出参数。原始 C ABI 在两个重要方面优于原始 Pascal ABI。参数推送顺序意味着第一个参数的堆栈偏移量始终是已知的,允许具有未知数量参数的函数,其中早期参数控制有多少其他参数(ala printf)。

C ABI 优越的第二种方式是调用者和被调用者不同意有多少参数时的行为。在 C 的情况下,只要您实际上不访问最后一个参数之后的参数,就不会发生任何不好的事情。在 Pascal 中,从堆栈中弹出错误数量的参数,整个堆栈被破坏。

最初的 Windows 3.1 ABI 基于 Pascal。因此,它使用了 Pascal ABI(从左到右顺序的参数,被调用者弹出)。由于参数编号的任何不匹配都可能导致堆栈损坏,因此形成了一种修改方案。每个函数名称都带有一个数字,表示其参数的大小(以字节为单位)。因此,在 16 位机器上,以下函数(C 语法):

int function(int a)

被修改为function@2,因为int是两个字节宽。这样做是为了如果声明和定义不匹配,链接器将无法找到函数,而不是在运行时破坏堆栈。相反,如果程序链接,那么您可以确保在调用结束时从堆栈中弹出正确数量的字节。

32 位 Windows 及更高版本使用stdcallABI。它类似于 Pascal ABI,除了推送顺序与 C 中的一样,从右到左。与 Pascal ABI 一样,名称 mangling 将参数字节大小更改为函数名称以避免堆栈损坏。

与此处其他地方的声明不同,C ABI 不会破坏函数名称,即使在 Visual Studio 上也是如此。相反,用 ABI 规范修饰的函数stdcall并不是 VS 独有的。GCC 也支持此 ABI,即使在为 Linux 编译时也是如此。这被Wine广泛使用,它使用自己的加载器来允许将 Linux 编译的二进制文件运行时链接到 Windows 编译的 DLL。

于 2016-04-15T11:54:22.520 回答
9

C++ 编译器使用名称修饰来允许重载函数的唯一符号名称,否则其签名将相同。它基本上也对参数的类型进行编码,这允许在基于函数的级别上实现多态性。

C 不需要这个,因为它不允许函数重载。

请注意,名称修改是不能依赖“C++ ABI”的一个(但肯定不是唯一的!)原因。

于 2016-04-14T11:40:57.177 回答
8

C++ 希望能够与链接到它或链接到它的 C 代码互操作。

C 需要非名称损坏的函数名称。

如果 C++ 损坏它,它将找不到从 C 中导出的未损坏函数,或者 C 将找不到 C++ 导出的函数。C 链接器必须获得它自己期望的名称,因为它不知道它来自或去往 C++。

于 2016-04-14T11:40:24.647 回答
3

修改 C 函数和变量的名称将允许在链接时检查它们的类型。目前,所有(?)C 实现都允许您在一个文件中定义一个变量并在另一个文件中将其作为函数调用。或者你可以声明一个签名错误的函数(例如void fopen(double),然后调用它。

早在 1991 年,我就通过使用 mangling提出了 C 变量和函数的类型安全链接的方案。该方案从未被采用,因为正如其他人在这里指出的那样,这会破坏向后兼容性。

于 2016-04-17T11:31:09.540 回答