为什么 C 函数不能被命名错误?

我最近接受了一次采访,有人问我,在 C + + 代码中使用 extern "C"有什么用。我回答说,它是在 C + + 代码中使用 C 函数,因为 C 不使用名称混淆。有人问我为什么 C 不使用名字拼写,老实说,我无法回答。

我知道当 C + + 编译器编译函数时,它给函数起了一个特殊的名字,主要是因为我们可以在 C + + 中重载同名的函数,这些函数必须在编译时解析。在 C 语言中,函数的名称保持不变,或者前面加一个 _。

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

13677 次浏览

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

所以前提是有缺陷的。

这并不是说他们“不能”,他们 没有,一般。

如果你想在一个叫做 foo(int x, const char *y)的 C 库中调用一个函数,让你的 C + + 编译器把它混入到 foo_I_cCP()中(或者随便什么,只是在这里现场创建了一个混合模式)是没有用的,因为它可以。

这个名称不会解析,函数是 C 语言的,它的名称不依赖于它的参数类型列表。因此,C + + 编译器必须知道这一点,并将该函数标记为 C,以避免损坏。

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

C + + 希望能够与 C 代码进行互操作,这些代码可以链接到 C + + ,也可以链接到 C + + 。

C 期望没有名称混乱的函数名。

如果 C + + 对它进行了错误处理,它将不会找到从 C 导出的非错误处理函数,或者 C 将不会找到 C + + 导出的函数。C 链接器必须得到它自己期望的名称,因为它不知道它是从 C + + 发出的还是去 C + + 。

C + + 编译器使用名称错位,以便允许重载函数使用唯一的符号名,否则这些函数的签名将是相同的。它基本上也对参数的类型进行编码,这允许在基于函数的级别上进行多态。

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

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

让 C + + 编译器也破坏 C 函数有什么错?

它们不再是 C 函数了。

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

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

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

这个问题在上面已经有了答案,不过我会试着联系上下文。

首先,C 先出现。因此,C 所做的就是某种程度上的“默认”。它不会弄乱名字,因为它就是不会。函数名就是函数名。全局就是全局,等等。

然后 C + + 出现了。C + + 希望能够使用与 C 相同的链接器,并能够链接用 C 编写的代码。但是 C + + 不能让 C 就这样“损坏”(或者说缺乏)。看看下面的例子:

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

在 C + + 中,这些是不同的函数,具有不同的主体。如果它们都没有损坏,那么它们都将被称为“ function”(或“ _ 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 + + 编译器现在知道导入 _function而不是 _function_int,一切都很好。

有些程序部分是用 c 语言编写的,部分是用其他语言编写的(通常是汇编语言,但有时是帕斯卡语言、 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 中]。能够定义链接器名称 因此对于这些功能的使用是至关重要的。

我将添加一个其他的答案,以解决发生的一些无关紧要的讨论。

最初调用的应用二进制接口是以相反的顺序在堆栈上传递参数(即从右向左推) ,调用者也在这里释放堆栈存储。现代 ABI 实际上使用寄存器来传递参数,但是许多错综复杂的考虑要回溯到最初的堆栈参数传递。

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

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

最初的 Windows 3.1 ABI 是基于 Pascal 的。因此,它使用 PascalABI (参数从左到右排列,被调用方弹出)。由于参数数目的任何不匹配都可能导致堆栈损坏,因此形成了一个损坏方案。每个函数名都被一个数字搞乱了,这个数字表示它的参数的大小(以字节为单位)。因此,在16位机器上,下面的函数(C 语法) :

int function(int a)

因为 int是两个字节宽的,所以被错误地设置为 function@2。这样做的目的是,如果声明和定义不匹配,链接器将无法找到函数,而不是在运行时损坏堆栈。相反,如果程序链接,则可以确保在调用结束时从堆栈中弹出正确的字节数。

32位 Windows 和后续使用 stdcall ABI 代替。它类似于帕斯卡 ABI,除了推送顺序类似于 C,从右到左。与 PascalABI 类似,名称错位将参数字节大小错位到函数名中,以避免堆栈损坏。

与其他地方的声明不同,C ABI 不会弄乱函数名,即使在 Visual Studio 上也是如此。相反,使用 stdcall ABI 规范修饰的错综复杂的函数并非 V.GCC 所独有,即使在为 Linux 编译时,GCC 也支持这种 ABI。红酒广泛使用这种方法,它使用自己的加载程序允许在运行时将 Linux 编译的二进制文件链接到 Windows 编译的 DLL。

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

早在1991年,我就提出了通过使用碾压来实现 C 变量和函数的类型安全连接方案。该计划从未被采纳,因为正如其他人指出的那样,这会破坏向下兼容。