未定义行为

来自cppreference.com
< 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):
        mov     eax, 1
        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):
        mov     eax, 1
        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():
        mov     eax, 11
        ret

空指针解引用

仅仅解引用空指针是否为未定义行为是有争议的,见 CWG 问题 232。以下示例演示从这种解引用的结果读取。

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

可能编译为(演示

foo(int*):
        xor     eax, eax
        ret
bar():
        ret

访问已传递给 std::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

外部链接

参阅