C99不支持函数重载的原因是什么?

7

显然(至少根据 gcc -std=c99),C99不支持函数重载。通常,不支持C中的某些新功能是出于向后兼容性的考虑,但在这种情况下,我无法想到任何一种情况可以证明函数重载会破坏向后兼容性。为什么没有包含这个基本特性?


2
如果你想使用函数重载的C语言,你可能会对C++感兴趣。 - SingleNegationElimination
@William,这是我在C语言理由文档中找到的内容:“如果已知参数不会被使用,则省略调用中的尾随参数的现有做法一直受到反对。由于省略这些参数会在调用和声明之间创建不等价关系,因此在这种情况下的行为是未定义的,最大程度地可移植的程序将避免使用此用法。因此,实现可以自由地为固定参数列表实现函数调用机制,如果提供了错误数量或类型的参数,则可能会(严重)失败。” - Johannes Schaub - litb
(来自http://www.lysator.liu.se/c/rat/c3.html#3-3-2-2) - Johannes Schaub - litb
3个回答

30
为了理解为什么在C中不太可能看到重载,可能有助于更好地了解C++中如何处理重载。编译代码之后,但在准备运行之前,必须链接中间的对象代码。这将把已编译函数和其他对象的粗略数据库转换为一个准备好加载/运行的二进制文件。这一额外步骤非常重要,因为它是可编译程序可用的模块化原理机制。这一步允许您从现有库中获取代码并将其与自己的应用逻辑混合使用。在此阶段,对象代码可以用任何语言编写,具有任何组合的特性。为了实现这一点,需要某种约定,以便链接器能够在另一个对象引用它时选择正确的对象。如果您使用汇编语言进行编码,则在定义标签时使用该标签,因为假定您知道自己在做什么。在C中,函数成为链接器的符号名称,因此当您编写时。
int main(int argc, char **argv) { return 1; }

编译器提供了一个目标代码存档,其中包含一个名为main的对象。这很有效,但意味着您不能有两个同名的对象,因为链接器无法决定应该使用哪个名称。链接器不知道任何关于参数类型的信息,对代码的了解也非常有限。 C++通过直接将附加信息编码到符号名称中来解决这个问题。返回类型、参数的数量和类型、参数的引用类型、是否为const等都添加到符号名称中,并在函数调用点引用。由于链接器可以确定函数调用是唯一的,因此它不必知道正在发生什么。这样做的缺点是符号名称看起来与原始函数名称完全不同。特别是,几乎无法预测重载函数的名称,以便您可以链接到它。要链接到外部代码,可以使用extern "C",这使得这些函数遵循符号名称的C风格,但当然您不能重载这样的函数。这些差异与每种语言的设计目标有关。C面向可移植性和互操作性。 C努力做出可预测且兼容的事情。 C++更加强调构建丰富而强大的系统,而不是与其他语言交互。我认为C不太可能追求任何会产生与C++一样难以交互的代码的特性。

编辑: Imagist 问道:

如果你将 int main(int argc, char** argv) 解析为 main-int-int-char **,而不是 main(并且这是标准的一部分),那么与函数交互是否会更不可移植或更困难?我在这里看不到问题。实际上,我认为这给了你更多信息(可以用于优化等)

为了回答这个问题,我再次转向 C++ 及其处理重载的方式。C++ 使用几乎完全按照描述的机制,但有一个注意点。C++ 没有标准化某些部分应该如何实现,然后继续建议某些省略的后果。特别地,C++ 有一个丰富的类型系统,包括虚类成员。如何实现此功能留给编译器编写者,vtable 解析的详细信息对函数签名有很大影响。因此,C++ 故意建议编译器编写者使名称混淆在编译器之间或具有这些关键特性的不同实现的相同编译器之间不兼容。

这只是深层问题的症状,即尽管像 C++ 和 C 这样的高级语言具有详细的类型系统,但低级机器代码完全没有类型。任意丰富的类型系统都建立在机器级别提供的无类型二进制之上。链接器无法访问高级语言可用的丰富类型信息。链接器完全依赖编译器处理所有类型抽象并生成适当的无类型对象代码。

C++通过在编码对象名称中编码所有必要的类型信息来实现这一点。然而,C有一个显著不同的重点,旨在成为一种可移植的汇编语言。因此,C更喜欢在声明名称和生成的对象符号名称之间保持严格的一对一对应关系。如果C混淆了它的名称,即使以标准化和可预测的方式进行,您也必须付出巨大的努力来将更改后的名称与所需的符号名称匹配,否则您就必须像在C++中一样关闭它。这种额外的努力几乎没有任何好处,因为与C ++不同,C的类型系统相当简单。

同时,定义几个类似命名的C函数,它们仅因其作为参数的类型而异,实际上是一种标准做法。如果想要详细了解这一点,请查看OpenGL namespace的示例。


1
如果您将int main(int argc, char** argv)解析为main-int-int-char **而不是main(并且这是标准的一部分),那么与函数交互是否真的会更困难或不太便携? 我在这里看不到问题。 实际上,我认为这为您提供了更多信息(可用于优化等)。 - Imagist
1
@Imagist - 这对于一门语言来说可能是有用的。但它也会立即使所有旧的 C 库无法被新的 C 代码使用,以及所有新的 C 库无法被旧的 C 代码使用。 - Chris Lutz
4
对于 C 语言来说,每隔10年就需要更改代码是非常频繁的。可以肯定的是,采用该语言的人数会非常少。 - SingleNegationElimination
如果只允许可扩展为内联的函数进行函数重载,会怎样呢?这种方法不需要名称修饰,并且可以明确定义语义为“第一匹配”。如果可以对参数施加约束(例如在某个范围内可以解决为常量),那么这将特别方便。例如,如果可以将x解析为0、1或2,或将y解析为0、1、2或3,则计算(x的y次方)的功能可能具有专门的内联扩展;如果没有这些分辨率之一起作用,则内联扩展为正常的函数调用。 - supercat
@supercat:你所描述的内容是否有什么无法通过只使用带有extern "C"导出符号的C++编译单元来实现的?C++是一种非常功能丰富的语言。它就像C语言,但具有一些额外的类型安全特性! - SingleNegationElimination
显示剩余2条评论

19
当你编译C源码时,符号名称将保持不变。如果使用函数重载,应提供名称混淆技术以防止名称冲突。因此,与C++一样,编译后的二进制文件会有机器生成的符号名称。
另外,C没有严格的类型限制。在C中,许多东西可以隐式地相互转换。重载解析规则的复杂性可能会在这种语言中引入混淆。

在我看来,对此的回应很简单:提供一个标准的名称混淆技术来保留类型信息。像 foo(bar) -> foo-barfoo(baz) -> foo-baz 这样的东西并不难实现。 - Imagist
3
实际上我提到了三点:1. 弱类型 2. 增加的复杂性 3. 名称冲突。你可以构建一个标准的名称混淆技术,但是如何使其与已经在C89下编译的库兼容?如果你在C99中构建一个库,又该如何在C89中使用它? - Mehrdad Afshari

5
许多语言设计师,包括我在内,认为函数重载与C的隐式转换相结合会导致代码难以理解。可以看看关于C ++积累的知识库作为证据。
总的来说,C99旨在成为一个与现有实践基本兼容的温和修订版。重载将是一个相当大的改变。

1
许多编程语言的设计者,包括我在内,认为通过指针直接访问内存可能导致代码难以理解。显然,解决方案并不是从语言中排除指针(至少在C语言的情况下不是)。 - Imagist
1
@Imagist。我同意你的观点。我宁愿用Modula-3编写操作系统,因为它提供了更好的机制来控制和理解指针代码。但是指针程序确实需要编写,阅读Dennis Ritchie在HOPL-II上的文章后,我相信C语言在这方面做得非常好,它将永远不会被取代。但人们应该停止在它上面编写应用程序!(多亏了Java,许多人已经这样做了。) - Norman Ramsey
我认为C语言的隐式转换已经让代码变得很难理解了,不需要再加上重载!+1 - SingleNegationElimination

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接