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

Undefined, unspecified and implementation-defined behavior

提问人:Zolomon 提问时间:3/8/2010 最后编辑:Gabriel StaplesZolomon 更新时间:8/29/2023 访问量:87176

问:

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

C++ C 定义 未指定 实现定义行为

评论

1赞 dmckee --- ex-moderator kitten 3/8/2010
我很确定我们已经吃过这个了,但我找不到它。Смотритетакже: stackoverflow.com/questions/2301372/...
1赞 Vijay 7/10/2013
theunixshell.blogspot.com/2013/07/......
1赞 Owen 7/21/2013
这里有一个有趣的讨论(“附录 L 和未定义的行为”部分)。
2赞 jamesdlin 3/8/2010
来自 comp.lang.c FAQ:人们似乎特别注意区分实现定义的、未指定的和未定义的行为。这些是什么意思?
0赞 Jesper Juhl 3/29/2023
en.cppreference.com/w/cpp/language/ub

答:

121赞 AnT stands with Russia 3/8/2010 #1

嗯,这基本上是 C 标准的直接复制粘贴:

3.4.1 1 实现定义的行为 未指定的行为 每个实现都记录了 做出选择

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

3.4.3 1 未定义的行为行为,在使用时不可移植或错误 程序构造或错误 数据,对于这个国际 标准不设要求

2 注意:可能的未定义行为 范围从忽略情况 完全有不可预测的结果, 在翻译过程中的行为或 程序执行记录在案 方式特征 环境(带或不带 发出诊断消息),以 终止翻译或执行 (随着诊断的发布 消息)。

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

3.4.4 1 未指定行为 使用未指定值或其他行为 其中本国际标准 提供两种或多种可能性,以及 对以下方面没有进一步的要求 在任何情况下都选择哪个

2 示例:未指定的示例 行为是 计算函数的参数。

评论

4赞 Zolomon 3/8/2010
实现定义的行为和未指定的行为之间有什么区别?
32赞 AnT stands with Russia 3/8/2010
@Zolomon:就像它说的:基本上是一样的,只是在实现定义的情况下,实现需要记录(保证)到底会发生什么,而在未指定的情况下,实现不需要记录或保证任何事情。
1赞 sbi 3/8/2010
@Zolomon:这反映在 3.4.1 和 2.4.4 之间的区别上。
13赞 supercat 5/6/2015
@Celeritas:超现代的编译器可以做得更好。给定一个编译器可以确定,由于调用函数的所有方法都调用了不发射导弹的 Undefined Behavior,因此它可以使调用成为无条件的。int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }launch_missiles()
4赞 AnT stands with Russia 3/21/2017
@northerner 正如引文所述,未指定的行为通常仅限于一组有限的可能行为。在某些情况下,您甚至可能会得出结论,在给定的上下文中,所有这些可能性都是可以接受的,在这种情况下,未指定的行为根本不是问题。未定义的行为是完全不受限制的(例如,“程序可能会决定格式化您的硬盘驱动器”)。未定义的行为始终是一个问题。
14赞 Anders Abel 3/8/2010 #2

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

他们的最终总结:

总而言之,未指定的行为通常是您不应该做的事情 担心,除非您的软件需要可移植。 相反,未定义的行为总是不可取的,也不应该 发生。

评论

1赞 supercat 7/5/2017
有两种类型的编译器,除非另有明确记录,否则将标准的大多数未定义行为形式解释为回退到底层环境记录的特征行为,以及默认情况下仅有用地公开标准描述为实现定义的行为的编译器。当使用第一种类型的编译器时,使用UB可以高效、安全地完成第一种类型的许多事情。第二种类型的编译器只有在提供选项来保证此类情况下的行为时才适合此类任务。
72赞 Khaled Alshaya 3/8/2010 #3

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

实现定义的行为:
该语言说我们有数据类型。编译器供应商指定他们应该使用的大小,并提供他们所做的事情的文档。

未定义的行为:
你做错了什么。例如,您在不适合 .你如何把这个价值放进去?其实是没办法的!任何事情都可能发生,但最明智的做法是获取该 int 的第一个字节并将其放入 .这样做来分配第一个字节是错误的,但这就是引擎盖下发生的事情。
intcharcharchar

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

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());

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

据称,给予编译器这种自由和要求“普通的从左到右评估”之间的差异可能很大。我不相信,但是由于无数的编译器“在那里”利用了自由,有些人热情地捍卫了这种自由,改变将是困难的,可能需要几十年的时间才能渗透到C和C++世界的遥远角落。令我失望的是,并非所有编译器都警告诸如 .同样,参数的计算顺序也未指定。++i+i++

IMO有太多的“事物”没有定义,没有具体说明,这很容易说,甚至举例,但很难解决。还应该注意的是,避免大多数问题并生成可移植代码并不是那么困难。

评论

3赞 Lazer 3/8/2010
因为行为不是吗?毕竟,编译器必须选择一门或另一门课程?fun(fun1(), fun2());"implementation defined"
1赞 Lazer 3/8/2010
@AraK:感谢您的解释。我现在明白了。顺便说一句,我知道这种情况会发生。真的,现在我们使用的编译器吗?"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"can
1赞 Khaled Alshaya 3/8/2010
@eSKay 你必须问一个大师,他亲手弄脏了许多编译器:)AFAIK VC 始终从右到左评估参数。
5赞 supercat 3/22/2011
@Lazer:这肯定会发生。简单场景:foo(bar, boz()) 和 foo(boz(), bar),其中 bar 是 int,boz() 是返回 int 的函数。假设 CPU 的参数应在寄存器 R0-R1 中传递。函数结果以 R0 格式返回;函数可能会破坏 R1。在“boz()”之前计算“bar”需要在调用 boz() 之前将 bar 的副本保存在其他地方,然后加载保存的副本。在“boz()”之后计算“bar”将避免内存存储和重新获取,并且是许多编译器都会做的优化,无论它们在参数列表中的顺序如何。
6赞 Nikolai Ruhe 1/14/2013
我不知道 C++,但 C 标准说 int 到 char 的转换要么是实现定义的,要么是很好地定义的(取决于类型的实际值和符号)。参见 C99 §6.3.1.3(在 C11 中未更改)。
488赞 fredoverflow 11/5/2010 #4

未定义的行为是 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;
}

该变量指向字符串文字,下面的两个赋值尝试修改该字符串文字。这个程序有什么作用?根据 C++ 标准 [lex.string] 注释 4,它调用未定义的行为p"hello!\n"

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

我能听到人们尖叫“但是等等,我可以编译这个没有问题并获得输出”或“你是什么意思未定义,字符串文字存储在只读内存中,因此第一次赋值尝试会导致核心转储”。这正是未定义行为的问题。基本上,该标准允许一旦你调用未定义的行为(甚至是鼻魔),任何事情都会发生。如果根据你的语言心智模型存在“正确”的行为,那么该模型就完全是错误的;C++ 标准有唯一的投票,句号。yellow

未定义行为的其他示例包括

[intro.defs] 还定义了未定义行为的两个不太危险的兄弟,未指定行为实现定义的行为

实现定义的行为 [defns.impl.defined]

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

未指定行为 [defns.unspecified]

行为,对于格式良好的程序构造和正确的数据,这取决于实现

[注意:不需要实现来记录发生的行为。 本文档通常描述可能的行为范围。 — 结束语]

未定义的行为 [defns.undefined]

本文档不施加任何要求的行为

[注意:当本文档省略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。 允许的未定义行为包括完全忽略结果不可预测的情况,到在翻译或程序执行过程中以环境特征的记录方式(有或没有发出诊断消息)的行为,再到终止翻译或执行(发出诊断消息)。[...] — 结束语]

您可以做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道自己在说什么的作者的好C++书籍。避免互联网教程。避免胡说八道。

评论

11赞 Johannes Schaub - litb 11/20/2010
合并导致的一个奇怪的事实是,这个答案只涵盖 C++,但这个问题的标签包括 C.C 对“未定义的行为”有不同的概念:它仍然需要实现提供诊断消息,即使行为也被声明为某些规则冲突(约束冲突)的未定义。
15赞 fredoverflow 1/17/2013
@Benoit 这是未定义的行为,因为标准说它是未定义的行为。在某些系统上,字符串文本确实存储在只读文本段中,如果尝试修改字符串文本,程序将崩溃。在其他系统上,字符串文字确实会出现更改。该标准没有强制要求必须发生什么。这就是未定义行为的含义。
8赞 Pacerier 9/27/2013
@FredOverflow,为什么一个好的编译器允许我们编译给出未定义行为的代码?编译这种代码到底能带来什么好处?为什么当我们尝试编译给出未定义行为的代码时,并非所有优秀的编译器都会给我们一个巨大的红色警告信号?
18赞 Tim Seguine 12/8/2013
@Pacerier 有些事情在编译时是不可检查的。例如,并不总是能够保证 null 指针永远不会被取消引用,但这是未定义的。
5赞 Mark 10/13/2015
@Celeritas,未定义的行为可能是不确定的。例如,不可能提前知道未初始化内存的内容是什么。:的值可能会在函数调用之间更改。int f(){int a; return a;}a
32赞 Johannes Schaub - litb 1/24/2013 #5

来自官方的 C Rationale 文档

术语“未指定行为”、“定义行为”和“实现定义行为”用于对编写程序的结果进行分类,这些程序的属性是标准没有或不能完全描述的。采用这种分类的目的是允许实现之间有一定的多样性,从而允许实施质量成为市场上的一股活跃力量,并允许某些流行的扩展,而不会消除符合标准的声望。该标准的附录 F 对属于这三类之一的行为进行了分类。

未指定的行为为实现者在翻译程序时提供了一定的自由度。这种自由度不会延伸到无法翻译程序的程度。

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

实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释这种选择。指定为实现定义的行为通常是用户可以根据实现定义做出有意义的编码决策的行为。实现者在决定实现定义应该有多广泛时,应牢记这一标准。与未指定的行为一样,仅仅无法转换包含实现定义行为的源代码并不是一个充分的响应。

评论

4赞 supercat 5/26/2016
超现代的编译器编写者还认为“未定义的行为”是授予编译器编写者许可,以假设程序永远不会接收到会导致未定义行为的输入,并任意改变程序在接收此类输入时行为方式的所有方面。
2赞 supercat 8/19/2016
我刚刚注意到的另一点是:C89 没有使用术语“扩展”来描述在某些实现上得到保证但在其他实现中没有保证的功能。C89 的作者认识到,当时大多数实现将相同地对待有符号算术和无符号算术,除非以某些方式使用结果,并且这种处理即使在有符号溢出的情况下也适用;然而,他们没有在附件J2中将其列为共同的扩展,这在我看来表明,他们认为这是一种自然状态,而不是扩展。
6赞 4pie0 5/10/2014 #6

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

行为,对于一个格式良好的程序构造和正确的数据,那 取决于实施和每个实施文件

有时,C++ 标准不会对某些构造施加特定行为,而是说必须通过特定的实现(库版本)选择和描述特定的、定义良好的行为。因此,即使标准没有描述这一点,用户仍然可以确切地知道程序将如何运行。


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

本国际标准不设要求的行为 [ 注意:当这个国际 标准省略了任何明确的行为定义,或者当程序 使用错误的构造或错误的数据。允许的未定义 行为范围从完全忽略情况 不可预知的结果,在翻译或程序过程中的行为 以记录在案的方式执行环境特征 (发出或不发出诊断消息),直至终止 翻译或执行(发布诊断 消息)。许多错误的程序构造不会产生未定义的 行为;他们需要被诊断。— 尾注 ]

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


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

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

C++ 标准并没有对某些构造施加特定的行为,而是说必须通过特定的实现(库版本)选择(但不一定描述)特定的、定义良好的行为。因此,在未提供描述的情况下,用户可能很难确切地知道程序的行为方式。

12赞 Suraj K Thomas 3/17/2015 #7

实施定义-

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

未指定 -

与已定义但未记录的实现相同

定义-

任何事情都可能发生,照顾好它。

评论

3赞 supercat 4/17/2015
我认为需要注意的是,“未定义”的实际含义在过去几年中发生了变化。过去是这样,评估 33 岁时可能会产生 0 或 2 的收益,但不会做其他任何古怪的事情。但是,较新的编译器在计算时可能会使编译器确定,由于之前必须小于 32,因此可以省略该表达式之前或之后的任何代码,这些代码仅在 32 或大于 32 时才相关。uint32_t s;1u<<ss1u<<sss
10赞 supercat 4/16/2015 #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 << powv

然而,当今一些编译器编写者的首选理念是,因为只有当程序要从事未定义行为时才能为负数,所以没有理由让程序剪辑负范围。尽管每个重要的编译器都支持负值的左移,并且大量现有代码依赖于该行为,但现代哲学会解释标准说左移负值是UB的事实,这意味着编译器编写者应该随意忽略这一点。vv

评论

0赞 Tom Swirly 5/26/2016
但是,以一种好的方式处理未定义的行为并不是免费的。现代编译器在某些 UB 情况下表现出如此奇怪的行为的全部原因是它们正在无情地优化,并且为了在这方面做得最好,他们必须能够假设 UB 永远不会发生。
1赞 Tom Swirly 5/26/2016
但是,负数上的UB是一个令人讨厌的小陷阱,我很高兴被提醒这一点!<<
1赞 supercat 5/26/2016
@TomSwirly:不幸的是,编译器编写者并不在乎,与要求代码不惜一切代价避免标准未定义的任何内容相比,提供超出标准规定的松散行为保证通常可以大幅提高速度。如果程序员不在乎在加法溢出的情况下是产生 1 还是 0,只要它没有其他副作用,编译器可能能够进行一些大规模的优化,如果程序员将代码编写为 .i+j>k(int)((unsigned)i+j) > k
1赞 supercat 5/26/2016
@TomSwirly:对他们来说,如果编译器 X 可以采用严格符合的程序来执行某些任务 T,并生成一个比编译器 Y 使用相同程序产生的效率高 5% 的可执行文件,这意味着 X 更好,即使 Y 可以生成执行相同任务的代码,前提是程序利用了 Y 保证但 X 没有的行为。
1赞 supercat 1/15/2022
@PSkocik:将 、 和 是编译器在函数调用中扩展的函数的参数视为一个简单的场景。在这种情况下,编译器可以用 替换,而 又可以替换成 ,完全跳过添加,从而消除对 值 的任何依赖性,并可能允许编译器消除比较和对 的确切值的任何依赖性,如果它可以确定该值将始终为正数。ijkfoo(x, y, x)i+j > kx+y > xy > 0xyy
1赞 Steve Summit 6/17/2021 #9

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

好:一个编译和工作的程序,出于正确的原因。

Bad:有错误的程序,编译器可以检测和抱怨的那种错误。

丑陋:一个有错误的程序,编译器无法检测和警告,这意味着程序编译,有时似乎可以正常工作,但有时也会奇怪地失败。这就是未定义的行为。

一些程序语言和其他形式化系统试图限制“未定义的鸿沟”——也就是说,它们试图安排事物,使大多数或所有程序要么是“好的”,要么是“坏的”,而很少有程序是“丑陋的”。然而,C 语言的一个特征是它的“未定义鸿沟”相当宽。

评论

0赞 supercat 6/17/2021
被标准描述为未定义行为的构造是“不可移植的或错误的”,但该标准没有试图区分那些错误的构造和那些不可移植的构造,但在由编写它们的实现与之兼容的其他实现处理时是正确的构造。
1赞 klutt 8/22/2022
公平地说,通常很有可能对未定义的行为发出警告。