命名空间
变体
操作

未定义行为

来自 cppreference.cn
< c‎ | language

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

  • 未定义行为 - 对程序的行为没有限制。未定义行为的例子包括:内存访问超出数组边界、有符号整数溢出、空指针解引用、在没有序列点的情况下,表达式中对同一标量多次修改、通过不同类型的指针访问对象等。编译器不要求诊断未定义行为(尽管许多简单情况会诊断),并且编译后的程序不要求执行任何有意义的操作。
  • 未指定行为 - 允许两种或多种行为,并且实现不要求记录每种行为的效果。例如,求值顺序,相同的字符串字面量是否不同等。每种未指定行为都会产生一组有效结果中的一个,并且在同一程序中重复时可能会产生不同的结果。
  • 实现定义行为 - 未指定行为,但每个实现都记录了如何做出选择。例如,字节中的位数,或者有符号整数右移是算术移位还是逻辑移位。
  • 区域设置特定行为 - 依赖于当前选定的区域设置的实现定义行为。例如,对于除 26 个小写拉丁字母之外的任何字符,islower 是否返回 true。

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

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

目录

[编辑] UB(未定义行为)与优化

因为正确的 C 程序没有未定义行为,所以当一个实际存在 UB 的程序在启用优化的情况下编译时,编译器可能会产生意想不到的结果。

例如,

[编辑] 有符号溢出

int foo(int x)
{
    return x + 1 > x; // either true or UB due to signed overflow
}

可以编译为(演示

foo:
        mov     eax, 1
        ret

[编辑] 越界访问

int table[4] = {0};
int exists_in_table(int v)
{
    // return 1 in one of the first 4 iterations or UB due to out-of-bounds access
    for (int i = 0; i <= 4; i++)
        if (table[i] == v)
            return 1;
    return 0;
}

可以编译为(演示

exists_in_table:
        mov     eax, 1
        ret

[编辑] 未初始化标量

_Bool p; // uninitialized local variable
if (p) // UB access to uninitialized scalar
    puts("p is true");
if (!p) // UB access to uninitialized scalar
    puts("p is false");

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

p is true
p is false
size_t f(int x)
{
    size_t a;
    if (x) // either x nonzero or UB
        a = 42;
    return a;
}

可以编译为(演示

f:
        mov     eax, 42
        ret

[编辑] 无效标量

int f(void)
{
    _Bool b = 0;
    unsigned char* p = (unsigned char*)&b;
    *p = 10;
    // reading from b is now UB
    return b == 0;
}

可以编译为(演示

f:
        mov     eax, 11
        ret

[编辑] 空指针解引用

int foo(int* p)
{
    int x = *p;
    if (!p)
        return x; // Either UB above or this branch is never taken
    else
        return 0;
}
 
int bar()
{
    int* p = NULL;
    return *p;    // Unconditional UB
}

可以编译为(演示

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;
    // Endless loop with no side effects is UB
    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: 待定)
  • 4 一致性 (p: 待定)
  • 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 行为 (p: 3-4)
  • 4/2 未定义行为 (p: 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.  空指针的乐趣,第 1 部分(由于空指针解引用导致的 UB 在 Linux 2.6.30 中引发的本地漏洞)
English 日本語 中文(简体) 中文(繁體)