C和C ++的不确定行为
#c #cpp

介绍

在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或它是纯粹的数学。但是,计算机数学的精度有限,因此有两种可能的情况:

  1. x != INT_MAX
+的行为定义明确;必须返回true
  2. x == INT_MAX
+的行为不确定;可以做任何事情。

允许编译器假设情况2从未发生。为什么?因为考虑这种情况的唯一原因是,如果编译器可以检查并对此做些事情,例如重写代码,就好像是:

    return x != INT_MAX && x+1 > x;

但这将插入您没有要求的检查;对于大多数情况,这将效率较低。开发人员通常在C和C ++上写入性能以进行性能,因此插入此类代码是对立的。

不确定行为的两个部分

实际上有两个不确定行为的部分:

  1. 实际在运行时执行未定义的行为;示例:

    • 删除无效指针。
    • 索引超过数组的末端。
    • ...
  2. 允许编译器假设不确定的行为永远不会发生(一个错误的前提)使其有时会产生令人惊讶(但非常有效)的代码。在逻辑上,如果您接受虚假的前提,则可以得出任何结论。例如:

    • 如果街道潮湿,最近下雨了。 (错误的前提。)
    • 街道是湿的。
    • 因此,它最近下雨了。 (结论:逻辑上有效,但错误。)

这是引起最惊喜的第二部分。另一个例子,请考虑:

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?理由是:

  1. 第一个通过循环的四次,该功能可能返回true
  2. 如果i4,则代码将执行未定义的行为(通过尝试访问数组末尾的元素)。
  3. 允许编译器假设不确定的行为永远不会发生(所有程序都是有效的);所以:
    • 可变的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做某事的功能,但是该代码是重写的,并且这条线被错误地遗留在现实世界中。 ,取决于编译器执行的优化以及以什么顺序可能导致不确定的行为。

假设编译器至少有两个优化:

  1. 死亡代码消除:未使用的代码被消除。
  2. 冗余NULL检查消除:如果编译器可以推断出特定的指针可能在给定的行上为null,则消除了if检查null的检查。

假设编译器按以上顺序进行优化。因此,它将:

  1. 消除int old_v = *p,因为不使用old_v
  2. 因为if ( p == nullptr )是下一行的*p = v之前的必要检查,因此可以消除。

到目前为止,一切都很好。但是,如果编译器以相反顺序进行优化怎么办?然后将:

  1. 知道删除零指针是不确定的行为,并且被允许假设不确定的行为永远不会发生,这意味着:
    • 如果代码到达if,那么上一行的*p 必须已成功。
    • 这意味着p永远不会为空。
    • 因此,零检查是不必要的,因此可以消除if
  2. 现在,它执行了消除代码并消除int old_v = *p

结果代码将是:

void assign_not_null( int *p, int v ) {
    *p = v;
}

逻辑上有效,但是错误

深度示例

有关奇异错误的深入示例未定义的行为可能会导致(以及如何查找和修复它们),请参见The Curious Case of the Disappearing “if”

其他语言中未定义的行为

在这一点上,您可能想知道是否以其他语言存在不确定的行为。通常,答案是没有两个例外的:

  1. 如果一种语言提供了执行不安全操作的机制,则通常可以执行不确定的行为。
  2. 数据竞赛是始终 不确定的行为。

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.

其他人跟进了卡住的鼻恶魔一词。

参考