为什么C ++具有“未定义的行为”(UB),而其他语言(如C#或Java)却没有?


50

这篇Stack Overflow帖子列出了C / C ++语言规范声明为“未定义行为”的情况的相当全面的列表。但是,我想了解为什么其他现代语言(例如C#或Java)没有“未定义行为”的概念。这是否意味着编译器设计者可以控制所有可能的方案(C#和Java)或不能控制(C和C ++)?




3
然而,即使在Java规范中,这篇SO帖子也涉及未定义的行为!
gbjbaanb

“为什么C ++具有'未定义的行为'”不幸的是,这似乎是客观上难以回答的问题之一,除了陈述“因为,由于X,Y和/或Z(可能都是原因nullptr)”烦人通过编写和/或采用建议的规范来定义行为”。:c
code_dredd

我会挑战前提。至少C#具有“不安全”代码。微软写道:“从某种意义上讲,编写不安全的代码就像在C#程序中编写C代码”,并举例说明了为什么要这样做:为了访问硬件或OS并提高速度。这就是C的发明目的(地狱,他们用C 编写了OS!),因此就可以了。
彼得-恢复莫妮卡

Answers:


72

未定义的行为是仅回想起来才被认为是非常糟糕的主意之一。

最初的编译器取得了巨大的成就,并欣然欢迎对替代语言-机器语言或汇编语言编程的改进。这些问题是众所周知的,并且专门发明了高级语言来解决这些已知问题。(当时的热情非常高,以至于HLL有时被誉为“编程的结局”-好像从现在起我们只需要简单地写下我们想要的内容,编译器就可以完成所有实际工作。)

直到后来,我们才意识到新方法带来的新问题。远离运行代码的实际机器,这意味着有更多的可能性静默地做不到我们期望他们做的事情。例如,分配变量通常会使初始值不确定。这不是问题,因为如果您不想在变量中保留值,就不会分配变量,对吗?当然,可以期望专业的程序员不会忘记分配初始值,不是吗?

事实证明,使用功能更强大的编程系统可以实现更大的代码库和更复杂的结构,是的,许多程序员确实确实会不时地进行此类监督,并且由此产生的不确定行为成为一个主要问题。即使在今天,大多数安全漏洞从微小到可怕都是由于一种或另一种形式的未定义行为造成的。(原因是通常情况下,未定义的行为实际上实际上是由计算的下一个较低级别的事物定义的,了解该级别的攻击者可以利用该摆动空间来制作程序,不仅可以实现意料之外的事情,而且还可以他们打算。)

自从我们认识到这一点以来,就一直有一种驱逐高级语言中未定义行为的普遍动力,而Java对此尤为彻底(这相对容易,因为它被设计为始终在自己专门设计的虚拟机上运行)。像C这样的旧语言在不失去与大量现有代码的兼容性的情况下,很难像这样进行改进。

编辑:如前所述,效率是另一个原因。未定义的行为意味着编译器编写者在利用目标体系结构方面有很多余地,因此每种实现都可以尽可能快地实现每种功能。在今天的动力不足的机器上,这比今天的更为重要,当时程序员的薪水通常是软件开发的瓶颈。


56
我认为C社区的许多人不会同意这一说法。如果您要对C进行改造并定义未定义的行为(例如,默认初始化所有内容,为函数参数选择评估顺序等),那么行为规范的代码库将可以继续正常工作。只有今天不能很好定义的代码才会被破坏。另一方面,如果您今天没有定义,编译器将继续自由地利用CPU体系结构和代码优化方面的新进展。
Christophe

13
答案的主要部分对我来说并不真正令人信服。我的意思是,int32_t add(int32_t x, int32_t y)在C ++中编写一个安全地将两个数字相加的函数基本上是不可能的。围绕该参数的通常参数与效率有关,但经常散布一些可移植性参数(如“编写一次,在编写它的平台上运行...,在其他地方没有;-)”)。因此,大概有一个论点可能是:有些事情是不确定的,因为您不知道自己是在16位微型控制器上还是在64位服务器上(一个弱者,但仍然是一个论点)
Marco13

12
@ Marco13同意-通过制作“已定义的行为,但不一定是用户想要的内容,并且在发生时没有警告,”来代替“未定义的行为”问题,而不是“未定义的行为”,而只是玩代码律师游戏IMO 。
alephzero

9
“即使在今天,大多数安全漏洞从微小到可怕都是由于一种或另一种形式的未定义行为造成的。” 需要引用。我以为现在大多数是XYZ注射。
约书亚

34
“不明确的行为是只有回想起来才被认为是非常糟糕的主意之一。” 那是你的意见。许多(包括我自己)不共享它。
与莫妮卡

103

基本上是因为Java和类似语言的设计者不希望其语言具有未定义的行为。这是一个折衷方案-允许不确定的行为可能会提高性能,但是语言设计人员将安全性和可预测性放在了更高的优先级。

例如,如果在C中分配数组,则数据是不确定的。在Java中,所有字节必须初始化为0(或其他一些指定的值)。这意味着运行时必须经过数组(O(n)操作),而C可以​​立即执行分配。因此,对于此类操作,C总是会更快。

如果使用数组的代码在读取之前仍要填充它,那么这对于Java来说基本上是浪费时间。但是,在先读取代码的情况下,在Java中可以获得可预测的结果,而在C中可获得不可预测的结果。


19
HLL困境的绝佳展现:安全性,易用性与性能。没有灵丹妙药:双方都有用例。
Christophe

5
@Christophe坦白地说,解决问题的方法要比让UB像C和C ++那样毫无争议地更好。您可以使用一种安全,易于管理的语言,并在不安全的地方进行逃生,以便您在有益的地方申请。TBH,很高兴能用一个标记显示“插入您需要的任何昂贵的运行时机器,我不在乎,但只要告诉我所有发生的UB,就可以编译我的C / C ++程序。 。”
亚历山大

4
故意读取未初始化位置的数据结构的一个很好的例子是Briggs和Torczon的稀疏集表示(例如,请参见 codingplayground.blogspot.com/2009/03/…)。在C中,这样的集的初始化是O(1),但是O(1 n)使用Java的强制初始化。
拱D.罗宾逊

9
确实,强制数据初始化可以使坏掉的程序更容易预测,但是它不能保证预期的行为:如果算法期望在读取有意义的数据的同时错误地读取隐式初始化的零,那么这就像有错误一样阅读一些垃圾。对于C / C ++程序,通过在下运行该进程将可以看到这样的错误,该错误将valgrind准确显示未初始化值的使用位置。您不能valgrind在Java代码上使用,因为运行时会进行初始化,从而使valgrinds检查无效。
cmaster

5
@cmaster这就是为什么C#编译器不允许您从未初始化的本地读取的原因。无需运行时检查,无需初始化,仅需进行编译时分析即可。但是,这仍然是一个权衡取舍-在某些情况下,您没有很好的方法来处理可能未分配的本地分支。在实践中,我没有发现一开始这并不是一个不好的设计的情况,可以通过重新思考代码来避免复杂的分支(人类很难解析)来更好地解决这个问题,但这至少是有可能的。
a安

42

未定义的行为通过赋予编译器自由度在某些边界或其他条件下执行奇怪或意外的事情(甚至是正常的事情),从而实现了显着的优化。

看到http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

使用未初始化的变量:在C程序中,这通常被称为问题源,有很多工具可以解决这些问题:从编译器警告到静态和动态分析器。通过不需要在变量进入范围时将所有变量初始化为零(例如Java),从而提高了性能。对于大多数标量变量,这将导致很少的开销,但是堆栈数组和malloc的内存将招致存储的内存集,这可能会非常昂贵,尤其是因为通常通常会完全覆盖存储。


有符号整数溢出:例如,如果对'int'类型的算术溢出,则结果不确定。一个示例是不能保证“ INT_MAX + 1”为INT_MIN。此行为启用对某些代码很重要的某些优化类别。例如,知道INT_MAX + 1是未定义的允许将“ X + 1> X”优化为“ true”。知道乘法“不能”溢出(因为这样做是不确定的)可以将“ X * 2/2”优化为“ X”。尽管这些看起来微不足道,但通常会通过内联和宏扩展来暴露这些事情。允许使用的更重要的优化是针对“ <=”循环,如下所示:

for (i = 0; i <= N; ++i) { ... }

在此循环中,如果溢出时未定义“ i”,则编译器可以假定循环将精确地迭代N + 1次,从而可以进行广泛的循环优化。另一方面,如果将变量定义为如果在溢出时回绕,则编译器必须假定循环可能是无限的(如果N为INT_MAX,则会发生这种情况)-然后会禁用这些重要的循环优化。由于太多代码使用“ int”作为归纳变量,因此这尤其会影响64位平台。


27
当然,未定义有符号整数溢出的真正原因是,在开发C时,至少使用了三种不同的带符号整数表示形式(一个补码,两个补码,符号幅度以及可能是偏移二进制) ,并且每个都针对INT_MAX + 1给出不同的结果。使溢出a + b变为未定义状态允许add b a在每种情况下都将其编译为本机指令,而不是潜在地要求编译器模拟某种其他形式的有符号整数算术运算。
标记

2
在所有可能的行为都符合应用程序要求的情况下,允许整数溢出以松散定义的方式运行可以进行重大优化。但是,如果要求程序员不惜一切代价避免整数溢出,这些优化中的大多数将被放弃。
超级猫

5
@supercat这是在最近的语言中避免未定义行为的更常见的另一个原因-程序员的时间比CPU的时间价值高得多。由于UB,允许C进行的优化类型在现代台式计算机上根本没有意义,并且使代码推理变得更加困难(更不用说安全隐患了)。即使在性能关键的代码中,您也可以从高级优化中受益,这些高级优化在C中很难执行(甚至更加困难)。我在C#中拥有自己的软件3D渲染器,并且能够使用例如a HashSet很棒。
a安

2
@supercat:Wrt_loosely defined_,整数溢出的逻辑选择是要求实现定义的行为。那是一个现有的概念,并不是实现上的不适当负担。我怀疑,大多数人都会放弃“ 2的补全”。<<可能是困难的情况。
MSalters

@MSalters有一个简单且经过充分研究的解决方案,它既不是未定义行为也不是实现定义行为:非确定性行为。也就是说,您可以说“ x << y计算出某个类型的有效值,int32_t但我们不会说哪个”。这允许实现者使用快速解决方案,但不会充当错误的前提条件,从而允许进行时间旅行样式优化,因为不确定性受限于此操作的输出-规范保证不会明显影响内存,易失性变量等。由表情评价。...
马里奥·卡内罗

20

在C的早期,有很多混乱。不同的编译器对语言的处理方式不同。当有兴趣为该语言编写规范时,该规范需要与程序员在其编译器上所依赖的C相当向后兼容。但是其中一些细节是不可移植的,并且通常没有意义,例如,假设特定的字节序或数据布局。因此,C标准保留了许多未定义或实现特定行为的细节,这为编译器编写者留出了很大的灵活性。C ++建立在C之上,还具有未定义的行为。

Java试图成为比C ++更安全,更简单的语言。Java根据完整的虚拟机定义语言语义。这样就为未定义的行为留出了很少的空间,另一方面,这使得Java实现可能难以完成要求(例如,引用分配必须是原子的,或者整数如何工作)。Java支持潜在的不安全操作时,通常会在运行时由虚拟机检查它们(例如,某些强制转换)。


所以您是说,向后兼容是C和C ++不会摆脱不确定行为的唯一原因吗?
西西尔

3
绝对是@Sisir中较大的一个。即使在经验丰富的程序员中,当编译器更改其处理未定义行为的方式时,您也应该惊讶不了多少东西中断。(案例分析,有一点乱的时候GCC开始优化了“为this检查而回,理由是空?” this的存在nullptr是UB,因此可从来没有真正发生。)
贾斯汀时间2恢复莫妮卡

9
@Sisir,另一个重要的是速度。在C的早期,硬件比今天的异构得多。通过简单地不指定在INT_MAX中加1时发生的情况,可以让编译器执行对体系结构最快的操作(例如,一个补码系统将产生-INT_MAX,而一个补码系统将产生INT_MIN)。同样,通过不指定在读取数组末尾时将发生的情况,可以使具有内存保护功能的系统终止该程序,而无需执行该操作的系统就不需要实施昂贵的运行时边界检查。
标记

14

JVM和.NET语言很容易:

  1. 他们不必直接与硬件一起工作。
  2. 他们只需要与现代台式机和服务器系统或相当类似的设备一起工作,或者至少与为它们设计的设备一起工作。
  3. 它们可以对所有内存强加垃圾回收,并强制初始化,从而获得指针安全性。
  4. 它们由单个参与者指定,后者也提供了单个最终实现。
  5. 他们选择安全而不是性能。

尽管有一些选择的好处:

  1. 系统编程是一个完全不同的游戏,为应用程序编程毫不妥协地进行优化是合理的。
  2. 诚然,一直以来,外来硬件的数量都在减少,但是小型嵌入式系统仍然存在。
  3. GC非常不适用于不可替代的资源,并且为了获得更好的性能而交易更多的空间。而且大多数(但不是全部)强制初始化都可以被优化。
  4. 进行更多竞争具有优势,但委员会意味着妥协。
  5. 所有这些边界检查的确会加起来,即使大多数可以进行优化。空指针检查通常可以通过虚拟地址空间将访问捕获为零开销来完成,尽管优化仍然受到限制。

在提供逃生舱口的情况下,那些逃生舱口会引发全面的未定义行为。但是至少至少它们通常仅用于很少的很短的行程中,因此更易于手动验证。


3
确实。我为自己的工作使用C#编程。我每隔一段时间就会接触不安全锤之一(中的unsafe关键字或属性System.Runtime.InteropServices)。通过将这些内容留给少数知道如何调试非托管内容的程序员,并且尽可能少地减少实用性,我们就可以减少问题。自上次与性能相关的不安全之锤以来,已经过去了10多年,但有时您必须这样做,因为实际上没有其他解决方案。
约书亚

19
我经常在模拟设备的平台上工作,其中sizeof(char)== sizeof(short)== sizeof(int)== sizeof(float)==1。它也可以饱和加法(因此INT_MAX + 1 == INT_MAX) ,关于C的好处是我可以使用一个合规的编译器来生成合理的代码。如果该语言要求二进位补全,那么每次添加都会以测试和分支结束,这在DSP重点部分中是毫无用处的。这是当前的生产部件。
Dan Mills

5
@BenVoigt我们中的一些人生活在一个小型计算机可能是4k代码空间,固定的8级调用/返回堆栈,64字节RAM,1MHz时钟,数量少于1000美元的情况下的世界。现代手机是一台小型PC,出于所有目的和用途,其存储空间几乎不受限制,几乎可以看作是PC。并非所有世界都是多核的,并且缺乏严格的实时约束。
Dan Mills

2
@DanMills:这里不是在谈论带Arm Cortex A处理器的现代手机,而是在2002年左右谈论“功能手机”。是的192kB SRAM远远超过了64字节(这不是“小”而是“小”),但是192kB还没有被准确称为30年来的“现代”台式机或服务器。同样,这几天20美分将为您提供一个MSP430,它具有远远超过64字节的SRAM。
Ben Voigt

2
@BenVoigt 192kB在过去的30年中可能不是台式机,但是我可以向您保证,提供网页完全足够了,我认为按照这个词的定义将其作为服务器。事实是,对于很多通常包含配置Web服务器的嵌入式应用程序来说,这是完全合理(大量,甚至是)的内存。当然,我可能没有在其上运行亚马逊,但我可能正在这样一个核心上运行装有IOT废品的冰箱(有足够的时间和空间)。没人不需要为此而使用解释语言或JIT语言!
Dan Mills

8

Java和C#的特征是,至少在开发的早期,供应商就占主导地位。(分别是Sun和Microsoft)。C和C ++是不同的;从一开始,他们就已经有多个相互竞争的实现。C也特别在异类硬件平台上运行。结果,实现之间存在差异。标准化C和C ++的ISO委员会可以就一个大的共同点达成共识,但是在实现不同的边缘,标准为实现留下了空间。

这也是因为在偏向另一种选择的硬件体系结构上选择一种行为可能会很昂贵-字节顺序是显而易见的选择。


“大公分母”的字面意思什么?您是在谈论子集还是超集?您是否真的意味着足够的共同点?这是最小公倍数还是最大公因数?这对于我们不讲街头术语,只讲数学的机器人来说非常令人困惑。:)
tchrist

@tchrist:常见的行为是一个子集,但是这个子集非常抽象。在许多通用标准未指定的地方,必须选择实际的实现。现在,其中一些选择非常明确,因此是实现定义的,而其他选择则更加模糊。在运行时内存布局是一个例子:必须有一个选择,但目前还不清楚你会如何将其记录下来。
MSalters

2
原始的C由一个人制作。根据设计,它已经具有大量的UB。随着C的流行,事情肯定会变得更糟,但是UB从一开始就在那里。Pascal和Smalltalk的UB少得多,并且几乎同时开发。C的主要优点是非常容易移植-所有可移植性问题都委托给了应用程序员:P我什至已经将一个简单的C编译器移植到了我的(虚拟)CPU上。进行LISP或Smalltalk之类的工作要付出更大的努力(尽管我确实为.NET运行时提供了有限的原型:)。
a安

@Luaan:是Kernighan还是Ritchie?不,它没有未定义的行为。我知道,我的办公桌上有原始的AT&T钢印编译器文档。该实现做了它所做的事情。未指定行为和未定义行为之间没有区别。
MSalters

4
@MSalters Ritchie是第一个人。克尼根(Kernighan)后来才加入(人数不多)。好吧,它没有“未定义的行为”,因为该术语尚不存在。但是它确实具有与今天称为undefined相同的行为。由于C没有规范,所以即使“未指定”也很麻烦:)这只是编译器不关心的,细节由应用程序程序员决定。它并不是为了产生可移植的应用程序而设计的,只是意味着编译器易于移植。
a安

6

真正的原因在于一方面C和C ++以及另一方面Java和C#(仅用于几个示例)在意图上存在根本差异。由于历史原因,此处的许多讨论都谈论C而不是C ++,但是(您可能已经知道)C ++是C的直接后代,因此它所说的有关C的说法同样适用于C ++。

尽管它们在很大程度上被遗忘了(有时甚至会否认它们的存在),但UNIX的第一个版本是用汇编语言编写的。C的大部分(如果不是唯一的)最初目的是将UNIX从汇编语言移植到高级语言。目的的一部分是用高级语言编写尽可能多的操作系统,或者从另一个方向看待该操作系统,以最大程度地减少用汇编语言编写的数量。

为此,C需要提供与汇编语言几乎相同级别的对硬件的访问。PDP-11(例如)将I / O寄存器映射到特定地址。例如,您将读取一个内存位置,以检查是否已在系统控制台上按下了某个键。当有数据等待读取时,在该位置设置了一位。然后,您将从另一个指定的位置读取一个字节,以检索已按下键的ASCII码。

同样,如果要打印一些数据,则将检查另一个指定的位置,并且在输出设备就绪时,将数据写入另一个指定的位置。

为了支持为此类设备编写驱动程序,C允许您使用某种整数类型指定任意位置,将其转​​换为指针,然后在内存中读取或写入该位置。

当然,这有一个非常严重的问题:从1970年代初开始,并不是地球上的每台机器都具有与PDP-11相同的内存布局。因此,当您使用该整数,将其转换为指针,然后通过该指针进行读取或写入时,没有人可以为您将要获得的东西提供任何合理的保证。仅举一个明显的例子,读写可能会映射到硬件中的单独寄存器,因此(与普通内存相反)如果您编写某些东西,然后尝试将其读回,则您所读的内容可能与您编写的内容不匹配。

我可以看到一些可能性:

  1. 定义所有可能的硬件的接口-指定您可能要读取或写入的以任何方式与硬件交互的所有位置的绝对地址。
  2. 禁止该级别的访问,并下令任何想要这样做的人都必须使用汇编语言。
  3. 允许人们这样做,但是让他们自己阅读(例如)他们所针对的硬件的手册,并编写适合他们所使用的硬件的代码。

其中1似乎很荒谬,几乎不值得进一步讨论。2基本上是在抛弃语言的基本意图。这实际上使第三种选择完全是他们可以合理考虑的唯一选择。

经常出现的另一点是整数类型的大小。C采取的“位置” int应为体系结构建议的自然大小。因此,如果我正在编程32位VAX,int则可能应该是32位,但是如果我正在编程36位Univac,int则可能应该是36位(依此类推)。仅使用保证为8位大小倍数的类型为36位计算机编写操作系统可能是不合理的(甚至是不可能的)。也许我只是肤浅,但在我看来,如果我正在为36位计算机编写操作系统,则可能要使用支持36位类型的语言。

从语言的角度来看,这导致了更多未定义的行为。如果我取适合32位的最大值,那么加1会发生什么?在典型的32位硬件上,它将翻转(或可能引发某种硬件故障)。另一方面,如果它在36位硬件上运行,它将只是...添加一个。如果该语言要支持编写操作系统,则不能保证任何一种行为,您只需要允许类型的大小和溢出行为在一个与另一个之间变化即可。

Java和C#可以忽略所有这些。它们并非旨在支持编写操作系统。与他们一起,您有两种选择。一种是使硬件支持他们所需要的-因为他们要求8、16、32和64位的类型,所以只需构建支持这些大小的硬件即可。另一个明显的可能性是,该语言只能在提供所需环境的其他软件之上运行,而不管底层硬件可能想要什么。

在大多数情况下,这并不是一个非此即彼的选择。而是,许多实现都做了一点。通常,您可以在操作系统上运行的JVM上运行Java。通常,操作系统是用C编写的,而JVM是用C ++编写的。如果JVM在ARM CPU上运行,则该CPU包括ARM的Jazelle扩展的机会非常大,可以根据Java的需求更紧密地定制硬件,从而减少了在软件中的需求,并且Java代码运行得更快(或更少)。慢慢来)。

摘要

C和C ++具有未定义的行为,因为没有人定义可接受的替代方法,这些替代方法使它们可以执行预期的操作。C#和Java采用了不同的方法,但是该方法与C和C ++的目标相差甚远(如果有的话)。特别是,似乎都没有提供在大多数随意选择的硬件上编写系统软件(例如操作系统)的合理方法。两者通常都依靠现有系统软件(通常用C或C ++编写)提供的功能来完成其工作。


4

C标准的作者希望读者认识到他们认为显而易见的东西,并在已发布的基本原理中有所提及,但并没有直截了当地说:委员会不需要命令编译器作家来满足其客户的需求,因为客户应该比委员会更了解他们的需求。如果很明显,期望某些平台的编译器以某种方式处理构造,则没有人应该关心标准是否说该构造调用未定义行为。该标准未能强制要求兼容的编译器以任何方式有用地处理一段代码,这并不意味着程序员应该愿意购买不这样做的编译器。

这种语言设计方法在编译器作者需要将其产品出售给付费客户的世界中非常有效。在编译器编写者与市场影响隔离的世界中,它完全崩溃了。令人信服的是,是否存在适当的市场条件来像他们指导1990年代流行的语言一样来操纵一种语言,甚至更怀疑任何理智的语言设计师是否希望依靠这种市场条件。


我觉得您在这里已经描述了一些重要的内容,但是却使我无所适从。你能澄清你的答案吗?特别是第二段:它说现在的条件和以前的条件不同,但是我不明白。到底发生了什么变化?另外,现在的“方式”与以前有所不同。也许也解释一下?
anatolyg

4
您的广告系列似乎将所有未定义的行为替换为未指定的行为,或者更受限制的行为仍在继续。
重复数据删除器

1
@anatolyg:如果尚未阅读,请阅读已发布的C Rationale文档(在Google中输入C99 Rationale)。第11页的23-29行讨论了“市场”,第13页的5-8行讨论了有关可移植性的意图。您如何看待商业编译器公司的老板会如何反应,如果编译器作者告诉程序员,他们抱怨优化器破坏了代码,认为其他所有编译器都对代码进行了有效处理,则其他编译器会对其进行有效处理,因为该代码执行了标准未定义的操作,并且拒绝支持它,因为这将促进两岸的持续...
supercat

1
...使用这样的构造?这种观点在clang和gcc的支持板上显而易见,并且已阻碍了内在函数的开发,该内在函数比破碎的gcc和clang要支持的语言更容易,更安全地促进优化。
超级猫

1
@supercat:您在浪费时间向编译器供应商抱怨。为什么不将您的疑虑传达给语言委员会?如果他们同意您的意见,则会发出勘误表,您可以用它来击败编译器团队。而且该过程比开发该语言的新版本要快得多。但是,如果他们不同意,那么您至少会得到实际的原因,而编译器作者只是要重复(一遍又一遍)“我们没有指定代码被破坏,该决定是由语言委员会做出的,我们遵循他们的决定。”
Ben Voigt

3

C ++和c都具有描述性标准(无论如何都是ISO版本)。

仅存在于解释语言如何工作并提供关于语言是什么的单一参考。通常,由编译器供应商和库编写者来带路,并且一些建议会包含在主要的ISO标准中。

Java和C#(或我认为是Visual C#)具有规定性标准。他们会提前明确告诉您该语言的内容,其工作方式以及允许的行为。

更为重要的是,Java实际上在Open-JDK中具有“参考实现”。(我认为Roslyn可算作Visual C#参考实现,但找不到该源。)

在Java的情况下,如果标准中有任何歧义,Open-JDK会采用某种方式。Open-JDK的操作方式是标准的。


情况比这更糟:我认为委员会从未就应描述性还是规定性达成共识。
超级猫

1

未定义的行为使编译器可以在各种体系结构上生成非常有效的代码。Erik的答案提到了优化,但超出了这一范围。

例如,带符号的溢出是C语言中未定义的行为。在实践中,期望编译器生成一个简单的带符号的附加操作码供CPU执行,并且该行为将与特定的CPU一样。

这使C在大多数体系结构上都能很好地执行并产生非常紧凑的代码。如果标准指定带符号的整数必须以某种方式溢出,那么行为不同的CPU将需要更多的代码来生成简单的带符号的加法。

这就是C中许多未定义行为的原因,也是为什么int系统之间的大小之类的东西有所不同的原因。Int取决于架构,通常选择大于的最快,最有效的数据类型char

早在C是新的时候,这些考虑就很重要。计算机的功能较弱,通常处理速度和内存有限。在性能确实很重要的地方使用了C,并且期望开发人员了解计算机如何足够好地运行,以了解这些未定义的行为在其特定系统上实际上将是什么。

诸如Java和C#之类的较新语言更喜欢消除未定义行为而不是原始性能。


-5

从某种意义上说,Java也有它。假设您给Arrays.sort提供了不正确的比较器。它可以抛出检测到它的异常。否则,它将以某种无法保证特定的方式对数组进行排序。

同样,如果您从多个线程修改变量,结果也是不可预测的。

C ++只是走得更远,以使更多未定义的情况(或者Java决定定义更多的操作)并为其命名。


4
这不是我们在这里讨论的那种不确定的行为。“不正确的比较器”有两种类型:一种定义总顺序,另一种则不。如果提供的比较器一致地定义了项目的相对顺序,则行为是明确定义的,而不仅仅是程序员想要的行为。如果您提供的比较器在相对顺序方面不一致,那么行为仍然是明确定义的:sort函数将引发异常(这也可能不是程序员想要的行为)。
标记

2
至于修改变量,通常不将竞争条件视为未定义的行为。我不知道Java如何处理对共享数据的分配的详细信息,但是了解该语言的一般原理,我敢肯定它必须是原子的。a如果可以从中获得51或73,则同时将53和71分配给它将是未定义的行为,但是,如果只能获得53或71,则它是定义明确的行为。
标记

@Mark如果数据块大于系统的本机字长(例如,在16位字长的系统上为32位变量),则可能需要要求分别存储每个16位部分的体系结构。(在这种情况下,SIMD是另一种潜在的情况。)在那种情况下,除非编译器特别注意确保原子执行,否则即使是简单的源代码级分配也不一定是原子的。
的CVn
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.