首先,我会注意到,尽管我在这里只提到了“ C”,但实际上同样适用于C ++。
提到戈德尔的评论部分(但仅部分)正确。
当您深入了解它时,C标准中未定义的行为在很大程度上只是指出了标准要定义的内容与未定义的内容之间的界限。
哥德尔定理(有两个)基本上说不可能定义一个数学系统,该数学系统可以(通过其自己的规则)被证明是完整且一致的。您可以制定规则使其完整(例如,他处理的是自然数的“正常”规则),否则就可以证明其一致性,但不能两者兼而有之。
对于诸如C之类的东西,它并不直接适用-在大多数情况下,对于大多数语言设计人员而言,系统完整性或一致性的“可证明性”并不是优先考虑的问题。同时,是的,他们可能(至少在某种程度上)受到了影响,因为他们知道定义一个“完美”的系统是不可能的,而这个系统应该是完整且一致的。知道这样的事情是不可能的,这可能会使其后退,稍作呼吸并决定他们将要定义的范围变得容易一些。
冒着再次被指责的风险,我将C标准描述为(部分)受两个基本思想支配:
- 该语言应支持尽可能广泛的各种硬件(理想情况下,所有“合理”的硬件都应降至合理的下限)。
- 该语言应支持针对给定环境编写尽可能广泛的各种软件。
第一个意思是,如果有人定义了一个新的CPU,则应该有可能为此提供良好,可靠,可用的C实现,只要设计至少合理地接近一些简单的准则即可-基本上,遵循冯·诺依曼(Von Neumann)模型的一般顺序,并至少提供一些合理的最小内存量,足以允许C实现。对于“托管”实现(在操作系统上运行的实现),您需要支持某种与文件相当接近的概念,并且其字符集应具有一定的最小字符集(需要91个)。
第二种意味着应该可以编写直接操作硬件的代码,因此您可以编写诸如引导加载程序,操作系统,无需任何操作系统即可运行的嵌入式软件之类的东西。最终在这方面存在一些限制,因此几乎任何限制实际的操作系统,引导加载程序等可能至少包含一点用汇编语言编写的代码。同样,即使是小型的嵌入式系统,也可能至少包括某种预编写的库例程,以允许访问主机系统上的设备。尽管很难定义精确的边界,但其目的是使对此类代码的依赖性保持最小。
语言中未定义的行为在很大程度上由语言支持这些功能的意图所驱动。例如,该语言允许您将任意整数转换为指针,并访问该地址处的任何内容。该标准不会试图说出您执行操作时将发生的情况(例如,即使从某些地址进行读取也可能具有外部可见的影响)。同时,它不会尝试阻止您执行此类操作,因为您需要使用某种可以使用C编写的软件。
还有一些其他设计元素驱动的不确定行为。例如,C的另一目的是支持单独的编译。例如,这意味着您打算使用一个链接器将各个部分“链接”在一起,该链接器大致遵循我们大多数人所看到的链接器的常用模型。特别是,无需知道语言的语义,就可以将单独编译的模块组合成一个完整的程序。
还有另一种类型的不确定行为(在C ++中比C更常见),其存在的原因仅仅是由于编译器技术的局限性-我们基本上知道的是错误,并且可能希望编译器将其诊断为错误,但是考虑到当前对编译器技术的限制,可以在任何情况下对它们进行诊断都令人怀疑。其中许多是由其他要求(例如,单独编译)驱动的,因此,这主要是在平衡相互冲突的要求之间达成的问题,在这种情况下,委员会通常选择支持更大的功能,即使这意味着缺乏诊断可能的问题,而不是限制确保诊断所有可能问题的能力。
这些意图上的差异驱动了C与Java或Microsoft的基于CLI的系统之间的大部分差异。后者被明确地限制为使用一组更为有限的硬件,或者要求软件模拟它们所针对的更具体的硬件。他们还特别打算防止对硬件的任何直接操纵,而是要求您使用JNI或P / Invoke之类的东西(以及用C语言编写的代码)来进行这种尝试。
回到戈德尔定理,我们可以得出类似的结论:Java和CLI选择了“内部一致”的选择,而C选择了“完全”的选择。当然,这是一个非常粗略的比喻-我怀疑任何人的企图的正式证明无论是内部一致性或在任何情况下完整。但是,一般概念确实与他们所选择的内容非常吻合。