为什么不能对C函数进行名称修改?


136

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

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

我的查询是:允许C ++编译器也处理C函数怎么了?我本来以为编译器给它们起什么名字都没关系。我们在C和C ++中以相同的方式调用函数。


75
C没有需要到裂伤的名称,因为它没有函数重载。
EOF 2016年

9
如果C ++编译器修改了函数名,如何将C库与C ++代码链接?
2016年

6
“我回答说它是在C ++代码中使用C函数,因为C不使用名称修饰。” -我认为是另一回事。Extern“ C”使C ++函数可在C编译器中使用。来源
rozina

3
@ Engineer999:如果您使用C ++编译器编译C的子集(也就是C ++),则函数名称的确会被弄乱。但是,如果您希望能够链接使用不同编译器创建的二进制文件,则不需要名称修改。
EOF 2016年

13
C 处理名称。通常,整齐的名称是带下划线的函数名称。有时,它是函数的名称,后跟一个下划线。extern "C"表示要以与“ C”编译器相同的方式修改名称。
皮特·贝克尔

Answers:


187

上面已经回答了这个问题,但是我将尝试将其放在上下文中。

首先,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编译器链接或编译的函数:

extern "C" int function(int a);

现在,C ++编译器知道要导入_function而不是_function_int,并且一切都很好。


1
@ShacharShamesh:我在其他地方问过这个问题,但是,用C ++编译的库中的链接呢?当编译器逐步执行并编译我的代码时,该代码调用C ++编译库中的函数之一,如何仅看其声明或函数调用就知道要对该函数进行命名或命名?如何知道在哪里定义了它,而又在其他地方进行了名称修改?因此,在C ++中必须有一个标准的名称处理方法?
Engineer999

2
每个编译器都以其自己的特殊方式进行处理。如果您使用相同的编译器编译所有内容,那就没关系。但是,如果您尝试使用通过Borland编译器编译的库,而该库是从您使用Microsoft编译器构建的程序中获得的,那么……好运;您将需要它:)
Mark VY16'Apr

6
@ Engineer999曾经想知道为什么没有可移植的C ++库,但是它们要么指定要使用的编译器(和标准库)的确切版本(和标志),要么仅导出C API?你去。C ++几乎是有史以来最少可移植的语言,而C恰恰相反。在这方面我们已经付出了很多努力,但是就目前而言,如果您想要真正可移植的东西,可以坚持使用
C。– Voo

1
@Voo好吧,从理论上讲,您应该能够仅通过遵守标准(例如)来编写可移植的代码-std=c++11,并避免使用标准以外的任何内容。这与声明Java版本相同(尽管较新的Java版本是向后兼容的)。人们使用编译器特定的扩展和平台相关的代码不是标准的错误。另一方面,您不能责怪他们,因为标准中遗漏了很多东西(特别是IO,例如套接字)。该委员会似乎正在慢慢地赶上。如果我错过了什么,请纠正我。
mucaho

14
@mucaho:您正在谈论源代码的可移植性/兼容性。即API。Voo在谈论二进制兼容性,而无需重新编译。这需要ABI兼容性。C ++编译器会定期在版本之间更改其ABI。(例如,g ++甚至没有尝试建立一个稳定的ABI。我认为它们并不是为了娱乐而破坏ABI,但是当有收获并且没有其他好的方法时,它们不会避免需要ABI更改的更改。去做吧。)。
彼得·科德斯

45

总的来说,并不是他们“不能”,不是

如果您想在名为的C库中调用一个函数foo(int x, const char *y),那么最好不要让您的C ++编译器将其混入foo_I_cCP()(或随便在此处组成一个混入方案),因为这样做可以。

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

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


这是我所缺少的部分。当C ++编译器仅看到其声明或看到其被调用时,为什么还要修改函数名称。看到函数名称时,它不仅会破坏函数名称吗?这对我来说更有意义
Engineer999

13
@ Engineer999:如何为定义使用一个名称,为声明使用另一个名称?“有一个叫Brian的函数可以调用。” “好吧,我叫布莱恩。” “抱歉,没有名为Brian的功能。” 原来这叫格雷厄姆。
Lightness Races in Orbit

如何在C ++编译库中进行链接?当编译器逐步执行并编译我们的代码时,该代码调用C ++编译库中的函数之一,如何仅看其声明或函数调用就知道要对该函数进行命名或命名?
Engineer999

1
@ Engineer999两者必须在同一处理上达成一致。因此他们看到了头文件(请记住,本机DLL中的元数据很少-头就是该元数据),然后转到“啊,对,Brian应该真的是Graham”。如果这不起作用(例如,使用两个不兼容的修改方案),则您将不会获得正确的链接,并且您的应用程序将失败。C ++有很多这样的不兼容性。在实践中,您必须显式使用乱码的名称并禁用乱码(例如,您告诉代码执行Graham,而不是Brian)。在实际操作中... extern "C":)
六安'16

1
@ Engineer999我可能是错的,但是您是否有使用Visual Basic,C#或Java(甚至在某种程度上甚至是Pascal / Delphi)等语言的经验?这些使互操作看起来非常简单。在C语言中,尤其是在C ++中,仅此而已。您需要遵守很多调用约定,您需要知道谁负责什么内存,并且必须有头文件来告诉您函数声明,因为DLL本身没有包含足够的信息-特别是在DLL的情况下。纯C。如果没有头文件,则通常需要反编译DLL才能使用它。
a安

32

允许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定义为相同并摆脱这种“问题”,这部分是历史性的-原始的C ABI不足以用于C ++,因为C ++具有名称空间,类和运算符重载,所有这些其中某种需要以一种计算机友好的方式以符号的名称表示-但是有人可能会争辩说,使现在遵守C ++的C程序在C社区上是不公平的,这将不得不忍受更加复杂的工作ABI只是为了其他一些想要互操作性的人。


2
+int(PI/3),但含一滴盐:我会非常谨慎地谈论“ C ++ ABI” ... AFAIK,尽管有尝试定义C ++ ABI的尝试,但没有真正的 事实上 / 法律上的标准-isocpp.org/files /papers/n4028.pdf声明(我完全同意),引述一点,具有讽刺意味的是,C ++实际上一直支持一种通过稳定的二进制ABI发布API的方法,即通过外部变量“ C”诉诸C ++的C子集。 ”。C++ Itanium ABI就是这样- 一些用于Itanium的C ++ ABI ...在stackoverflow.com/questions/7492180/c-abi-issues-list上

3
@vaxquis:是的,不是“ C ++的ABI”,而是“ C ++ ABI”,就像我拥有一个不能在所有房屋上使用的“房屋钥匙”一样。猜测它可能是更清晰,但我试图使它尽可能明确由短语出发“的C ++ ABI 在你的系统中使用。为了简洁起见,我在以后的发言中都省略了澄清器,但在这里我接受可以减少混淆的编辑!
Lightness Races in Orbit

1
AIUI C abi倾向于是平台的属性,而C ++ ABI倾向于是单个编译器的属性,甚至往往是单个编译器版本的属性。因此,如果要在使用不同供应商工具构建的模块之间链接,则必须使用C abi作为接口。
plugwash '16

夸大了“名称混用函数不再是C函数”的声明-如果已知名称杂乱无章,则可以从普通香草C中调用名称混用函数。名称的更改不会使其对C ABI的依从性降低,即不会使它与C函数的依从性降低。换种说法更有意义-C ++代码必须在不声明为“ C”的情况下调用C函数,因为在尝试链接被调用方时
彼得-恢复莫妮卡

@ PeterA.Schneider:是的,标题短语被夸大了。该答案的整个其余部分包含了相关事实细节。
Lightness Races in Orbit

21

尽管以简单的方式,MSVC实际上确实破坏了C名称。有时会追加@4或其他少量。这涉及调用约定和堆栈清除的需要。

因此前提是有缺陷的。


2
那不是真正的名字修饰。它只是一种特定于供应商的命名(或名称修饰)约定,以防止将可执行文件链接到使用具有不同调用约定的函数构建的DLL时出现的问题。
彼得

2
加上一个前缀_呢?
OrangeDog

12
@Peter:从字面上看也是一样。
Lightness Races in Orbit

5
@Frankie_C:任何C标准都未指定“调用方清理堆栈”:从语言角度看,这两个调用约定都不比另一个更为规范。
Ben Voigt

2
从MSVC的角度来看,“标准调用约定”就是您的选择 /Gd, /Gr, /Gv, /Gz。(也就是说,除非函数声明显式指定了调用约定,否则将使用标准的调用约定。)。您正在考虑__cdecl哪个是默认的标准调用约定。
MSalters '16

13

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

在大多数平台上,都有一个规范-通常称为ABI [应用程序二进制接口],它描述了编译器必须执行的操作才能生成具有特定名称的函数,该函数可以接受某些特定类型的参数并返回某个特定类型的值。在某些情况下,ABI可以定义多个“呼叫约定”。用于此类系统的编译器通常提供一种指示应针对特定功能使用哪种调用约定的方法。例如,在Macintosh上,大多数工具箱例程都使用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中)。因此,能够定义链接器名称对于使用此类功能至关重要。


是的,这就是答案。如果只是C和C ++,那么很难理解为什么要这样做。要理解,我们必须将事物放在静态链接的旧方法的上下文中。静态链接对Windows程序员来说似乎很原始,但这是C 无法修饰名称的主要原因。
user34660 '16

2
@ user34660:不是qutie。这是C无法强制要求存在某些功能的原因,这些功能的实现将需要修改可导出的名称,或者允许存在多个以次要特征区分的相似名称的符号。
超级猫

我们是否知道有尝试“授权”这样的事情,或者这些事情是C ++之前可用于C的扩展?
user34660 '16

@ user34660:关于“静态链接对于Windows程序员而言似乎是原始的……”,但是动态链接有时对于使用Linux的人们来说似乎是主要的PITA,在安装程序X(可能是C ++编写)时,意味着必须跟踪并安装特定的版本您的系统上已经具有不同版本的库。
jamesqf

@jamesqf,是的,Unix在Windows之前没有动态链接。我对Unix / Linux中的动态链接知之甚少,但是听起来它不像通常在操作系统中那样无缝。
user34660 '16

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一样,名称修饰会将参数字节大小修饰为函数名称,以避免堆栈损坏。

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


9

C ++编译器使用名称修饰,以允许重载函数使用唯一的符号名,否则,它们的签名将是相同的。它基本上也对自变量的类型进行编码,从而允许在基于函数的级别上实现多态。

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

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


8

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

C需要非名称混杂的函数名称。

如果C ++对其进行了修改,则它将找不到从C导出的非破坏函数,或者C将找不到C ++导出的函数。C链接器必须获得它本身期望的名称,因为它不知道它来自C ++或来自C ++。


3

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

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


1
您的意思是“允许在链接时检查其类型”。类型在编译期进行检查,但是未重整名称链接无法检查在不同的编译单元使用的声明是否同意。如果他们不同意,那就是您的构建系统从根本上被破坏了,需要修复。
cmaster-恢复莫妮卡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.