提问人:Zolomon 提问时间:3/8/2010 最后编辑:Gabriel StaplesZolomon 更新时间:8/29/2023 访问量:87176
未定义、未指定和实现定义的行为
Undefined, unspecified and implementation-defined behavior
答:
嗯,这基本上是 C 标准的直接复制粘贴:
3.4.1 1 实现定义的行为 未指定的行为 每个实现都记录了 做出选择
2 示例 一个例子 实现定义的行为是 高阶位的传播 有符号整数向右移动。
3.4.3 1 未定义的行为行为,在使用时不可移植或错误 程序构造或错误 数据,对于这个国际 标准不设要求
2 注意:可能的未定义行为 范围从忽略情况 完全有不可预测的结果, 在翻译过程中的行为或 程序执行记录在案 方式特征 环境(带或不带 发出诊断消息),以 终止翻译或执行 (随着诊断的发布 消息)。
3 示例 一个例子 未定义的行为是 整数溢出。
3.4.4 1 未指定行为 使用未指定值或其他行为 其中本国际标准 提供两种或多种可能性,以及 对以下方面没有进一步的要求 在任何情况下都选择哪个
2 示例:未指定的示例 行为是 计算函数的参数。
评论
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
launch_missiles()
未定义行为与未指定行为对此进行了简短描述。
他们的最终总结:
总而言之,未指定的行为通常是您不应该做的事情 担心,除非您的软件需要可移植。 相反,未定义的行为总是不可取的,也不应该 发生。
评论
也许更简单的措辞可能比严格的标准定义更容易理解。
实现定义的行为:
该语言说我们有数据类型。编译器供应商指定他们应该使用的大小,并提供他们所做的事情的文档。
未定义的行为:
你做错了什么。例如,您在不适合 .你如何把这个价值放进去?其实是没办法的!任何事情都可能发生,但最明智的做法是获取该 int 的第一个字节并将其放入 .这样做来分配第一个字节是错误的,但这就是引擎盖下发生的事情。int
char
char
char
未指定行为:
这两个函数中的哪一个首先执行?
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有太多的“事物”没有定义,没有具体说明,这很容易说,甚至举例,但很难解决。还应该注意的是,避免大多数问题并生成可移植代码并不是那么困难。
评论
fun(fun1(), fun2());
"implementation defined"
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
can
未定义的行为是 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
未定义行为的其他示例包括
- 访问超出其边界的数组,
- 除以零,
- 取消引用空指针,
- 在对象生存期结束后访问对象,或者
- 写一些据称很聪明的表达方式,比如.
i++ + ++i
[intro.defs] 还定义了未定义行为的两个不太危险的兄弟,未指定行为和实现定义的行为:
实现定义的行为 [defns.impl.defined]
行为,对于一个格式良好的程序构造和正确的数据,这取决于实现和每个实现文档
未指定行为 [defns.unspecified]
行为,对于格式良好的程序构造和正确的数据,这取决于实现
[注意:不需要实现来记录发生的行为。 本文档通常描述可能的行为范围。 — 结束语]
未定义的行为 [defns.undefined]
本文档不施加任何要求的行为
[注意:当本文档省略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为。 允许的未定义行为包括完全忽略结果不可预测的情况,到在翻译或程序执行过程中以环境特征的记录方式(有或没有发出诊断消息)的行为,再到终止翻译或执行(发出诊断消息)。[...] — 结束语]
您可以做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道自己在说什么的作者的好C++书籍。避免互联网教程。避免胡说八道。
评论
int f(){int a; return a;}
a
来自官方的 C Rationale 文档
术语“未指定行为”、“未定义行为”和“实现定义行为”用于对编写程序的结果进行分类,这些程序的属性是标准没有或不能完全描述的。采用这种分类的目的是允许实现之间有一定的多样性,从而允许实施质量成为市场上的一股活跃力量,并允许某些流行的扩展,而不会消除符合标准的声望。该标准的附录 F 对属于这三类之一的行为进行了分类。
未指定的行为为实现者在翻译程序时提供了一定的自由度。这种自由度不会延伸到无法翻译程序的程度。
未定义的行为为实现者提供了不捕获某些难以诊断的程序错误的许可证。它还确定了可能符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来增强语言。
实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释这种选择。指定为实现定义的行为通常是用户可以根据实现定义做出有意义的编码决策的行为。实现者在决定实现定义应该有多广泛时,应牢记这一标准。与未指定的行为一样,仅仅无法转换包含实现定义行为的源代码并不是一个充分的响应。
评论
C++ 标准 n3337 § 1.3.10 实现定义的行为
行为,对于一个格式良好的程序构造和正确的数据,那 取决于实施和每个实施文件
有时,C++ 标准不会对某些构造施加特定行为,而是说必须通过特定的实现(库版本)选择和描述特定的、定义良好的行为。因此,即使标准没有描述这一点,用户仍然可以确切地知道程序将如何运行。
C++ 标准 n3337 § 1.3.24 未定义的行为
本国际标准不设要求的行为 [ 注意:当这个国际 标准省略了任何明确的行为定义,或者当程序 使用错误的构造或错误的数据。允许的未定义 行为范围从完全忽略情况 不可预知的结果,在翻译或程序过程中的行为 以记录在案的方式执行环境特征 (发出或不发出诊断消息),直至终止 翻译或执行(发布诊断 消息)。许多错误的程序构造不会产生未定义的 行为;他们需要被诊断。— 尾注 ]
当程序遇到未根据 C++ 标准定义的构造时,它被允许做任何它想做的事情(也许给我发一封电子邮件,或者给你发一封电子邮件,或者完全忽略代码)。
C++ 标准 n3337 § 1.3.25 未指定行为
行为,对于一个格式良好的程序构造和正确的数据,那 取决于实现 [ 注意:实现不是 需要记录发生的行为。可能的范围 行为通常由本国际标准描述。— 完 注意 ]
C++ 标准并没有对某些构造施加特定的行为,而是说必须通过特定的实现(库版本)选择(但不一定描述)特定的、定义良好的行为。因此,在未提供描述的情况下,用户可能很难确切地知道程序的行为方式。
实施定义-
实现者希望,应该有很好的文档,标准给出了选择,但一定要编译
未指定 -
与已定义但未记录的实现相同
定义-
任何事情都可能发生,照顾好它。
评论
uint32_t s;
1u<<s
s
1u<<s
s
s
从历史上看,实现定义的行为和未定义的行为都代表了这样的情况:标准的作者希望编写高质量实现的人使用判断来决定哪些行为保证(如果有的话)对在预期目标上运行的预期应用程序字段中的程序有用。高端数字运算代码的需求与低级系统代码的需求完全不同,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
然而,当今一些编译器编写者的首选理念是,因为只有当程序要从事未定义行为时才能为负数,所以没有理由让程序剪辑负范围。尽管每个重要的编译器都支持负值的左移,并且大量现有代码依赖于该行为,但现代哲学会解释标准说左移负值是UB的事实,这意味着编译器编写者应该随意忽略这一点。v
v
评论
<<
i+j>k
(int)((unsigned)i+j) > k
i
j
k
foo(x, y, x)
i+j > k
x+y > x
y > 0
x
y
y
未定义的行为是丑陋的——就像“好的、坏的和丑陋的”。
好:一个编译和工作的程序,出于正确的原因。
Bad:有错误的程序,编译器可以检测和抱怨的那种错误。
丑陋:一个有错误的程序,编译器无法检测和警告,这意味着程序编译,有时似乎可以正常工作,但有时也会奇怪地失败。这就是未定义的行为。
一些程序语言和其他形式化系统试图限制“未定义的鸿沟”——也就是说,它们试图安排事物,使大多数或所有程序要么是“好的”,要么是“坏的”,而很少有程序是“丑陋的”。然而,C 语言的一个特征是它的“未定义鸿沟”相当宽。
评论
下一个:什么是对象切片?
评论