未定义行为

< cpp‎ | language

若违反某些规则,则令整个程序失去意义。

解释

C++ 标准为不被归入下列分类之一的每个程序都精确定义了其可观察行为

  • 非良构(ill-formed)——程序拥有语法错误或可诊断的语义错误。遵从标准的 C++ 编译器必须为此给出诊断,即使它定义了为这种代码赋予了含义的语言扩展(例如用非常量长度数组)也应如此。标准文本用 shall(应当)shall not(不应当)ill-formed(非良构)给出了这些要求。
  • 非良构而不要求诊断(ill-formed no diagnostic required)——程序拥有通常情况下可能无法诊断的语义错误(例如 ODR 的违规或者其他只能在连接时检测的错误)。若执行这种程序则行为未定义。
  • 由实现定义的行为(implementation-defined behavior)——程序的行为随实现而变动,遵从标准的实现必须为每个这样的行为的效果提供文档。例如 std::size_t 的类型或字节中的位数,或者 std::bad_alloc::what 的文本。由实现定义的行为的一个子集是本地环境特定行为(locale-specific behavior),它取决于实现所提供的本地环境
  • 未指明的行为( unspecified behavior )——程序的行为随实现而变动,而不要求遵从标准的实现为每个行为的效果提供文档。例如求值顺序,等同的字符串字面量是否为相异对象,数组分配的开销,等等。每个未指明行为均产生合法结果集合中的一个结果。
  • 未定义行为(undefined behavior,UB)——对程序的行为无任何限制。未定义行为的例子是数组边界外的内存访问,有符号整数溢出,空指针的解引用,在表达式中对同一标量多于一次中间无序列点 (C++11 前)无序 (C++11 起)的修改,通过不同类型的指针访问对象,等等。不要求编译器诊断未定义行为(尽管许多简单情形确实会得到诊断),而且不要求所编译的程序做任何有意义的事。

UB 与优化

因为正确的 C++ 程序不含未定义行为,故在启用优化选项以编译确实含有 UB 的程序时,编译器可能产生不期待的结果:

例如,

有符号溢出

int foo(int x) {
    return x+1 > x; // 要么为 true 要么因有符号溢出而致 UB
}

可编译为(演示

foo(int):
        movl    $1, %eax
        ret


边界外访问

int table[4] = {};
bool exists_in_table(int v)
{
    // 在头 4 次迭代中返回 true,或因边界外访问而致 UB
    for (int i = 0; i <= 4; i++) {
        if (table[i] == v) return true;
    }
    return false;
}

可能编译为(演示

exists_in_table(int):
        movl    $1, %eax
        ret

未初始化标量

std::size_t f(int x)
{
    std::size_t a;
    if(x) // x 非零或 UB
        a = 42;
    return a; 
}

可能编译为(演示

f(int):
        mov     eax, 42
        ret

以下给出的输出曾在旧版本 gcc 上观察到

bool p; // 未初始化局部变量
if(p) // UB :访问未初始化标量
    std::puts("p is true");
if(!p) // UB :访问未初始化标量
    std::puts("p is false");

可能的输出:

p is true
p is false

非法标量

int f() {
  bool b = true;
  unsigned char* p = reinterpret_cast<unsigned char*>(&b);
  *p = 10;
  // 从 b 读取现在是 UB
  return b == 0;
}

可编译成(演示

f():
        movl    $11, %eax
        ret

空指针解引用

int foo(int* p) {
    int x = *p;
    if(!p) return x; // 要么如上产生 UB,要么不可能采用此分支
    else return 0;
}
int bar() {
    int* p = nullptr;
    return *p;        // 无条件 UB
}

可能编译为( foo 用 gcc 、 bar 用 clang

foo(int*):
        xorl    %eax, %eax
        ret
bar():
        retq

访问已传递给 realloc 的指针

选择 clang 以观察所示输出

#include <iostream>
#include <cstdlib>
int main() {
    int *p = (int*)std::malloc(sizeof(int));
    int *q = (int*)std::realloc(p, sizeof(int));
    *p = 1; // UB :访问传递给 realloc 的指针
    *q = 2;
    if (p == q) // UB :访问传递给 realloc 的指针
        std::cout << *p << *q << '\n';
}

可能的输出:

12

无副作用的无限循环

选择 clang 以观察所示输出

#include <iostream>
 
int fermat() {
  const int MAX = 1000;
  int a=1,b=1,c=1;
  // 无副作用的无限循环是 UB
  while (1) {
    if (((a*a*a) == ((b*b*b)+(c*c*c)))) return 1;
    a++;
    if (a>MAX) { a=1; b++; }
    if (b>MAX) { b=1; c++; }
    if (c>MAX) { c=1;}
  }
  return 0;
}
 
int main() {
  if (fermat())
    std::cout << "Fermat's Last Theorem has been disproved.\n";
  else
    std::cout << "Fermat's Last Theorem has not been disproved.\n";
}

可能的输出:

Fermat's Last Theorem has been disproved.

外部链接

参阅