在C和C++中,什么是未定义行为(UB)?又有哪些未指定行为和实现定义行为呢?它们之间有何区别?
在C和C++中,什么是未定义行为(UB)?又有哪些未指定行为和实现定义行为呢?它们之间有何区别?
未定义行为是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
。具体而言,第1.3.24节规定:本国际标准中的语义描述定义了一个参数化的非确定性抽象机。
本国际标准将抽象机的某些方面和操作描述为实现定义的(例如,
sizeof(int)
)。这些构成了抽象机的参数。每个实现都应包括描述其特征和行为的文档。抽象机的某些其他方面和操作被描述为未指定的(例如,函数参数求值的顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机的非确定性方面。
某些其他操作被描述为未定义的(例如,取消引用空指针的效果)。[ 注:本国际标准对包含未定义行为的程序的行为没有任何要求。—end note ]
允许的未定义行为范围从完全忽略具有不可预测结果的情况到以环境特征为特征进行翻译或程序执行的文档方式行为(无论是否发出诊断消息),再到终止翻译或执行(发出诊断消息)。
你如何避免遇到未定义行为?基本上,你需要阅读由懂得自己在说什么的作者所写的好的 C++ 书籍。避免使用互联网教程。避免使用 Bullschildt。
int f(){int a; return a;}
:a
的值可能会在函数调用之间更改。 - Mark嗯,基本上这只是从C标准中直接复制粘贴过来的:
3.4.1 实现定义行为是指未指定行为,每个实现都会记录选择方式。int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
,编译器可以确定只要不发射导弹就会引起未定义行为,因此可以将调用 launch_missiles()
的行为变为无条件执行。 - supercat使用更简单的措辞可能比标准的严格定义更容易理解。
实现定义行为:
语言规定我们有数据类型。编译器厂商指定了它们应该使用的大小,并提供了他们所做的文档。
未定义行为:
您正在执行错误操作。例如,您在int
中有一个非常大的值,而它却无法容纳在char
中。那么该如何将该值放入char
中呢?实际上没有办法!任何事情都可能发生,但最明智的做法是将该int的第一个字节放入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?
该编程语言没有指定计算顺序,可能由左到右或由右到左!因此未指定的行为可能会导致未定义的行为,但是您的程序肯定不应该产生未指定的行为。sizeof(int)
的定义。因此,它不能说sizeof(int)
在程序的某些部分为4,而在其他部分为8。与未指定的行为不同,编译器可以说:“好吧,我要从左到右评估这些参数,并且下一个函数的参数将从右到左评估。”它可以发生在同一个程序中,这就是为什么它被称为未指定。事实上,如果一些未指定的行为被指定,C++可能会更容易。在这里查看Stroustrup博士的回答:fun(fun1(), fun2());
这条语句,它的行为难道不是“实现定义”的吗?毕竟编译器必须选择其中一种执行顺序,对吗? - Lazer来自官方 C 语言解释文档
术语 未指定行为(unspecified behavior)、未定义行为(undefined behavior) 和 实现定义行为(implementation-defined behavior) 被用于对编写程序的结果进行分类,这些程序的属性标准没有或不能完全描述。采用此分类的目的是允许不同实现之间存在一定的差异,从而使得实现质量成为市场上的一个活跃因素,并允许某些流行的扩展,同时不会削弱符合标准的含金量。标准附录 F 列出了这三种行为中属于哪一类。
未指定行为 使实现者在翻译程序时有一定的自由度。但这种自由度并不包括无法翻译程序的情况。
未定义行为 允许实现者不捕获某些难以诊断的程序错误。它还确定了可能符合语言扩展的领域:实现者可以通过提供正式未定义行为的定义来扩展语言。
实现定义行为 给实现者选择适当方法的自由,但要求向用户解释这个选择。通常被指定为实现定义的行为是那些用户可以根据实现定义做出有意义的编码决策的行为。在决定实现定义应该多么广泛时,实现者应该记住这个标准。与未指定行为一样,简单地无法翻译包含实现定义行为的源代码不是一个足够的响应。
实现定义(Implementation defined)-
实现者希望能够被很好地记录下来,标准给出了选择但一定要保证能够编译
未指定(Unspecified)-
与实现定义相同,但没有文档记录
未定义(Undefined)-
任何事情都有可能发生,请注意。
uint32_t s;
,当s
为33时评估 1u<<s
可能会产生0或2,但不会出现其他奇怪的情况。然而,新编译器评估 1u<<s
可能会导致编译器确定因为s
之前必须小于32,所以任何在该表达式之前或之后只有在s
大于等于32时才相关的代码可能会被省略。 - supercat从历史上看,实现定义行为(Implementation-Defined Behavior)和未定义行为(Undefined Behavior)都代表标准的作者期望编写高质量实现的人能够根据判断力决定程序所需的行为保证是否有用于既定应用领域在既定目标上运行的程序。高端数值计算代码的需求与低层系统代码的需求完全不同,而无论是UB还是IDB都允许编译器编写人员灵活地满足这些不同的需求。这两类都没有强制要求实现必须以对任何特定目的或甚至对任何目的都有用的方式运行。然而,声称适用于特定目的的高质量实现应该表现出符合这样目的的行为,无论标准是否要求。
实现定义行为和未定义行为之间唯一的区别是前者要求实现定义和记录一致的行为,即使在实现无论如何操作也不会有用的情况下也要如此。它们之间的分界线不在于实现定义行为通常是否有用(编译器编写人员应该在实际可行的情况下定义有用的行为,无论标准是否要求),而是在于是否可能存在定义行为同时具有成本和无用性的实现。这样的实现可能存在的判断并不以任何方式、形式或形态意味着对在其他平台上支持定义行为的有用性的任何判断。
不幸的是,自20世纪90年代中期以来,编译器编写人员开始将行为保证缺失解释为即使在它们是至关重要的应用领域和成本几乎为零的系统上也不值得付出代价的判断。编译器编写人员不再将未定义行为视为行使合理判断的邀请,而是将其视为一种借口来不这样做。
例如,给定以下代码:
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(未定义行为)事实上是一个麻烦的小陷阱,很高兴被提醒了! - Tom Swirlyi+j>k
在溢出时产生1或0,只要它没有任何其他副作用,编译器可能能够进行一些巨大的优化,而如果程序员将代码编写为 (int)((unsigned)i+j) > k
就不可能实现这些优化。 - supercati
、j
和k
是编译器在函数调用foo(x, y, x)
中展开的函数的参数。在这种情况下,编译器可以将i+j > k
替换为x+y > x
,然后再将其替换为y > 0
,跳过加法运算,消除对x
值的任何依赖,并可能允许编译器消除比较和对y
确切值的任何依赖,如果它能确定y
始终为正数。 - supercat未定义行为是“丑陋的”--就像“好的、坏的和丑陋的”一样。
好的:一个编译并以正确的原因工作的程序。
坏的:一个程序存在错误,编译器可以检测到并抱怨的错误类型。
丑陋的:一个程序存在错误,编译器无法检测和警告,这意味着程序编译,有时似乎正常工作,但也会偶尔失败。这就是未定义行为。
一些编程语言和其他形式系统努力限制“未定义性的鸿沟”--也就是说,他们试图安排事情,使得大多数或所有程序都是“好的”或“坏的”,而很少有“丑陋的”。然而,C语言的“未定义性鸿沟”相当宽广,这是其特征之一。