什么是C 和 C++ 中的未定义行为(UB)?未指定的行为和实现定义的行为呢?它们之间有什么区别?
9 回答
未定义的行为是 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)
)。这些构成了抽象机的参数。每个实现都应包括描述其在这些方面的特征和行为的文档。抽象机的某些其他方面和操作在本国际标准中描述为未指定(例如,函数参数的评估顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的不确定性方面。
本国际标准中将某些其他操作描述为未定义(例如,取消引用空指针的效果)。[注意:本国际标准对包含未定义行为的程序的行为没有要求。——<em>尾注]
具体来说,第 1.3.24 节规定:
允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(有发出的诊断消息)。
您可以做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道他们在说什么的作者写的好的 C++ 书籍。避免使用互联网教程。避免公牛。
好吧,这基本上是标准的直接复制粘贴
3.4.1 1实现定义的行为未指定的行为,其中每个实现都记录了如何做出选择
2 示例 实现定义行为的一个示例是当有符号整数右移时高位的传播。
3.4.3 1未定义的行为 使用不可移植或错误程序结构或错误数据时的行为,本国际标准对此没有要求
2 注 可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(有发出诊断消息)。
3 示例 未定义行为的一个示例是整数溢出行为。
3.4.4 1未指定的行为使用未指定的值,或本国际标准提供两种或多种可能性并且在任何情况下选择的其他行为都没有强加进一步要求
2 示例 未指定行为的一个示例是评估函数参数的顺序。
也许简单的措辞比标准的严格定义更容易理解。
实现定义的行为
语言说我们有数据类型。编译器供应商指定他们应该使用什么大小,并提供他们所做工作的文档。
未定义的行为
你做错了什么。例如,您有一个非常大的值 inint
不适合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?
语言没有指定评估,从左到右或从右到左!因此,未指定的行为可能会也可能不会导致未定义的行为,但您的程序肯定不应该产生未指定的行为。
@eSKay我认为你的问题值得编辑答案以澄清更多:)
因为
fun(fun1(), fun2());
行为不是“实现定义”吗?毕竟,编译器必须选择其中一门课程?
实现定义和未指定之间的区别在于,编译器应该在第一种情况下选择一种行为,但在第二种情况下不必这样做。例如,一个实现必须有一个且只有一个sizeof(int)
. 因此,不能说sizeof(int)
程序的某些部分是 4,而其他部分是 8。与未指定的行为不同,编译器可以说 OK 我将从左到右评估这些参数,而下一个函数的参数从右到左评估。它可以发生在同一个程序中,这就是为什么它被称为unspecified。事实上,如果指定了一些未指定的行为,C++ 可能会变得更容易。看看Stroustrup 博士对此的回答:
据称,赋予编译器这种自由和要求“普通的从左到右的评估”之间可以产生的差异可能是显着的。我不相信,但是由于无数编译器“在那里”利用了这种自由,而且有些人热情地捍卫这种自由,因此改变将是困难的,并且可能需要几十年才能渗透到 C 和 C++ 世界的遥远角落。令我失望的是,并非所有编译器都会警告诸如
++i+i++
. 同样,未指定参数的评估顺序。IMO 太多的“事物”未定义,未指定,这很容易说,甚至可以举出例子,但很难解决。还应该注意的是,避免大多数问题并生成可移植代码并不是那么困难。
来自官方的 C 基本原理文档
术语未指定的行为、未定义的行为和实现定义的行为用于对编写标准没有或不能完全描述其属性的程序的结果进行分类。采用这种分类的目的是允许实现之间的某种多样性,从而允许实现质量成为市场上的积极力量,并允许某些流行的扩展,而不会消除符合标准的声望。标准附录 F 列出了属于这三类之一的行为。
未指定的行为给了实现者一些翻译程序的自由度。这个自由度不会延伸到无法翻译程序的程度。
未定义的行为允许实现者不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来扩充语言。
实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释该选择。指定为实现定义的行为通常是用户可以根据实现定义做出有意义的编码决策的行为。实施者在决定实施定义的范围时应牢记这一标准。与未指定的行为一样,仅仅未能翻译包含实现定义的行为的源代码并不是一个适当的响应。
Undefined Behavior vs. Unspecified Behavior有一个简短的描述。
他们的最后总结:
总而言之,未指定的行为通常是您不应该担心的,除非您的软件需要可移植。相反,未定义的行为总是不可取的,永远不应该发生。
实施定义-
实现者希望,应该有据可查,标准提供选择,但一定要编译
未指定 -
与实现定义但未记录的相同
不明确的-
任何事情都有可能发生,请注意它。
从历史上看,实现定义的行为和未定义的行为都代表了这样一种情况,标准的作者期望编写高质量实现的人会使用判断来决定哪些行为保证(如果有的话)对在预期应用程序领域中运行的程序有用。预期目标。高端数字运算代码的需求与低级系统代码的需求大不相同,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暗示编译器编写者应该随意忽略这一点。
C++ 标准 n3337 § 1.3.10 实现定义的行为
行为,对于格式良好的程序构造和正确的数据,这取决于实现和每个实现文档
有时,C++ 标准不会对某些结构施加特定的行为,而是说必须通过特定的实现(库的版本)来选择和描述特定的、明确定义的行为。因此,即使标准没有描述这一点,用户仍然可以准确地知道程序的行为方式。
C++ 标准 n3337 § 1.3.24 未定义行为
本国际标准未强加要求的行为 [注:当本国际标准省略任何明确的行为定义或程序使用错误构造或错误数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。——尾注]
当程序遇到未根据 C++ 标准定义的构造时,它可以做任何它想做的事情(可能给我发一封电子邮件,或者给你发一封电子邮件,或者完全忽略代码)。
C++ 标准 n3337 § 1.3.25 未指定行为
行为,对于格式良好的程序构造和正确的数据,这取决于实现 [注意:实现不需要记录发生的行为。可能的行为范围通常由本国际标准描述。——尾注]
C++ 标准没有对某些结构施加特定的行为,而是说必须通过特定的实现(库的版本)来选择特定的、明确定义的行为(bot 不需要描述)。因此,在没有提供描述的情况下,用户可能很难确切地知道程序的行为方式。
未定义的行为是丑陋的——例如,“好的、坏的和丑陋的”。
好:出于正确的原因,可以编译和工作的程序。
坏:程序有错误,编译器可以检测到并抱怨。
丑陋:一个程序有错误,编译器无法检测和警告,这意味着程序可以编译,并且有时看起来可以正常工作,但有时也会奇怪地失败。这就是未定义的行为。
一些程序语言和其他形式系统努力限制“不确定性的鸿沟”——也就是说,他们试图安排事情,使大多数或所有程序要么“好”,要么“坏”,而很少有“丑陋”的程序”。然而,C 的一个特征是它的“不确定性鸿沟”非常宽。