介绍
在C或C ++中进行编程,您可能已经听过“不确定的行为”一词,至少知道它很糟糕。但是,这是什么呢?有什么例子?为什么它存在?
C的简短历史
c是由丹尼斯·里奇(Dennis Ritchie)在1970年代初在Bell Labs创建的,并于1978年通过出版The C Programming Language介绍给世界。
对于那些对C的更详细历史感兴趣的人,请参见The Development of the C Language,Dennis M. Ritchie,1993年4月。
在1989年第一个C标准之前,有两种C:
的口味-
无法携带的C :使用用于特定硬件的任何技术,用于编程操作系统的一个步骤。
-
半便携式C :使用
#ifdef
制作了许多程序``半便士'',因此它们在所有考虑的硬件和操作系统上进行编译并运行相同。
随着C的流行增长,很明显需要一个标准。 c编程语言 十多年来的C确定描述,但它不足以成为标准。对于标准,您理想地希望它准确地指定语言各个方面的每种情况。
为什么存在不确定的行为?
然而,到1980年代中期,有许多不可支配和半便携式的程序 。问题在于,使用编译器1在计算机1上进行X产生结果R1,而在计算机2上使用编译器2进行X产生R2的X及其各自的程序依赖于这些结果。通常,标准必须要求结果R1或R2中只有一个是正确的。更新的标准符合编译器将打破许多程序。
不执行此操作,实施的概念定义了行为,未指定的行为和 不确定的行为是作为标准的一部分发明的作为逃避孵化场,允许许多工作计划继续与标准配合的编译器合作。
虽然这似乎并不完全令人满意,但明确地指出X是定义,未指定或未定义的实施之一,这是对X。
完全没有说明的改进。BAD的三冠王
在koude1 FAQ中解释了C和C ++的三个有点不良行为之间的差异,问题11.33:
-
实施定义的行为:实施必须选择某些行为,必须是一致的,并且必须必须 /em>被记录。
-
未指定的行为:像实现一样,除了不需要记录。
-
未定义的行为:完全可以发生任何事情。该程序可能执行不正确(崩溃或默默地生成错误的结果),也可能幸运地完成程序员的意图。
实施定义行为的一个示例是:
new_offset = ftell( f ) + fread( buf, 1, n, f );
此行为是定义的,因为评估了
未指定。+
操作数的顺序(左,右;右,然后是左)。
虽然实施定义的行为不好,但至少是一致的(使用同一平台,编译器和编译器标志)。
未定义的行为 在那:
- 同一可执行文件的不同运行可以产生不同的结果!
- 同一运行可执行文件可以在不同的时间产生不同的结果!
未定义的行为示例
C和C ++(因为它从C继承了C)列出了数百件导致不确定行为的事物。常见的示例包括:
- 签名的整数溢出和下底。
- 对象在其寿命之外被提及。
- 使用指向寿命已经结束的对象的指针。
- 从非初始化的对象读取。
- 无关阵列指针的加法/减法。
- 索引超过数组的末端。
- 修改const对象。
- 数据竞赛。
- ...
- 任何未明确列出的任何事物,定义的行为或未指定的行为是未定义的行为!
含义和示例
但是不确定的行为有什么含义?
-
允许编译器假设不确定的行为 从不 发生,因此所有程序都是有效的。
-
这允许编译器生成非常有效的代码,尤其是在紧密循环中。 (这是 关于不确定行为的好处。)
一个可能导致未定义行为的简单示例是:
bool no_overflow( int x ) {
return x+1 > x;
}
通常,您永远不会像这样编写愚蠢的代码;但是,这种代码有时可以从宏扩展或模板实例化中发生,因此编译器应该做得很好。
编译器将毫不奇怪地生成以下优化的x86-64 assembly:
no_overflow:
movl $1, %eax ; return true
ret
因为x+1
是始终> x
或它是纯粹的数学。但是,计算机数学的精度有限,因此有两种可能的情况:
-
x != INT_MAX
:+
的行为定义明确;必须返回true
。 -
x == INT_MAX
:+
的行为不确定;可以做任何事情。
允许编译器假设情况2从未发生。为什么?因为考虑这种情况的唯一原因是,如果编译器可以检查并对此做些事情,例如重写代码,就好像是:
return x != INT_MAX && x+1 > x;
但这将插入您没有要求的检查;对于大多数情况,这将效率较低。开发人员通常在C和C ++上写入性能以进行性能,因此插入此类代码是对立的。
不确定行为的两个部分
实际上有两个不确定行为的部分:
-
实际在运行时执行未定义的行为;示例:
- 删除无效指针。
- 索引超过数组的末端。
- ...
-
允许编译器假设不确定的行为永远不会发生(一个错误的前提)使其有时会产生令人惊讶(但非常有效)的代码。在逻辑上,如果您接受虚假的前提,则可以得出任何结论。例如:
- 如果街道潮湿,最近下雨了。 (错误的前提。)
- 街道是湿的。
- 因此,它最近下雨了。 (结论:逻辑上有效,但错误。)
这是引起最惊喜的第二部分。另一个例子,请考虑:
extern int table[4];
bool exists_in_table( int v ) {
for ( int i = 0; i <= 4; ++i ) {
if ( table[i] == v )
return true;
}
return false;
}
编译器将出奇地生成以下优化的x86-64 assembly:
exists_in_table(int):
movl $1, %eax ; return true
ret
那怎么可能? for
循环和if
去哪儿了?该问题源于代码具有错误的事实。 (您注意到了吗?)错误是i <= 4
应该是i < 4
。但是即便如此,编译器如何生成return true
?理由是:
- 第一个通过循环的四次,该功能可能返回
true
。 - 如果
i
是4
,则代码将执行未定义的行为(通过尝试访问数组末尾的元素)。 - 允许编译器假设不确定的行为永远不会发生(所有程序都是有效的);所以:
- 可变的
i
可以永远是4
。 (错误的前提。) - 暗示我们 必须 在
i < 4
时发现了匹配。 - 因此,我们总是可以返回
true
。 (结论:逻辑上有效,但错误。)
- 可变的
这是 不是 编译器错误。鉴于假设程序员编写了一个有效的程序与无效程序之间的选择,我们告诉编译器选择以前的程序,并相应地优化。
优化可以使情况变得更糟
考虑以下内容:
void assign_not_null( int *p, int v ) {
int old_v = *p;
if ( p == nullptr )
return;
*p = v;
}
行int old_v = *p
是死亡代码。 (大概是用old_v
做某事的功能,但是该代码是重写的,并且这条线被错误地遗留在现实世界中。 ,取决于编译器执行的优化以及以什么顺序可能导致不确定的行为。
假设编译器至少有两个优化:
- 死亡代码消除:未使用的代码被消除。
-
冗余NULL检查消除:如果编译器可以推断出特定的指针可能在给定的行上为null,则消除了
if
检查null的检查。
假设编译器按以上顺序进行优化。因此,它将:
- 消除
int old_v = *p
,因为不使用old_v
。 - 因为
if ( p == nullptr )
是下一行的*p = v
之前的必要检查,因此可以消除。
到目前为止,一切都很好。但是,如果编译器以相反顺序进行优化怎么办?然后将:
- 知道删除零指针是不确定的行为,并且被允许假设不确定的行为永远不会发生,这意味着:
- 如果代码到达
if
,那么上一行的*p
必须已成功。 - 这意味着
p
永远不会为空。 - 因此,零检查是不必要的,因此可以消除
if
。
- 如果代码到达
- 现在,它执行了消除代码并消除
int old_v = *p
。
结果代码将是:
void assign_not_null( int *p, int v ) {
*p = v;
}
逻辑上有效,但是错误。
深度示例
有关奇异错误的深入示例未定义的行为可能会导致(以及如何查找和修复它们),请参见The Curious Case of the Disappearing “if”。
其他语言中未定义的行为
在这一点上,您可能想知道是否以其他语言存在不确定的行为。通常,答案是没有两个例外的:
- 如果一种语言提供了执行不安全操作的机制,则通常可以执行不确定的行为。
- 数据竞赛是始终 不确定的行为。
Ada具有相似但较弱的有限错误概念。
但对于具有始终定义的行为的语言,支付的价格是在性能中:
- 总是初始化变量。
- 总是检查数组索引。
- 垃圾收集。
- ...
结论
未定义的行为是获得标准的最小巴德折衷。
结语
要开车回家,即不确定的行为意味着 ,约翰·伍兹(John Woods
From: John F. Woods
Newsgroups: comp.lang.c
Date: Feb 25, 1992, 11:51:52 AM
> * Undefined behavior -- behavior, upon use of a nonportable or
> erroneous program construct, ... for which the standard imposes
> no requirements.
Permissible undefined behavior ranges from
> ignoring the situation completely
with unpredictable results,
> to having demons fly out of your nose.
In short, you can't use sizeof() on a structure whose elements
haven't been
defined, and if you do, demons may fly out of your
nose.
OK, OK; so the Standard doesn't *ACTUALLY* mention demons or
noses. Not as
such, anyway.
其他人跟进了卡住的鼻恶魔一词。