C和C ++中未定义,未指定和实现定义的行为有什么区别?
答案 0 :(得分:366)
未定义的行为 是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 ++标准有唯一的投票期限。
未定义行为的其他示例包括访问超出其边界的数组,dereferencing the null pointer,accessing objects after their lifetime ended或编写i++ + ++i
之类的allegedly clever expressions。
C ++标准的第1.9节还提到了未定义行为的两个不那么危险的兄弟,未指明的行为和实现定义的行为:
本国际标准中的语义描述定义了参数化的非确定性抽象机器。
抽象机的某些方面和操作在本国际标准中描述为实现定义(例如,
sizeof(int)
)。这些构成了抽象机器的参数。每个实施应包括描述其在这些方面的特征和行为的文件。抽象机器的某些其他方面和操作在本国际标准中描述为未指定(例如,对函数参数的评估顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的非确定性方面。
本国际标准中将某些其他操作描述为 undefined (例如,取消引用空指针的效果)。 [注意:此国际标准对包含未定义行为的程序的行为不施加任何要求。 - 结束注释]
具体而言,第1.3.24节规定:
允许的未定义行为范围从完全忽略情况,结果不可预测,在翻译或程序执行期间以环境特征的文件化方式行事(有或没有发出诊断消息),终止翻译或执行(发布诊断信息)。
您可以做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道他们在谈论什么的作者的good C++ books。螺丝网络教程。螺旋公牛队。
答案 1 :(得分:91)
嗯,这基本上是标准
的直接复制粘贴3.4.1 1 实施定义的行为未指明的行为在哪里 每个实施文件如何 做出选择
2示例一个例子 实现定义的行为是 当高阶位传播时 有符号整数向右移动。
3.4.3 1 未定义的行为行为,使用不可移植或错误的行为 程序构造或错误的 数据,本国际 标准没有要求
2 注意可能的未定义行为 范围从忽视这种情况 完全有不可预知的结果, 在翻译期间表现 程序执行记录在案 方式的特点 环境(有或没有 发出诊断信息) 终止翻译或执行 (随着诊断的发布 消息)。
3示例一个例子 未定义的行为是行为 整数溢出。
3.4.4 1 未指明的行为使用未指定的值或其他行为 这个国际标准在哪里 提供两种或更多种可能性 没有进一步的要求 在任何情况下都会选择
2 示例未指定的示例 行为是其中的顺序 评估函数的参数。
答案 2 :(得分:55)
也许简单的措辞可以比标准的严格定义更容易理解。
实施定义的行为
该语言表示我们有数据类型。编译器供应商指定他们使用的大小,并提供他们所做的文档。
未定义的行为
你做错了什么。例如,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,我将从左到右评估这些参数,并且从右到左评估下一个函数的参数。它可能发生在同一个程序中,这就是为什么它被称为 未指定 。实际上,如果指定了一些未指定的行为,C ++可能会变得更容易。请看Dr. Stroustrup's answer for that:
声称有所不同 什么可以产生给予 编译器这种自由和 要求“普通的从左到右 评估“可能很重要。我是 不相信,但无数 编制者“在那里”占据优势 自由和一些人 热情地捍卫自由,a 改变将是困难的,可能 需要几十年才能渗透到 C和C ++的遥远角落 世界。我很失望,不是全部 编译器警告代码如 ++我+ I ++。同样,参数的评估顺序是 未指定的。海事组织遗留了太多“事情” 未定义,未指定, 实现定义等。但是, 这很容易说,甚至可以给予 例子,但很难修复。它 还应该指出的是,它不是 所有这些都很难避免 问题和生产便携 代码。
答案 3 :(得分:24)
来自官方C原理文件
术语未指定行为,未定义行为和实现定义行为用于对编写其属性为标准的程序的结果进行分类不能或不能完全描述。采用这种分类的目的是允许实现中的某种变化,这允许实现的质量成为市场中的主动力量以及允许某些流行的扩展,而不去除与标准的一致性的标记。标准的附录F对属于这三个类别之一的行为进行了编目。
未指定的行为为实现者提供了翻译程序的一些自由度。只要没有翻译程序,这个范围就不会延伸。
未定义的行为使实施者许可证不会捕获难以诊断的某些程序错误。它还标识了可能符合语言扩展的区域:实现者可以通过提供正式未定义行为的定义来扩充语言。
实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释此选择。指定为实现定义的行为通常是用户可以基于实现定义做出有意义的编码决策的行为。在决定实施定义应该有多广泛时,实施者应该牢记这个标准。与未指定的行为一样,只是无法转换包含实现定义的行为的源不是一个充分的响应。
答案 4 :(得分:9)
Undefined Behavior vs. Unspecified Behavior有一个简短的描述。
他们的最后总结:
总而言之,未指明的行为通常是您不应该做的事情 担心,除非您的软件需要便携式。 相反,未定义的行为总是不受欢迎的,永远不应该 发生。
答案 5 :(得分:8)
历史上,实现定义行为和未定义行为都代表了标准作者期望编写质量实现的人会使用判断来决定哪些行为保证(如果有的话)对预期应用领域中的程序有用的情况。在预定目标上运行。高端数字运算代码的需求与低级系统代码的需求大不相同,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的事实。暗示编译器编写者应该随意忽略它。
答案 6 :(得分:6)
实施定义 -
实施者希望,应该有详细记录,标准提供选择,但确保编译
未指定 -
与实施定义相同但未记录
未定义 -
任何事情都可能发生,照顾它。
答案 7 :(得分:5)
C ++标准n3337 § 1.3.10 实施定义的行为
行为,对于格式良好的程序构造和正确的数据,即 取决于实施和每个实施文件
有时候C ++ Standard并没有对某些构造强加特定的行为,而是说必须选择特定的,明确定义的行为,并通过特定的实现(库的版本)描述。因此,即使标准没有描述,用户仍然可以确切知道程序的行为。
C ++标准n3337 § 1.3.24 未定义的行为
本国际标准没有要求的行为 [注意:本国际时可能会出现未定义的行为 标准省略了行为或程序的任何明确定义 使用错误的构造或错误的数据。允许的未定义 行为的范围从完全忽略情况 不可预知的结果,在翻译或程序期间表现 以文件化的方式执行环境特征 (有或没有发出诊断信息),终止 翻译或执行(发布诊断 信息)。许多错误的程序结构不会产生未定义的 行为;他们需要被诊断出来。 - 结束说明]
当程序遇到未根据C ++标准定义的构造时,允许它做任何想做的事情(可能发送电子邮件给我或者可能发送电子邮件给你或者可能完全忽略代码)。
C ++标准n3337 § 1.3.25 未指明的行为
行为,对于格式良好的程序构造和正确的数据,即 取决于实现[注意:实现不是 需要记录发生的行为。可能的范围 行为通常由本国际标准划定。 - 结束 注意]
C ++ Standard并没有对某些构造强加特定的行为,而是说必须通过特定的实现(库的版本)选择特定的,定义良好的行为( bot不必描述)。因此,在没有提供描述的情况下,用户很难确切知道程序的行为方式。
答案 8 :(得分:0)
未定义的行为是丑陋的——比如“好的、坏的和丑陋的”。
好:一个可以编译并运行的程序,原因是正确的。
坏:有错误的程序,编译器可以检测到并抱怨的类型。
丑陋:程序有错误,编译器无法检测并警告,这意味着程序可以编译,并且在某些时候似乎可以正常工作,但在某些情况下也会奇怪地失败的时间。这就是未定义行为。
一些程序语言和其他正式系统试图限制“不确定性鸿沟”——也就是说,他们试图安排事情,使大多数或所有程序要么“好”要么“坏”,而很少是“丑陋的”。然而,C 的一个特征是它的“不确定性鸿沟”相当广泛。
答案 9 :(得分:-3)
在某些情况下,有许多构造应该以有用和可预测的方式运行,但实际上在所有实现中都不能在所有情况下都这样做。通常,应使用构造的一组情况将取决于目标平台和应用程序领域。由于针对不同目标和领域的实施应处理不同的案件,因此本标准将处理哪些案件的问题视为实施质量问题。此外,由于该标准的作者认为没有必要禁止“符合”实施的质量太差以至于没有用处,因此他们通常不必费心明确要求他们期望所有非垃圾实施都可以支持的情况下的行为。即使没有授权。
例如,代码:
struct foo {int x;} = {0};
int main(void)
{
foo.x = 1;
return foo.x-1;
}
使用类型int
的左值[即foo.x
]访问类型为struct foo
的对象的存储值,即使N1570 6.5p7包含的内容都不允许访问类型为struct foo
的对象,除非通过类型为l的左值访问struct foo
或字符类型的左值,该标准也不包含任何可免除6.5p7要求的struct-member-access表达式的语言。
显然,任何不能处理简单的struct-member-access表达式的编译器都应被视为质量极低,并且可能不适合任何事物。因此,应该合理地预期,寻求提供高质量实施的任何人都将支持这种结构,而不管该标准是否要求它。只要可以信任编译器作者,就可以做出真正的努力来生产适合其预期用途的高质量编译器,并公开说明其编译器是否适用的目的,那么就没有理由使用标准废墨了。试图陈述显而易见的事情。实际上,许多应该具有可用行为和可预测行为的动作是未定义行为,因为标准的编写者相信编译器作者可以进行合理的判断,而不是以动作引用未定义行为为借口将判断排除在外