“未定义行为”是否真的允许发生“任何”事情?

105
“未定义行为”的典型谬误例子是“鼻涕妖”,这是一种物理不可能性,无论C和C++标准允许什么。
因为C和C++社区往往非常强调未定义行为的不可预测性以及编译器在遇到未定义行为时可以使程序做任何事情的想法,我原以为标准对于未定义行为的行为没有任何限制。
但是,C++标准中的相关引用似乎是: > [C++14:defns.undefined]:[..]可接受的未定义行为范围从完全忽略情况及带有不可预测结果的行为,到在翻译或程序执行期间表现出特定环境的文档化方式(无论是否发出诊断消息),到终止翻译或执行(附带发出诊断消息)[...]。
实际上,这里指定了一小组可能的选项:
  • 忽视情况——是的,标准继续说这将会有“不可预测的结果”,但这并不意味着编译器会插入代码(我假设这是产生鼻妖的先决条件)。
  • 表现出环境特征的已记录方式——这听起来相对无害。(我肯定没有听说过任何鼻妖的已记录案例。)
  • 终止翻译或执行——并附带诊断信息。如果所有未定义的行为都能如此友好地表现就好了。

我认为在大多数情况下,编译器选择忽略未定义的行为;例如,在读取未初始化的内存时,插入任何代码以确保一致性行为可能会适得其反。我想奇怪类型的未定义行为(例如“时间旅行”)将属于第二类——但这要求这些行为被记录并“具有环境特征”(所以我猜鼻妖只会被恶魔计算机产生?)。

我是否误解了定义?这些只是可能构成未定义行为的示例,而不是一份全面的选项清单吗?声称“任何事情都可能发生”仅仅是忽略情况的意外副作用吗?
两个澄清小点:
  • 我认为对于原始问题,对于大多数人来说都很清楚,但我仍然会说明:我确实意识到“鼻妖”是开玩笑的。
  • 请不要写一个(另一个)回答来解释UB允许特定于平台的编译器优化,除非您还解释它如何允许实现定义行为所不允许的优化。

这个问题并不是讨论未定义行为的优缺点的论坛,但它似乎变成了这样的讨论。无论如何,对于那些认为这是一个重要话题的人来说,这个关于假想C编译器没有未定义行为的帖子可能会更有兴趣。

3
这实际上与操作系统的差异有关。例如,内存是否初始化为零?是否启用了堆栈保护?是否使用地址随机化?规范没有明确说明,因为可能存在不同的行为,包括“grue”(一个虚构的词语)。 - Elliott Frisch
15
未定义行为一直都是一个笑话,直到 有人被烧毁 - Paul
5
我会尽力进行翻译,以下是翻译的结果: 与其说“鼻部恶魔”,不如说未定义的行为可能会召唤你的前任。 - Brian Bi
8
"可允许的未定义行为范围包括完全忽略情况,可能会导致不可预测的结果。" 我认为这基本涵盖了一切可能发生的情况。 - juanchopanza
9
一般英语用法上,如果有人说“我们的旅行社提供从澳大利亚到土耳其再到加拿大的假期”- 这并不意味着这些国家是唯一可选的;该列表不是详尽无遗的暗示。 - Tony Delroy
显示剩余36条评论
9个回答

84

是的,它允许任何事情发生。该注释只是举了一些例子。定义相当清晰:

未定义行为:国际标准对此不强制执行任何要求的行为。


常见的混淆点:

您应该了解“没有要求”意味着实现不需要将行为定义为未定义或做出奇怪/非确定性操作!

按照C++标准,实现完全可以记录一些明智的行为并相应地表现。1因此,如果您的编译器声称在有符号溢出时环绕,则逻辑(正常?)会指示您可以依赖该行为在该编译器上。只要另一个编译器没有声称这样做,就不要期望其表现相同。

1甚至允许记录一件事并做另一件事。那很愚蠢,你可能会把它扔进垃圾桶里——你为什么要相信文档向你撒谎的编译器呢?——但这并不违反C++标准。


3
与现代编译器的行为相比较,似乎很有趣。然而,将预期反映短语意思的规范示例与现代编译器的行为进行比较是有益的。我从未见过任何证据表明标准的作者们打算让编译器使用未定义行为来确定程序接收到哪些输入或没有接收到哪些输入。 - supercat
17
@supercat 的示例和注释并非规范性内容。 - T.C.
7
很明显,这个意图本质上是要“确定程序不会收到哪些输入”,只是当时编译器没有那么先进。例如,当n等于或超过x的类型宽度时,'x<<n'完全是UB (未定义行为),原因在于编译器可以假设'n'不会达到该值,而不必实现复杂和昂贵的逻辑来处理这种情况。从概念上讲,进行此优化并执行基于UB的其他更高级DCE并没有区别。 - R.. GitHub STOP HELPING ICE
2
除非我们知道base是一个长度为length的对象的起始位置,否则用true替换p >= base && p < base+length会更有效率,而且也符合规范。这比实际操作位和其他东西要快得多。无限次的减速肯定是不切实际的! - Yakk - Adam Nevraumont
4
@supercat 不是说你使用"实际的"这个词太过模糊了吗?当然,你会在看到时知道它的意思。现今的编译器都可以自由地声明它们的指针存在于一个平面内存空间中。有些编译器选择不做(许多)标准以外的保证,并利用这种自由。其他编译器则不一样。实际的程序员要么必须将他们的代码限制在一个版本的一个编译器中,使用一个标准,要么根据标准编写代码。尽量只在有大量警告和回报巨大的情况下涉及未定义行为,最好断言编译器的版本。 - Yakk - Adam Nevraumont
显示剩余23条评论

24

未定义行为的一个历史目的是允许某些操作在不同平台上具有不同的潜在有用效果。例如,在 C 的早期阶段,假设

int i=INT_MAX;
i++;
printf("%d",i);
一些编译器可能保证代码将打印某个特定值(对于二进制补码机器,通常是INT_MIN),而其他编译器则保证程序在未达到printf语句之前终止。根据应用程序的要求,这两种行为都可能有用。将行为定义为未定义意味着,如果在可靠地捕获溢出的平台上运行,则可以放弃溢出检查,其中异常程序终止是溢出的可接受后果,但生成似乎有效但错误的输出不会被放弃。在溢出情况下不允许异常终止的应用程序,但生成算术不正确的输出将被放弃,如果在不捕获溢出的平台上运行,则可以放弃溢出检查。
然而,最近一些编译器作者似乎已经开始比赛,看看谁可以最有效地消除任何不受标准规定存在的代码。例如,给定...
#include <stdio.h>

int main(void)
{
  int ch = getchar();
  if (ch < 74)
    printf("Hey there!");
  else
    printf("%d",ch*ch*ch*ch*ch);
}

一个超现代的编译器可能会得出结论,如果ch大于或等于74,则计算ch*ch*ch*ch*ch将产生未定义的行为,并且因此程序应无条件地打印"Hey there!",而不管键入了什么字符。


3
哇,我们是如何从“有潜在用处”发展到现在这种情况的呢?在这种情况下,许多C++社区似乎坚决反对任何试图确定某些编译器在遇到允许UB的情况时的确切行为的尝试,并解释说“这并不重要,你的程序已经存在UB了”。 - Kyle Strand
12
不,这是关于可移植性的。我们现在生活在一个互联的时代,软件分发速度快得超乎想象。我们不再为地下室里那台尘封的超级计算机编写程序,至少大多数人已经不是了。实际上,这是编程中几十年来的范式转变;现在编写符合标准的严谨代码具有明显的实际好处(理想情况下,我们一直都应该这样做),而工具链编写者可以利用这一点来生产真正快速和高效的编译器。为什么不呢? - Lightness Races in Orbit
5
如果目标是要有一个可用的可移植语言,委员会应该认识到存在一些不同的变体(例如方言),其中可以使用p>= object.base && p<object.base+object.size来测试p是否是对象的一部分,但并非所有平台都能实现,而其他平台则不允许这种比较,但可以在更多平台上实现。此外,委员会还应定义一些数据类型,如果受支持,将需要在所有平台上保持一致行为。目前,C语言具有两种不同的32位有符号整数类型... - supercat
4
...还有两种不同的无符号32位整数类型。在所有uint32_t值都可以表示为int的平台上,两个uint32_t值相减会得到一个有符号结果。在一些uint32_t值无法表示为int的平台上,相减会得到一个uint32_t结果。这两种类型被称为uint32_t,但它们的语义非常不同。同样,在int大于32位的平台上,对int32_t进行递增将总是具有定义行为。在int恰好为32位的平台上,对int32_t进行递增可能会导致未定义行为(UB)。 - supercat
2
我认为很难证明 UB 的存在是有道理的;它的缺点比好处还多。 - Giorgi Moniava
显示剩余8条评论

17

挑剔: 你没有引用标准。

这些是生成C++标准草案的来源。除非被C++工作组(ISO/IEC JTC1/SC22/WG21)正式采纳,否则不应将这些源视为ISO出版物,也不应从中生成文件。

解释: 根据ISO/IEC第2部分指南,注释不属于规范性

文档文本中集成的说明和示例只能用于提供旨在帮助理解或使用文档的附加信息。 它们不得包含要求(“必须”;请参见3.3.1和表H.1)或任何被认为对文档使用至关重要的信息,例如说明(命令式;请参见表H.1)、建议(“应该”;请参见3.3.2和表H.2)或许可(“可以”;请参见表H.3)。注释可以写成事实陈述。

我加粗的部分。这就排除了“全面列出选项”的可能性。然而,举例子确实算是“旨在协助理解文档的附加信息”。

请记住,“鼻妖”梗不应被字面理解,就像用气球来解释宇宙膨胀如何运作一样,在物理现实中没有任何真实性质。这只是为了说明当允许做任何事情时,讨论“未定义行为”应该怎么做是愚蠢的。是的,这意味着在外太空中并不存在真正的橡皮筋。


1
回复:吹毛求疵:在另一个答案中看到从2003标准中引用了这个语句,我被激发去查找了草案标准中的这个语句。措辞看起来非常相似,所以我认为这个措辞至少在过去十年里没有太大变化,这就是为什么我感到放心从草案中引用它(而且它是免费和在线的)。 - Kyle Strand
4
这些标准的最终版本并非免费提供,需要支付高昂的门槛费用,因此无法提供链接。但是,最终草案在所有相关技术和语言方面与最终版本完全相同。如果没有这些草案,引用和参考该标准实际上是不可能的。那么你更喜欢:1)从最终(在这个方面相同的)草案中引用,还是2)不引用,因此只是没有任何根据地陈述?(而你怎么知道太空中没有橡皮筋呢?) - too honest for this site
请注意,C标准使用“shall”一词的方式与几乎所有其他标准的用法不同。在大多数标准中,违反约束将使实现非符合性,但这并不适用于C标准。违反约束的程序不能严格符合,但标准认为是“符合”的,并且明确旨在不贬低那些不受其要求限制但行为由某些实现有用地定义的非可移植程序。 - supercat

14
在每个C和C++标准中,“未定义行为”的定义本质上是,标准不对发生什么做任何要求。
是的,这意味着允许任何结果。但是没有特定的结果需要发生,也没有要求不发生的结果。如果您有编译器和库,在回应特定未定义行为实例时始终产生特定行为,也无关紧要。这样的行为并不是必需的,甚至在将来的修复版本中可能会发生变化 - 而且编译器在每个C和C++标准的版本中仍然是完全正确的。
如果您的主机系统具有硬件支持,例如插入在鼻孔中的探针连接形式,则未定义行为的发生有可能导致不良的鼻部效果。

6
从历史上看,事实上C标准没有定义某种行为,并不意味着实现不应该这样做。实际上,一些触发未定义行为的事情是因为在C标准通过之前,不同的实现提供了两个(或更多)相互矛盾的保证,程序依赖于这两个保证中的任何一个。 - supercat
1
@Peter:问题不仅仅是让人们同意一个标准。C语言之所以能够繁荣发展的原因之一在于,各种平台的编译器可以为其用户提供不同的性能、可用性和稳健性之间的权衡,以满足这些平台用户的需要。 - supercat
2
一个很好的例子是取消引用空指针。在SPARC上,这会给你一个值为0的结果,而写入操作则会默默地丢弃结果。但在MS-DOS上,该位置保存着中断表。试着去解决这个问题吧。 - MSalters
3
但我相信标准单独定义了“实现定义”的行为,这与你所说的是相符的。例如,对于有符号值而言,>> 的操作是实现定义的(这意味着必须发生某些在编译器文档中一致且被定义的事情),而 << 的操作则是未定义的(这意味着任何事情都可能发生,也没有人需要定义它)。不要怪编译器编写者;很明显,现代标准的编写者对目前发生的情况非常满意,否则他们只需将所有当前未定义的行为变为实现定义的就行了! - Muzer
1
指令(这可能是不切实际的,因为这些问题可能会受到寄存器分配的影响,而寄存器分配又可能受到许多其他因素的影响)。我建议标准明确禁止程序执行某些操作的地方(通常在语法或结构层面),如果标准打算禁止某些操作,它本可以这样做。 - supercat
显示剩余4条评论

8

我想回答你的一个问题,因为其他答案已经很好地回答了一般性问题,但没有解决这个问题。

“忽略情况——是的,标准继续说这将会有‘不可预测的结果’,但这并不意味着编译器会插入代码(我认为这将是鼻妖出现的前提条件)。”

在以下情况下,即使编译器不插入任何代码,使用合理的编译器也可以很有道理地预期到鼻妖的出现:

if(!spawn_of_satan)
    printf("Random debug value: %i\n", *x); // oops, null pointer deference
    nasal_angels();
else
    nasal_demons();

编译器可以证明*x是一个空指针引用,作为某些优化的一部分,完全有权利说“好吧,我看到他们在这个if分支中解引用了一个空指针。因此,在该分支的一部分中,我可以做任何事情。所以我可以进行优化:"

if(!spawn_of_satan)
    nasal_demons();
else
    nasal_demons();

"接着,我可以将其优化为以下内容:"
nasal_demons();

您可以看到,在正确的情况下,这种技术对于优化编译器非常有用,但也可能会导致灾难。我曾经看过一些案例,实际上在某些情况下,优化能够优化这种情况是很重要的。当我有更多时间时,我可能会试着找出这些案例。
编辑:我记得一个例子,如果您非常频繁地检查指针是否为NULL(可能在内联的帮助函数中),即使在已经解除引用且未更改它之后,这种优化是有用的。优化编译器可以看到您已经取消了引用,因此可以优化掉所有“是NULL”的检查,因为如果您已经取消了引用并且它确实为空,任何事情都可以发生,包括不运行“是NULL”检查。我相信类似的论证也适用于其他未定义行为。

1
是的,我意识到如果用户在某些情况下要求鼻妖,那么如果程序存在未定义行为(UB),它们可能会在意想不到的情况下被召唤出来。当我说某些UB行为需要插入代码时,我指的是完全意外的行为,这些行为在你的代码中并没有明确写入。 - Kyle Strand
一定存在某些边角案例,其中生成完全利用未定义行为的新代码更加高效。我稍后会找出我读过的一些文章。 - Muzer
我很想看看,但请记住,原问题可以被重新表述为“标准是否真的允许在UB中插入任意代码”,这个问题已经得到回答了。 - Kyle Strand
Mehrdad的回答表明,插入代码是允许的。 - Kyle Strand
1
@Muzer:事实是,C标准定义的行为集合不足以高效地执行许多操作,但绝大多数编译器历史上都提供了一些扩展,允许程序比通常情况下更高效地满足其要求。例如,在某些平台上,给定int a,b,c,d;,当值在范围内时,a*b>c*d的实现最有效的计算方式将是(int)((unsigned)a*b)>(int)((unsigned)c*d),而在其他平台上,最有效的函数将是... - supercat
显示剩余8条评论

8
首先,需要注意的是,未定义行为不仅指用户程序的行为是未定义的,编译器的行为也是未定义的。同样,未定义行为不是在运行时遇到的,而是源代码的属性。
对于编译器编写者来说,“行为未定义”意味着“你不必考虑这种情况”,甚至可以假设“没有源代码会产生这种情况”。当编译器遇到未定义行为时,可能会有意或无意地执行任何操作,并仍然符合标准。因此,如果您允许访问您的鼻子...
然后,并不总是能够知道程序是否存在未定义行为。 例如:
int * ptr = calculateAddress();
int i = *ptr;

想要知道这是否可能成为未定义行为,需要了解calculateAddress()返回的所有可能值,但在一般情况下这是不可能的(见“停机问题”)。编译器有两种选择:

  • 假设ptr始终具有有效地址
  • 插入运行时检查以保证特定行为

第一种选择可以生成快速的程序,并将避免不良影响的负担放在程序员身上,而第二种选择可以生成更安全但更慢的代码。

C和C++标准保留了这个选择,大多数编译器选择第一种,而例如Java则强制使用第二种。


为什么行为不是实现定义,而是未定义的?

实现定义的意思是(N4296,1.9§2):

某些抽象机器的方面和操作被描述为在本国际标准中实现定义(例如,sizeof(int))。这些构成了抽象机器的参数。每个实现都应包括描述其特征和行为的文档,在这些方面上。这样的文档应定义对应于该实现的抽象机器实例(以下简称“相应实例”)。

强调是我的。换句话说:编译器编写者必须记录源代码使用实现定义特性时机器代码的确切行为。

在程序中写入一个随机的非空无效指针是最不可预测的事情之一,因此这将需要降低性能的运行时检查。
在我们拥有内存管理单元(MMUs)之前,通过写入错误的地址,你可以摧毁硬件,这非常接近鼻子恶魔;-)

不仅仅是空指针,有符号溢出或除以零也是通常无法预见的编译时错误。抱歉,我没有理解你在前两句话中的意思。 - alain
是的,我意识到Rust并没有绕过停机问题,但空指针解引用是最常见的错误类型之一,而且这也是你所举的例子。我的前两句话基本上是在说你的回答并没有真正解决UB的问题;是的,在C/C++中,解引用空指针是UB,但它也可能是实现定义的,这是不同的(也不太宽容)。 - Kyle Strand
1
是的,如果我没记错的话,Stroustrup后悔引入了空指针。这是一篇很棒的文章,解释了UB的优点:http://blog.regehr.org/archives/213 - alain
2
编译器的行为并非未定义。编译器不应该格式化您的硬盘,或发射导弹,或崩溃。未定义的是编译器生成的可执行文件(如果有的话)的行为。 - M.M
2
"UB在运行时不会被遇到,它是源代码的属性。" -它有两种不同的情况。例如,如果用户输入整数但未检查他们是否输入“0”,则可能在运行时遇到UB。 - M.M
显示剩余11条评论

4
留下行为未定义的原因之一是让编译器在优化时可以做任何假设。如果存在某些条件是必须满足的,才能应用某种优化,并且这些条件依赖于代码中的未定义行为,则编译器可能会假定它们已经满足了,因为符合规范的程序不能以任何方式依赖于未定义的行为。值得注意的是,编译器在这些假设上不需要保持一致(这在实现定义行为中并非如此)。因此,假设您的代码包含像下面这个明显牵强的例子:
int bar = 0;
int foo = (undefined behavior of some kind);
if (foo) {
   f();
   bar = 1;
}
if (!foo) {
   g();
   bar = 1;
}
assert(1 == bar);

编译器在第一个块中可以假定!foo为真,在第二个块中假定foo为真,从而优化整个代码块。 现在,逻辑上,foo或!foo必须为真,因此看代码时,您可以合理地假定运行代码后bar必须等于1。 但由于编译器以那种方式进行了优化,bar永远不会被设置为1。现在该断言变为false且程序终止,这是如果foo没有依赖于未定义的行为就不会发生的行为。
现在,如果编译器看到未定义的行为,它是否可能插入全新的代码? 如果这样做能够更好地进行优化,那么完全可以。 是否经常发生? 可能不会,但您永远无法保证,因此基于鼻涕鬼可能存在的假设是唯一安全的方法。

叹息。你看过我的编辑吗?我要求人们不要发布有关优化的答案,除非这些答案清楚地说明了什么使 UB 比 "实现定义" 行为更适合优化。此外,我想知道标准允许“什么”,而不是“为什么”允许它,所以从技术上讲,这并没有回答问题 - 虽然我确实赞赏对 UB 的辩护,因为我越来越反对 UB 的想法。 - Kyle Strand
3
能够不一致是其中一个重要的区别。sizeof(int)是由具体实现定义的,但它不会在程序运行过程中从4变为8。如果它是未定义的,那么它可能会这样做。而且具体实现还往往有额外的限制:例如,sizeof(int) * CHAR_BIT必须至少为16,而如果它是未定义的,它可以是或做任何事情。 - Ray
这听起来像是你的答案中应该包含的有用区分。 - Kyle Strand
啊,我看到你已经这样做了。 - Kyle Strand
给定 int i=INT_MAX; long l1,l2; i+=function_returning_one(); l1=i; second_function(); l2=i; 我不认为 l1 会产生 INT_MAX+1u,而 l2 会产生 -INT_MAX-1 的“惊人”结果;实际上,在许多 DSP 上,这种行为可能是一个常见的结果(编译器会将 16 位值 i 加到 32 位累加器中,将结果存储在 il2 中,调用 second_function(),加载 i(16 位),并将其存储到 l2。将代码写成 i=(int)((unsigned)i+function_returning_one()); 将导致 l1l2 产生 -INT_MAX-1,但会使代码更难读和理解... - supercat
显示剩余3条评论

4

未定义行为是指出现了规范的作者没有预见到的情况所导致的结果。

以交通信号灯为例,红色意味着停止,黄色意味着准备停止,绿色意味着前行。在这个例子中,驾车的人是规范的实施者。

如果同时出现绿色和红色怎么办?你是先停再走吗?还是等到红灯灭掉只剩绿灯时再过去?这是规范没有描述的一种情况,因此,驾驶员所做的任何事情都是未定义的行为。有些人会做一件事,有些人会做另一件事。由于无法保证会发生什么,建议避免出现这种情况。同样适用于代码。


4
在C/C++语言中,情况未必如此。在许多情况下,故意预见并故意保持未定义行为。在C/C++中,未定义行为是规范中明确给出的一些例子。我没有理由认为每个参与第一个标准制定的人都没有考虑过解引用NULL指针时应该发生什么。相反,他们可能有意将其保持为未定义状态,以便编译器不必特殊处理它,从而减慢代码速度。 - Muzer
2
如果交通信号灯出现故障,请像停车标志一样处理。如果代码出现故障,请谨慎对待,但尽可能继续进行。 - chux - Reinstate Monica
1
@Muzer:我认为 UB 的一个更大的原因是允许代码利用平台特性,这在某些情况下很有用但在其他情况下会很麻烦。在某些机器上,溢出捕获整数算术是正常行为,而非捕获算术则很昂贵。在其他机器上,整数算术溢出通常会包装,而溢出捕获则非常昂贵。对于标准来说,强制要求捕获或非捕获行为不仅会增加其中一种平台上所有算术的成本,而且还会雪上加霜... - supercat
1
更糟的是,想要利用这种不受欢迎的行为计算 x+y 的代码(写给实现该行为的硬件)将不得不添加额外的逻辑来实现所需的行为,并且由于编译器中包含的逻辑,所有添加的逻辑都会运行得更慢。因此,本应该被翻译为 add r1,r2,r3 的东西最终会变成一些庞然大物,可能比如果溢出是未定义的情况下满足要求的最佳代码少了不到10% 的速度。 - supercat
1
@supercat 但是C语言的重点一直是可移植性。因此,如果您的代码在不同平台上执行不同的操作,除非这真的是必要的并且是您想要的(例如内联汇编之类的东西),否则您的代码就是有问题的。因此,您应该编写代码以避免这些情况。因此,编译器能够将此行为转换为任何内容,并无情地利用这种情况,在我看来是完全有效的。人们永远不应该依赖于可能在编译器/架构之间有所不同的任何行为。 - Muzer
显示剩余18条评论

3
未定义的行为允许编译器在某些情况下生成更快的代码。考虑两个不同的处理器架构,它们的加法操作有所不同:处理器A固有地在溢出时丢弃进位位,而处理器B会生成错误。(当然,处理器C固有地生成鼻涕动力纳米机中的额外一位能量-这只是释放额外能量的最简单方法...)
如果标准要求生成错误,则为处理器A编译的所有代码都基本上被迫包含附加指令,以执行某种检查以观察是否溢出,并在发生溢出时生成错误。这将导致更慢的代码,即使开发人员知道他们只会添加小数值。
未定义行为以速度为代价牺牲可移植性。通过允许“任何事情”发生,编译器可以避免为永远不会发生的情况编写安全检查。(当然,你知道...它们可能会。)
此外,当程序员确切地知道未定义行为在其给定环境中实际引起的结果时,他们可以自由地利用该知识来获得额外的性能。
如果您想确保您的代码在所有平台上的行为完全相同,则需要确保永远不会发生“未定义行为”-但这可能不是您的目标。
编辑:(响应于OP的编辑)
实现定义的行为将要求一致地生成鼻涕动力纳米机。未定义的行为允许零星地生成鼻涕动力纳米机。
这就是未定义行为相对于实现特定行为具有优势的地方。考虑到在特定系统上需要额外的代码以避免不一致的行为。在这些情况下,未定义行为可以提高速度。

1
可能只是说“你可以做任何想做的事情”比试图列出你可以做和不能做的所有事情更容易。当然,在PC平台上,你通常会从外部USB设备生成鼻涕鬼……这可能不会在电子计算机上意外发生……但它可能会在图灵完备的Ouija板上意外发生。并非所有计算机都必须是电子的,因此并非所有鼻涕鬼都必须来自有意恶意的代码。有些可能只是来自不安全的代码。 - Allen
1
@KyleStrand:编写正确的C代码,就不会出现问题。标准不应该改变。如果您确实想要特定的行为,编译器已经增加了选项和内置函数来明确执行您想要的操作。C是关于快速代码的。我建议使用Java、C#、Go等语言进行手把手教学。 - Zan Lynx
1
@ZanLynx:汇编语言比现代C语言更少出错。在汇编语言中,如果存储一个不再有效的指针的内存位置应该保持为null,那么可以使用类似于“ldr r1,[r0] / cmp r1,#0 / bne oops”的代码进行安全测试,并且知道汇编器不会做任何奇怪的事情。对于大多数平台上明智的C编译器来说,“assert(*q==null);”应该是安全的。如果“q”不是null,则断言将失败,终止程序,或者系统将检测到“q”是无效指针并终止程序。然而,超现代的C认为如果编译器... - supercat
1
如果确定 q 不能为非空,而比较会引发 UB,则应该不仅删除比较,还应删除其他代码,因为它们在这种情况下被认为没有用处,可能导致比断言旨在防止的行为更糟糕的结果。 - supercat
1
@supercat,我很高兴提出这个问题,即使没有其他原因,也是为了间接地激发你所有的评论。 - Kyle Strand
显示剩余10条评论

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