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

在C和c++中未定义的行为 (UB)是什么?未指明的行为实现定义行为怎么样?它们之间的区别是什么?

78099 次浏览

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

3.4.1 1 实现定义的行为未指定的行为 每个实现都记录了如何

的例子 实现定义的行为是 传播时的高阶位 有符号整数右移

3.4.3 1 未定义的行为行为,在使用不可移植或错误 程序构造或错误的 数据,为此国际 标准没有要求

< p > 2 可能的未定义行为 范围从忽略情况 结果完全不可预测, 在翻译或 程序的执行有文档 他们的行事风格 环境(有或没有 发出诊断消息),到 终止翻译或执行 (出具诊断报告 消息)。< / p >

3 EXAMPLE 未定义的行为是行为上 整数溢出。< / p >

3.4.4 1 未指明的行为使用未指定的值,或其他行为 本国际标准 提供两种或更多的可能性和 没有进一步的要求 在任何实例中选择哪个

< p > 2 未指定的示例 行为是指 对函数的实参求值

未定义行为vs.未指定行为有一个简短的描述。

他们最后的总结是:

总而言之,未指定的行为通常是你不应该做的 担心,除非你的软件被要求是可移植的。 相反,未定义的行为总是不受欢迎的,也不应该这样做 发生。< / p >

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

< p > # EYZ0
语言说我们有数据类型。编译器供应商指定他们应该使用的大小,并提供他们所做的工作的文档 < p > # EYZ0
你做错了什么。例如,您在int中有一个非常大的值,而这个值不适合char。你如何把这个值放在char?其实没有办法!任何事情都可能发生,但最明智的做法是将该int的第一个字节放在char中。这样分配第一个字节是错误的,但这是在引擎盖下发生的 < p > # EYZ0
这两个函数哪个先执行?< / p >
void fun(int n, int m);


int fun1() {
std::cout << "fun1";
return 1;
}
int fun2() {
std::cout << "fun2";
return 2;
}


//...


fun(fun1(), fun2()); // which one is executed first?

语言没有指定计算值,从左到右还是从右到左!因此,未指定的行为可能会导致未定义的行为,也可能不会导致未定义的行为,但可以肯定的是,您的程序不应该产生未指定的行为。


@eSKay我认为你的问题值得编辑答案以澄清更多:)

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

实现定义和未指定的区别在于,编译器应该在第一种情况下选择一个行为,但在第二种情况下不必这样做。例如,一个实现必须有且只有一个sizeof(int)定义。因此,它不能说sizeof(int)对于程序的某些部分是4,而对于其他部分是8。与未指定的行为不同,编译器可以这样说:“好吧,我将从左到右计算这些参数,而下一个函数的参数将从右到左计算。”它可以发生在同一个程序中,这就是为什么它被称为< >强未指明的< / >强。事实上,如果指定了一些未指定的行为,c++可能会变得更简单。看看Stroustrup博士对此的回答是:

据称,给编译器这种自由和要求“普通的从左到右求值”之间的差异;可能意义重大。我不相信,但有无数的编译器“;利用自由的优势和一些人热情地捍卫这种自由,改变将是困难的,可能需要几十年才能渗透到C和c++世界的遥远角落。我很失望,不是所有的编译器都对++i+i++这样的代码发出警告。类似地,参数的求值顺序是不指定的。

在我看来,太多的“事情”;没有定义,没有具体说明,说起来容易,甚至可以举个例子,但很难解决。还应该注意的是,避免大多数问题并生成可移植代码并不难。

<强> < / >强未定义行为是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))。这些构成了抽象机器的参数。每个实现都应该包括描述其在这些方面的特征和行为的文档。

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

某些其他操作在本国际标准中被描述为未定义的(例如,取消空指针引用的效果)。[# eyz1: # eyz2 - # eyz3]

具体来说,第1.3.24节指出:

允许的未定义行为范围从完全忽视这种情况会导致不可预测的结果,到在翻译或程序执行期间以环境特征的文档化方式(有或没有发出诊断消息)行为,到终止翻译或执行(发出诊断消息)。

你能做些什么来避免遇到未定义的行为?基本上,你必须读清楚自己在说什么的作者写的优秀的c++书籍。避免网上教程。避免bullschildt。

来自官方的C基本原理文档

术语未指明的行为、未定义的行为和实现定义行为被用来对编写的程序的结果进行分类,这些程序的属性在标准中没有或不能完全描述。采用这种分类的目标是在不去掉符合标准的标记的情况下,允许实现之间的某种多样性,从而允许实现的质量在市场上发挥积极作用,并允许某些流行的扩展。标准附录F列出了属于这三类之一的行为。

未指明的行为让实现者在翻译程序时有一定的自由度。这个范围还没有扩展到无法翻译程序。

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

实现定义行为给予实现者选择适当方法的自由,但要求向用户解释这种选择。被指定为实现定义的行为通常是那些用户可以基于实现定义做出有意义的编码决策的行为。实现者在决定一个实现定义应该有多广泛时应该牢记这个标准。与未指定的行为一样,简单地不翻译包含实现定义的行为的源并不是一个充分的响应。

c++标准n3337 § 1.3.10 实现定义的行为< / >强

行为,对于格式良好的程序构造和正确的数据,即 取决于实现和每个实现文档

有时候c++标准并没有将特定的行为强加于某些结构,而是说特定的、定义良好的行为必须由特定的实现(库的版本)选择和描述。所以用户仍然可以确切地知道程序的行为,即使标准没有描述这一点。


c++标准n3337 § 1.3.24 未定义的行为< / >强

本国际标准不作任何要求的行为 [注:当本国际 标准省略了任何明确的行为定义或当一个程序 使用错误的结构或错误的数据。允许定义 行为包括完全忽视情况 不可预测的结果,以行为在翻译或程序 以具有环境特征的文件化方式执行 (无论是否发出诊断消息),直至终止 翻译或执行(伴随着诊断的发布) 消息)。许多错误的程序构造不会产生未定义的 行为;他们被要求进行诊断。- end note]

当程序遇到不是根据c++标准定义的构造时,它可以做任何它想做的事情(可能给我发一封电子邮件,也可能给你发一封电子邮件,或者可能完全忽略代码)。


c++标准n3337 § 1.3.25 未指明的行为< / >强

行为,对于格式良好的程序构造和正确的数据,即 依赖于实现[注:实现不是 需要记录发生的行为。可能的范围 行为通常是由本国际标准来描述的。- - -结束 注意]< / p >

c++标准没有将特定的行为强加于某些结构,而是说特定的、定义良好的行为必须由特定的实现(库的版本)选择(Bot没有必要描述)。因此,在没有提供描述的情况下,用户很难确切地知道程序将如何运行。

实现定义的,

实现者希望,应该有良好的文档,标准给出了选择,但一定要编译

未指定的,

与实现定义的相同,但没有文档化

未定义的,

不管发生什么事,都要小心。

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

实现定义的行为和未定义的行为之间的唯一区别是,前者要求实现定义并记录一致的行为即使在实现不可能做任何事情的情况下也是有用的。它们之间的分界线不是定义行为对于实现是否通常有用(编译器编写者应该在实际情况下定义有用的行为,无论标准是否要求他们这样做),而是是否可能存在定义行为既昂贵又无用的实现。这种实现可能存在的判断并不以任何方式、形状或形式暗示对在其他平台上支持已定义行为的有用性的判断。

不幸的是,自20世纪90年代中期以来,编译器作者开始将缺乏行为授权解释为一种判断,即行为保证不值得付出代价,即使在它们至关重要的应用领域,甚至在它们几乎不花费任何成本的系统上。而不是把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的事实解释为编译器编写者可以随意忽略这一点。

未定义的行为是丑陋的——就像“好的,坏的,丑陋的”。

好:一个能够编译并正常工作的程序。

坏的:有错误的程序,这种错误编译器可以检测到并报错。

丑陋的:一个程序有一个错误,编译器不能检测并警告,这意味着程序编译,可能在某些时候正确工作,但也可能在某些时候奇怪地失败。这就是未定义行为。

一些程序语言和其他正式系统试图限制“未定义的鸿沟”。——也就是说,他们设法安排事情,使大多数或所有的程序要么“好”;或者是“坏”的,只有极少数是“丑”的。然而,C语言的一个特征是它的“未定义的海湾”;相当宽。