什么是C和C ++中的未定义行为?未指定的行为和实现定义的行为呢?它们之间有什么区别?
什么是C和C ++中的未定义行为?未指定的行为和实现定义的行为呢?它们之间有什么区别?
Answers:
未定义的行为是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 ++书籍。螺丝互联网教程。螺丝废话。
int f(){int a; return a;}
:的值a
可能在函数调用之间改变。
好吧,这基本上是标准的直接复制粘贴
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; }
一个编译器可以确定,由于调用不发射导弹的功能的所有方法都调用了Undefined Behavior,因此可以使调用成为launch_missiles()
无条件的。
也许措辞比标准的严格定义更容易理解。
实现定义的行为
语言表示我们有数据类型。编译器供应商指定应使用的大小,并提供有关其所做工作的文档。
未定义的行为
您正在做错事。举例来说,您有个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太多的“事物”是未定义的,未指定的,实现定义的等等。但是,这很容易说,甚至给出示例,但很难解决。还应注意,避免大多数问题并生成可移植代码并不是很困难。
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基本原理文档
术语“ 未指定行为”,“ 未定义行为”和“ 实现定义的行为”用于对编写程序的结果进行分类,这些程序的属性标准不能或不能完全描述。采用此分类的目的是允许实现中的某种变化,这使实现的质量成为市场上的活跃力量,并允许某些流行的扩展,而不会消除对标准的崇高敬意。该标准的附录F列出了属于这三种类别之一的那些行为。
未指定的行为使实施者可以自由翻译程序。这种纬度不会扩展到无法翻译程序的程度。
未定义的行为使实施者可以不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供正式未定义行为的定义来扩展语言。
实施定义的行为使实施者可以自由选择适当的方法,但要求向用户说明这一选择。通常,指定为实现定义的行为是指用户可以根据实现定义做出有意义的编码决策的行为。在确定实施定义应有多广泛时,实施者应牢记此标准。与未指定的行为一样,仅不翻译包含实现定义的行为的源就不足为响应。
从历史上看,实现定义的行为和未定义的行为都代表了这样的情况,在该情况下,标准的作者期望编写高质量实现的人们将使用判断来决定哪些行为保证(如果有的话)将对运行在目标应用程序领域中的程序有用。预定目标。高端数字运算代码的需求与低层系统代码的需求完全不同,并且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为负数的事实是一个令人讨厌的小陷阱,我很高兴得到提醒!
i+j>k
加法溢出的情况下产生1还是0(如果没有其他副作用),那么编译器可能能够进行一些大规模的优化,而如果程序员将代码编写为,则是不可能的(int)((unsigned)i+j) > k
。
C ++标准n3337 § 1.3.10 实现定义的行为
行为,对于格式正确的程序构造和正确数据,取决于实施以及每个实施文档
有时,C ++ Standard并未在某些构造上强加特定的行为,而是说必须选择并描述特定的,定义明确的行为通过特定的实现(库版本)。因此,即使Standard并未对此进行描述,用户仍然可以确切知道程序的行为方式。
C ++标准n3337 § 1.3.24 未定义行为
本国际标准不施加任何要求的行为[注:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括:完全忽略具有不可预测结果的情况,以翻译或程序执行过程中以环境特征记录的方式表现的行为(带有或不带有诊断消息),终止翻译或执行(带有发布)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。—尾注]
当程序遇到未按照C ++标准定义的构造时,它可以做任何想做的事情(可以给我发送电子邮件,或者给你发送电子邮件,或者完全忽略代码)。
C ++标准n3337 § 1.3.25 未指定的行为
行为,对于格式正确的程序构造和正确数据,取决于实现[注:不需要实现来记录发生哪种行为。可能的行为范围通常由本国际标准来描述。—尾注]
C ++ Standard并未在某些构造上强加特定的行为,而是说必须通过特定的实现(库的版本)选择特定的,定义明确的行为(无需描述bot)。因此,在没有提供任何描述的情况下,用户可能很难确切知道程序的行为方式。
实施定义-
实施者希望,应该有充分的文件记录,标准可以选择,但一定要编译
未指定-
与实现定义相同,但未记录
未定义
可能发生任何事情,请谨慎处理。
uint32_t s;
,评估1u<<s
何时s
是33可能会产生0或可能产生2,但别无所求。但是,较新的编译器进行评估1u<<s
可能会导致编译器确定,因为s
事先必须小于32,因此s
可以省略该表达式之前或之后仅与32 相关的代码才有意义的任何代码。