Namespaces
Variants

Undefined behavior

From cppreference.net

C语言标准精确规定了C语言程序的 可观察行为 ,但以下类别除外:

  • undefined behavior - 程序行为不受任何限制。未定义行为的示例包括:数组边界外的内存访问、有符号整数溢出、空指针解引用、在无序列点的情况下对同一标量进行 多次修改 、通过不同类型指针访问对象等。编译器不必诊断未定义行为(尽管许多简单情况会被诊断),且编译后的程序不需要执行任何有意义操作。
  • 未指定行为 - 允许两种或多种行为,且实现方无需记录每种行为的具体影响。例如: 求值顺序 、相同的 字符串字面量 是否具有独立性等。每个未指定行为会产生一组有效结果中的某个结果,且在同一个程序中重复执行时可能产生不同结果。
  • 实现定义行为 - 一种未指定行为,各实现需记录其具体选择方式。例如:字节的位数,或有符号整数右移采用算术移位还是逻辑移位。

(注意: 严格符合规范 的程序不依赖于任何未指定、未定义或实现定义的行为)

编译器必须对任何违反C语法规则或语义约束的程序发出诊断信息(错误或警告),即使其行为被指定为未定义或实现定义,或者编译器提供了允许接受此类程序的语言扩展。对于未定义行为的诊断不作额外要求。

目录

UB与优化

由于正确的C程序不会出现未定义行为,当实际存在UB的程序在启用优化的情况下编译时,编译器可能会产生意外结果:

例如,

有符号整数溢出

int foo(int x)
{
    return x + 1 > x; // 结果可能为真,或因有符号整数溢出引发未定义行为
}

可能被编译为 ( 演示 )

foo:
        mov     eax, 1
        ret

越界访问

int table[4] = {0};
int exists_in_table(int v)
{
    // 在前4次迭代中返回1,或由于越界访问导致未定义行为
    for (int i = 0; i <= 4; i++)
        if (table[i] == v)
            return 1;
    return 0;
}

可以编译为 ( 演示 )

exists_in_table:
        mov     eax, 1
        ret

未初始化标量

_Bool p; // 未初始化的局部变量
if (p) // 未初始化标量的未定义行为访问
    puts("p is true");
if (!p) // 未初始化标量的未定义行为访问
    puts("p is false");

可能产生以下输出(在旧版本 gcc 中观察到):

p 为 true
p 为 false
size_t f(int x)
{
    size_t a;
    if (x) // 当x非零时正常执行,否则将导致未定义行为
        a = 42;
    return a;
}

可以编译为 ( 演示 )

f:
        mov     eax, 42
        ret

无效标量

int f(void)
{
    _Bool b = 0;
    unsigned char* p = (unsigned char*)&b;
    *p = 10;
    // 现在读取 b 的值是未定义行为
    return b == 0;
}

可编译为 ( 演示 )

f:
        mov     eax, 11
        ret

空指针解引用

int foo(int* p)
{
    int x = *p;
    if (!p)
        return x; // 要么在上一行出现未定义行为,要么此分支永远不会执行
    else
        return 0;
}
int bar()
{
    int* p = NULL;
    return *p;    // 必然的未定义行为
}

可能被编译为 ( 演示 )

foo:
        xor     eax, eax
        ret
bar:
        ret

传递给 realloc 的指针访问

选择 clang 以观察所示输出

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = (int*)malloc(sizeof(int));
    int *q = (int*)realloc(p, sizeof(int));
    *p = 1; // UB access to a pointer that was passed to realloc
    *q = 2;
    if (p == q) // UB access to a pointer that was passed to realloc
        printf("%d%d\n", *p, *q);
}

可能的输出:

12

无副作用的无限循环

选择 clang 以观察所示输出

#include <stdio.h>
int fermat()
{
    const int MAX = 1000;
    // 无副作用的无限循环是未定义行为
    for (int a = 1, b = 1, c = 1; 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(void)
{
    if (fermat())
        puts("Fermat's Last Theorem has been disproved.");
    else
        puts("Fermat's Last Theorem has not been disproved.");
}

可能的输出:

Fermat's Last Theorem has been disproved.

参考文献

  • C23 标准 (ISO/IEC 9899:2024):
  • 3.4 行为 (p: TBD)
  • 4 一致性 (p: TBD)
  • C17 标准 (ISO/IEC 9899:2018):
  • 3.4 行为 (p: 3-4)
  • 4 一致性 (p: 8)
  • C11 标准 (ISO/IEC 9899:2011):
  • 3.4 行为 (p: 3-4)
  • 4/2 未定义行为 (p: 8)
  • C99标准(ISO/IEC 9899:1999):
  • 3.4 行为(第3-4页)
  • 4/2 未定义行为(第7页)
  • C89/C90 标准 (ISO/IEC 9899:1990):
  • 1.6 术语定义

参见

C++ 文档 关于 未定义行为

外部链接

1. 每个C程序员都应该了解的未定义行为 #1/3
2. 每个C程序员都应该了解的未定义行为 #2/3
3. 每个C程序员都应该了解的未定义行为 #3/3
4. 未定义行为可能导致时间旅行(以及其他后果,但时间旅行最奇妙)
5. 理解C/C++中的整数溢出
6. 未定义行为与费马大定理
7. 空指针趣味之旅,第一部分 (Linux 2.6.30中因空指针解引用导致的未定义行为引发的本地漏洞)