为什么C函数不能被名字打乱?
我最近接受了一个采访,有一个问题是在C ++代码中使用了extern "C"
。 我回答说在C ++代码中使用C函数是因为C不使用名称修饰。 有人问我为什么C不使用名称修改,老实说我无法回答。
我明白,当C ++编译器编译函数时,会给函数一个特殊的名字,主要是因为我们可以在C ++中重载相同名称的函数,而这些函数必须在编译时parsing。 在C中,函数的名称将保持不变,或者在_之前。
我的查询是:允许C ++编译器破坏C函数还有什么错? 我会假定编译器赋予它们的名称并不重要。 我们在C和C ++中以相同的方式调用函数。
这是上面的答案,但我会尝试把事情的背景。
首先,C来了。 因此,C所做的就是“默认”。 它不会破坏名称,因为它没有。 函数名称是一个函数名称。 全球化是全球化的,等等。
然后C ++来了。 C ++希望能够使用与C相同的链接器,并且能够与使用C编写的代码链接。但是,C ++不能让C“模糊”(或者缺less)。 看看下面的例子:
int function(int a); int function();
在C ++中,这些是独特的function,具有不同的机构。 如果它们都没有被破坏,则两者都将被称为“函数”(或“_function”),并且链接器将会抱怨符号的重新定义。 C ++解决scheme是将参数types转换为函数名称。 所以,一个被称为_function_int
,另一个被称为_function_void
(不是实际的mangling scheme),避免碰撞。
现在我们留下了一个问题。 如果在C模块中定义了int function(int a)
,并且我们只是在C ++代码中使用它的头(即声明)并使用它,那么编译器将生成一条指令给链接器以导入_function_int
。 当函数被定义时,在C模块中,它没有被调用。 它被称为_function
。 这将导致链接器错误。
为了避免这个错误,在函数声明过程中,我们告诉编译器它是一个被devise为与C编译器链接或编译的函数:
extern "C" int function(int a);
C ++编译器现在知道导入_function
而不是_function_int
,一切都很好。
这并不是说他们“不能”,总的来说,他们不是 。
如果你想在一个名为foo(int x, const char *y)
的C库中调用一个函数,让你的C ++编译器将其转换为foo_I_cCP()
(或者其他任何东西, )只是因为它可以。
该名称不会parsing,该函数在C中,其名称不依赖于参数types的列表。 所以C ++编译器必须知道这一点,并将该函数标记为C以避免进行修改。
请记住,所说的C函数可能在一个你没有的源代码库中,你所拥有的只是预编译的二进制文件和头文件。 所以你的C ++编译器不能做“它自己的东西”,它毕竟不能改变库中的东西。
允许C ++编译器破坏C函数还有什么问题?
他们不再是C的function了。
函数不仅仅是一个签名和一个定义, 一个函数如何工作很大程度上取决于调用约定等因素。 指定在您的平台上使用的“应用程序二进制接口”描述了系统如何相互通信。 您的系统正在使用的C ++ ABI指定了名称修改scheme,以便该系统上的程序知道如何调用库中的函数等等。 (阅读C ++ Itanium ABI就是一个很好的例子,你很快就会明白为什么它是必须的。)
这同样适用于您的系统上的C ABI。 有些C ABI实际上有一个名称修改scheme(例如Visual Studio),所以对于某些function来说,这不是关于“closures名称修改”,而是更多地从C ++ ABI切换到C ABI。 我们将C函数标记为C函数,C ABI(而不是C ++ ABI)是相关的。 声明必须符合定义(在同一个项目或某个第三方库中),否则声明是毫无意义的。 没有这个,你的系统根本就不知道如何定位/调用这些函数。
至于为什么平台没有定义C和C ++ ABI是相同的,并且摆脱了这个“问题”,这部分是历史的 – 原来的C ABI不足以用于C ++,它具有名称空间,类和运算符重载,所有其中需要以某种方式以计算机友好的方式用符号的名字来表示 – 但也有人可能会认为,使C程序现在遵守C ++对于C社区是不公平的,这将不得不忍受一个更加复杂的ABI只是为了一些想要互操作的人。
MSVC实际上是用一种简单的方式来破坏C的名字。 它有时会附加@4
或另一个小数字。 这涉及调用约定和堆栈清理的需要。
所以前提是有缺陷的。
有部分用C语言编写的程序,部分用其他语言编写(通常是汇编语言,但有时是Pascal,FORTRAN或其他),这是很常见的。 让程序包含由不同的人编写的不同组件的程序也是很常见的,这些组件可能没有任何东西的源代码。
在大多数平台上,有一个规范 – 通常被称为ABI [应用程序二进制接口],它描述了编译器必须做什么来产生具有特定名称的函数,该函数接受某些特定types的参数并返回某种特定types的值。 在某些情况下,ABI可能会定义多个“调用约定”; 这种系统的编译器通常提供一种方法来指示哪个调用约定应该用于特定的function。 例如,在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中]。 能够定义链接器名称对于这些function的使用至关重要。
我会添加一个其他的答案,以解决发生的一些切线的讨论。
C ABI(应用程序二进制接口)最初被要求以相反的顺序(即 – 从右向左推送)在堆栈上传递参数,调用者也释放堆栈存储。 现代的ABI实际上使用寄存器来传递参数,但是许多重要的考虑事项都回溯到原始的堆栈parameter passing。
相比之下,最初的Pascal ABI将参数从左向右推送,被调用者必须popup参数。 原来的C ABI在两个重点上优于原来的Pascal ABI。 参数push顺序意味着第一个参数的堆栈偏移量总是已知的,允许具有未知数量的参数的函数,其中早期参数控制有多less其他参数(ala printf
)。
C ABI优越的第二种方式是在主叫方和被叫方不同意有多less个参数的情况下的行为。 在C的情况下,只要你没有实际访问最后一个参数,没有什么不好的事情发生。 在Pascal中,错误数量的参数从堆栈popup,整个堆栈已损坏。
最初的Windows 3.1 ABI是基于Pascal的。 因此,它使用Pascal ABI(从左到右顺序的参数,被调用的pop)。 由于参数号码不匹配可能会导致堆栈损坏,所以形成了一个encryptionscheme。 每个函数的名字都用一个数字来表示,它的参数的大小以字节为单位。 所以,在16位机上,下面的函数(C语法):
int function(int a)
因为int
是两个字节宽,所以被破坏为function@2
。 这样做是为了在声明和定义不匹配的情况下,链接器将无法在运行时find该函数,而不会损坏堆栈。 相反,如果程序链接,则可以确保在调用结束时正确的字节数从堆栈中popup。
32位Windows并继续使用stdcall
ABI。 它与Pascal ABI类似,除了推送顺序与C中的顺序相同,从右到左。 就像Pascal ABI一样,这个名字将参数字节大小压缩到函数名称中,以避免堆栈损坏。
与其他地方的声明不同,即使在Visual Studio上,C ABI也不会破坏函数名称。 相反,使用stdcall
ABI规范修饰的mangling函数对于VS来说并不是唯一的。 即使在编译Linux时,GCC也支持这个ABI。 这被Wine广泛使用,它使用它自己的加载器来允许运行时将Linux编译的二进制文件链接到Windows编译的DLL文件。
C ++编译器使用name mangling来允许唯一的符号名称用于重载的函数,否则它们的签名将是相同的。 它基本上对参数types进行编码,从而允许在基于函数的级别上进行多态。
C不需要这个,因为它不允许超载的function。
请注意,名称修改是一个(但肯定不是唯一的!)原因,不能依靠“C ++ ABI”。
C ++希望能够与C代码进行互操作,这些C代码链接在一起,或者链接到C代码。
C预计非名称mangled函数名称。
如果C ++破坏它,它不会从C中find导出的非损坏的函数,或者C将找不到C ++导出的函数。 C链接器必须得到它自己期望的名称,因为它不知道它是来自还是来自C ++。
处理C函数和variables的名字将允许在链接时检查它们的types。 目前,所有(?)C实现允许您在一个文件中定义一个variables,并将其作为另一个文件中的函数调用。 或者你可以声明一个错误签名的函数(例如void fopen(double)
,然后调用它。
我提出了一个在1991年通过使用回归的方式来实现Cvariables和函数的types安全联系的scheme。该scheme从未被采用,因为正如其他人在这里指出的那样,这会破坏向后兼容性。