未定义,未指定和实现定义的行为


529

什么是C和C ++中的未定义行为?未指定的行为和实现定义的行为呢?它们之间有什么区别?


1
我非常确定我们已经在此之前这样做了,但是我找不到它。参见:stackoverflow.com/questions/2301372/...
dmckee ---前主持人小猫



1
这是一个有趣的讨论(“附件L和未定义的行为”部分)。
Owen

Answers:


404

未定义的行为是C和C ++语言的那些方面之一,对于其他语言的程序员来说,这可能是令人惊讶的(其他语言试图更好地隐藏它)。基本上,即使许多C ++编译器不会报告程序中的任何错误,也可以编写行为无法预测的C ++程序!

让我们看一个经典的例子:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

该变量p指向字符串文字"hello!\n",下面的两个赋值试图修改该字符串文字。这个程序做什么?根据C ++标准的第2.14.5节第11段,它调用未定义的行为

尝试修改字符串文字的效果是不确定的。

我听到有人在尖叫“但是,等等,我可以编译此文件并获取输出yellow”或“您的意思是未定义的字符串文字存储在只读存储器中,因此第一次分配尝试会导致核心转储”。这正是未定义行为的问题。基本上,一旦您调用未定义的行为(甚至是鼻恶魔),该标准将允许发生任何事情。如果根据您的语言思维模式有“正确”的行为,则该模式完全是错误的。C ++标准具有唯一的投票期限。

未定义行为的其他示例包括访问数组之外​​的数组,取消引用空指针,在对象生命周期结束后访问对象或编写所谓的聪明表达式(如)i++ + ++i

C ++标准的1.9节还提到了未定义行为的两个不太危险的兄弟,即未指定行为实现定义的行为

本国际标准中的语义描述定义了参数化的不确定性抽象机器。

抽象机的某些方面和操作在此国际标准中描述为实现定义的(例如sizeof(int))。这些构成了抽象机的参数。每个实现应包括描述这些方面的特性和行为的文档。

抽象机的某些其他方面和操作在本国际标准中描述为未指定(例如,对函数自变量的求值顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机的不确定性方面。

在本国际标准中,某些其他操作被描述为未定义(例如,取消引用空指针的效果)。[ 注意本国际标准对包含未定义行为的程序的行为没有任何要求。尾注 ]

具体而言,第1.3.24节规定:

允许的未定义行为的范围从完全忽略具有不可预测结果的情况到在翻译或程序执行过程中以具有环境特征的书面方式记录的行为(带有或不带有诊断消息)到终止翻译或执行(带有发布)诊断消息)。

您如何避免出现未定义的行为?基本上,您必须阅读知道他们在说什么的作者的优秀C ++书籍。螺丝互联网教程。螺丝废话。


6
这是合并产生的一个奇怪的事实,即该答案仅涵盖C ++,但此问题的标记包括C。C具有“未定义行为”的不同概念:即使行为也被声明为,它仍将要求实现提供诊断消息。对于某些违反规则(约束违反)未定义。
Johannes Schaub-litb 2010年

8
@Benoit这是未定义的行为,因为标准说这是未定义的行为,即周期。在某些系统上,实际上字符串文字存储在只读文本段中,如果您尝试修改字符串文字,程序将崩溃。在其他系统上,字符串文字确实会出现变化。该标准未规定必须执行的操作。这就是未定义行为的意思。
fredoverflow 2013年

5
@FredOverflow,为什么好的编译器允许我们编译具有未定义行为的代码?编译这种代码究竟有什么好处?当我们尝试编译具有未定义行为的代码时,为什么所有好的编译器都没有给我们一个巨大的红色警告信号?
Pacerier

14
@Pacerier有些事情在编译时是不可检查的。例如,并非总是可以保证永远不会取消引用空指针,但这是未定义的。
Tim Seguine 2013年

4
@Celeritas,未定义的行为可能是不确定的。例如,不可能事先知道未初始化存储器的内容是什么,例如。int f(){int a; return a;}:的值a可能在函数调用之间改变。
2015年

96

好吧,这基本上是标准的直接复制粘贴

3.4.1 1个实施定义的行为未指定的行为,其中每个实施记录了如何做出选择

2示例实现定义的行为的一个示例是有符号整数右移时的高位传播。

3.4.3 1未定义行为的行为,在使用非便携式或错误程序构造或错误数据时,本国际标准对此不施加任何要求

2注释可能的不确定行为包括从完全忽略情况以无法预测的结果到在翻译或程序执行过程中以环境特征的书面方式记录的行为(带有或不发出诊断消息)到终止翻译或执行(带有诊断消息的发布)。

3示例未定义行为的一个示例是整数溢出时的行为。

3.4.4 1未指明的行为使用未指明的值,或在本国际标准提供两种或两种以上可能性且在任何情况下均不对其施加任何其他要求的情况下的其他行为

2示例未指定行为的示例是对函数的参数进行评估的顺序。


3
实现定义的行为和未指定的行为有什么区别?
Zolomon 2010年

26
@Zolomon:就像它说的一样:基本相同,只是在实现定义的情况下要求实现记录(以保证)将要发生的确切情况,而在未指定的情况下,不需要记录实现或保证任何事情。
AnT

1
@Zolomon:这反映在3.4.1和2.4.4之间的差异中。
2010年

8
@Celeritas:超现代的编译器可以做得更好。给定int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }一个编译器可以确定,由于调用不发射导弹的功能的所有方法都调用了Undefined Behavior,因此可以使调用成为launch_missiles()无条件的。
2015年

2
@northerner如引号所述,未指定的行为通常仅限于一组有限的可能行为。在某些情况下,您甚至可能得出结论,在给定的上下文中所有这些可能性都是可以接受的,在这种情况下,未指定的行为根本不是问题。未定义的行为是完全不受限制的(例如,“程序可能决定格式化硬盘驱动器”)。未定义的行为始终是一个问题。
AnT

60

也许措辞比标准的严格定义更容易理解。

实现定义的行为
语言表示我们有数据类型。编译器供应商指定应使用的大小,并提供有关其所做工作的文档。

未定义的行为
您正在做错事。举例来说,您有个int不适合的非常大的价值char。您如何体现这一价值char?其实没有办法!可能会发生任何事情,但是最明智的做法是将int的第一个字节放入其中char。分配第一个字节是错误的,但这就是幕后的事情。

未指定的行为
这两个函数中的哪个先执行?

void fun(int n, int m);

int fun1()
{
  cout << "fun1";
  return 1;
}
int fun2()
{
  cout << "fun2";
  return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

语言没有指定评估,从左到右或从右到左!因此,未指定的行为可能会导致未定义的行为,也可能不会导致未定义的行为,但是当然您的程序不应产生未指定的行为。


@eSKay,我认为您的问题值得编辑答案以进一步阐明:)

对于fun(fun1(), fun2());不行为“实现定义”?毕竟,编译器必须选择一个或另一个过程?

实现定义的和未指定的之间的区别在于,编译器应该在第一种情况下选择一种行为,但在第二种情况下则不必选择行为。例如,实现必须具有的一个且只有一个定义sizeof(int)。因此,不能说sizeof(int)程序的某些部分为4,其他部分为8。与未指定的行为不同,在编译器可以说“ OK”的情况下,我将从左到右评估这些参数,而从下到右评估下一个函数的参数。它可以在同一程序中发生,因此被称为unspecified。实际上,如果指定了一些未指定的行为,C ++可能会变得更容易。在这里看看Stroustrup博士的回答

据称,可以给编译器带来这种自由的结果与需要“从左至右进行常规评估”之间的差异可能很大。我不敢相信,但是随着“无处不在”的众多编译器利用自由,并且有些人热情地捍卫了这种自由,改变将是困难的,并且可能需要数十年才能渗透到C和C ++世界的遥远角落。令我失望的是,并非所有编译器都针对++ i + i ++之类的代码发出警告。同样,参数的评估顺序也未指定。

IMO太多的“事物”是未定义的,未指定的,实现定义的等等。但是,这很容易说,甚至给出示例,但很难解决。还应注意,避免大多数问题并生成可移植代码并不是很困难。


1
因为fun(fun1(), fun2());这不是行为"implementation defined"吗?毕竟,编译器必须选择一个或另一个过程?
Lazer 2010年

1
@AraK:感谢您的解释。我现在明白了 顺便说一句,"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"我了解这种can情况。对于我们最近使用的编译器,真的吗?
Lazer

1
@eSKay您必须向一位大师咨询,他是谁被很多编译器弄脏了:) AFAIK VC总是从右到左评估参数。
AraK

4
@Lazer:肯定会发生。简单方案:foo(bar,boz())和foo(boz(),bar),其中bar是一个int,而boz()是一个返回int的函数。假定期望在其中将参数传递到寄存器R0-R1中的CPU。函数结果返回到R0;功能可能会破坏R1。要在“ boz()”之前评估“ bar”,则需要在调用boz()之前将bar的副本保存在其他位置,然后加载该保存的副本。在“ boz()”之后评估“ bar”将避免内存存储和重新获取,并且这是许多编译器都会执行的优化,而不管其在参数列表中的顺序如何。
超级猫

6
我不了解C ++,但是C标准说,将int转换为char是实现定义的,甚至定义得很好的(取决于实际值和类型的符号)。参见C99§6.3.1.3(在C11中不变)。
Nikolai Ruhe

27

摘自正式的C基本原理文档

术语“ 未指定行为”,“ 未定义行为”和“ 实现定义的行为”用于对编写程序的结果进行分类,这些程序的属性标准不能或不能完全描述。采用此分类的目的是允许实现中的某种变化,这使实现的质量成为市场上的活跃力量,并允许某些流行的扩展,而不会消除对标准的崇高敬意。该标准的附录F列出了属于这三种类别之一的那些行为。

未指定的行为使实施者可以自由翻译程序。这种纬度不会扩展到无法翻译程序的程度。

未定义的行为使实施者可以不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供正式未定义行为的定义来扩展语言。

实施定义的行为使实施者可以自由选择适当的方法,但要求向用户说明这一选择。通常,指定为实现定义的行为是指用户可以根据实现定义做出有意义的编码决策的行为。在确定实施定义应有多广泛时,实施者应牢记此标准。与未指定的行为一样,仅不翻译包含实现定义的行为的源就不足为响应。


3
超现代的编译器作者也将“未定义的行为”视为授予编译器作者许可以假定程序将永远不会接收到会导致未定义的行为的输入,并可以任意更改程序在收到此类输入时的行为方式。
超级猫

2
我刚刚注意到的另一点是:C89并未使用术语“扩展”来描述某些实现但没有其他实现的功能。C89的作者认识到,除非以某种方式使用结果,否则大多数当前实现都将以相同的方式对待有符号算术和无符号算术,并且即使在有符号溢出的情况下也要采用这种处理。他们没有在附件J2中将其列为通用扩展名,但对我而言,这表明他们将其视为自然状态,而不是扩展名。
超级猫

10

未定义行为与未指定行为对此进行了简短描述。

他们的最终总结:

综上所述,除非您的软件要求具有可移植性,否则通常不必担心未指定的行为。相反,未定义的行为始终是不希望的,并且永远不会发生。


1
有两种编译器:除非另有明确说明,否则它们将标准的大多数未定义行为形式解释为依赖于基础环境所记录的特征行为,而默认情况下仅有用地公开标准所描述为的行为的那些编译器。实现定义的。使用第一类型的编译器时,可以使用UB高效,安全地完成许多第一类型的事情。第二种类型的编译器仅在提供保证此类情况下行为的选项时,才适合此类任务。
超级猫

8

从历史上看,实现定义的行为和未定义的行为都代表了这样的情况,在该情况下,标准的作者期望编写高质量实现的人们将使用判断来决定哪些行为保证(如果有的话)将对运行在目标应用程序领域中的程序有用。预定目标。高端数字运算代码的需求与低层系统代码的需求完全不同,并且UB和IDB都为编译器编写者提供了满足这些不同需求的灵活性。这两个类别都不要求实现以对任何特定目的甚至对任何目的都有用的方式表现。但是,声称适合特定目的的质量实现应以适合该目的的方式运行该标准是否要求

实现定义行为与未定义行为之间的唯一区别是,前者要求实现定义并记录一致的行为,即使在实现可能无法做的情况下也是如此。它们之间的分界线不是通常是否对实现定义行为有用(无论标准是否要求编译器编写者应在可行时定义有用的行为),而是是否可能存在定义行为的实现同时代价高昂的实现。而且没用。对此类实现可能存在的判断,绝不以任何方式,形式或形式暗示对支持其他平台上定义的行为的有用性的任何判断。

不幸的是,自1990年代中期以来,编译器作者就开始将缺乏行为指令解释为一种判断,即即使在至关重要的应用领域,甚至在几乎不花费任何成本的系统上,行为保证也不值得付出代价。相反治疗UB作为邀请作出合理的判断,编译器的编写者已经开始把它当作借口这样做。

例如,给出以下代码:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

二进制补码实现v << pow无需考虑v是肯定的还是否定的,就无需花费任何精力将表达式视为二进制补码转换。

但是,在当今某些编译器编写者中,首选的哲学建议是,由于v只有在程序要进行未定义行为的情况下它才能为负,因此没有理由让程序限制的负范围v。即使以前每个有意义的编译器都支持负值的左移,并且大量现有代码都依赖于该行为,但是现代哲学仍会解释以下事实:标准称左移负值为UB暗示编译器作者应该随意忽略它。


但是,以一种不错的方式处理未定义的行为并不是免费的。现代编译器在UB的某些情况下表现出如此怪异的行为的全部原因是,他们不断地优化,并且要做到最好,他们必须能够假设UB永远不会发生。
汤姆·史威利

但是,<<UB为负数的事实是一个令人讨厌的小陷阱,我很高兴得到提醒!
汤姆·斯威

1
@TomSwirly:不幸的是,与要求代码不惜一切代价避免标准未定义的东西相比,编译器作者并不关心提供超出标准所规定的宽松行为保证通常可以大大提高速度。如果程序员不在乎i+j>k加法溢出的情况下产生1还是0(如果没有其他副作用),那么编译器可能能够进行一些大规模的优化,而如果程序员将代码编写为,则是不可能的(int)((unsigned)i+j) > k
超级猫

1
@TomSwirly:对他们来说,如果编译器X可以采用严格遵循标准的程序来执行某些任务T,并且生成的可执行文件的效率比在同一程序下编译器Y的执行效率高5%,那么这意味着X会更好,即使Y如果使用利用Y保证但X不能保证的行为的程序,则可以生成执行相同任务三倍的效率的代码。
超级猫

6

C ++标准n3337 § 1.3.10 实现定义的行为

行为,对于格式正确的程序构造和正确数据,取决于实施以及每个实施文档

有时,C ++ Standard并未在某些构造上强加特定的行为,而是说必须选择并描述特定的,定义明确的行为通过特定的实现(库版本)。因此,即使Standard并未对此进行描述,用户仍然可以确切知道程序的行为方式。


C ++标准n3337 § 1.3.24 未定义行为

本国际标准不施加任何要求的行为[注:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括:完全忽略具有不可预测结果的情况,以翻译或程序执行过程中以环境特征记录的方式表现的行为(带有或不带有诊断消息),终止翻译或执行(带有发布)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。—尾注]

当程序遇到未按照C ++标准定义的构造时,它可以做任何想做的事情(可以给我发送电子邮件,或者给你发送电子邮件,或者完全忽略代码)。


C ++标准n3337 § 1.3.25 未指定的行为

行为,对于格式正确的程序构造和正确数据,取决于实现[注:不需要实现来记录发生哪种行为。可能的行为范围通常由本国际标准来描述。—尾注]

C ++ Standard并未在某些构造上强加特定的行为,而是说必须通过特定的实现(库的版本)选择特定的,定义明确的行为(无需描述bot)。因此,在没有提供任何描述的情况下,用户可能很难确切知道程序的行为方式。


6

实施定义-

实施者希望,应该有充分的文件记录,标准可以选择,但一定要编译

未指定-

与实现定义相同,但未记录

未定义

可能发生任何事情,请谨慎处理。


2
我认为必须指出,“未定义”的实际含义在过去几年中发生了变化。它曾经是给定的uint32_t s;,评估1u<<s何时s是33可能会产生0或可能产生2,但别无所求。但是,较新的编译器进行评估1u<<s可能会导致编译器确定,因为s事先必须小于32,因此s可以省略该表达式之前或之后仅与32 相关的代码才有意义的任何代码。
supercat 2015年
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.