命名空间
变体
操作

未定义行为

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

如果违反了语言的某些规则,则使整个程序失去意义。

目录

[编辑] 解释

C++ 标准精确定义了所有不属于以下任一类别的 C++ 程序的可观察行为

  • 格式错误 (ill-formed) - 程序存在语法错误或可诊断的语义错误。
  • 即使符合标准的 C++ 编译器定义了语言扩展来赋予此类代码意义(例如可变长数组),它也必须发出诊断。
  • 标准文本使用 shallshall notill-formed 来指示这些要求。
  • 格式错误,无需诊断 (ill-formed, no diagnostic required) - 程序存在语义错误,这些错误在一般情况下可能无法诊断(例如违反 ODR 或其他仅在链接时才能检测到的错误)。
  • 如果执行此类程序,其行为是未定义的。
  • 实现定义行为 (implementation-defined behavior) - 程序的行为在不同实现之间有所不同,符合标准的实现必须记录每种行为的效果。
  • 例如,std::size_t 的类型或字节中的位数,或 std::bad_alloc::what 的文本。
  • 实现定义行为的一个子集是区域设置特定行为 (locale-specific behavior),它取决于实现提供的区域设置
  • 未指定行为 (unspecified behavior) - 程序的行为在不同实现之间有所不同,符合标准的实现无需记录每种行为的效果。
  • 例如,求值顺序,相同的字符串字面量是否不同,数组分配开销的大小等。
  • 每种未指定行为都会产生一组有效结果中的一个。
  • 错误行为 (erroneous behavior) - 建议实现诊断的(不正确)行为。
  • 错误行为始终是程序代码不正确的后果。
  • 常量表达式的求值永远不会导致错误行为。
  • 如果执行包含指定为错误行为的操作,则允许并建议实现发出诊断,并且允许在操作发生后的未指定时间终止执行。
  • 如果实现可以根据关于程序行为的实现特定假设确定错误行为是可达的,则可以发出诊断,这可能导致误报。
错误行为的示例
#include <cassert>
#include <cstring>
 
void f()
{   
    int d1, d2;       // d1, d2 have erroneous values
    int e1 = d1;      // erroneous behavior
    int e2 = d1;      // erroneous behavior
    assert(e1 == e2); // holds
    assert(e1 == d1); // holds, erroneous behavior
    assert(e2 == d1); // holds, erroneous behavior
 
    std::memcpy(&d2, &d1, sizeof(int)); // no erroneous behavior, but
                                        // d2 has an erroneous value
 
    assert(e1 == d2); // holds, erroneous behavior
    assert(e2 == d2); // holds, erroneous behavior
}
 
unsigned char g(bool b)
{
    unsigned char c;     // c has erroneous value
    unsigned char d = c; // no erroneous behavior, but d has an erroneous value
    assert(c == d);      // holds, both integral promotions have erroneous behavior
    int e = d;           // erroneous behavior
    return b ? d : 0;    // erroneous behavior if b is true
}
(C++26 起)
  • 未定义行为 (undefined behavior) - 对程序行为没有任何限制。
  • 未定义行为的一些示例包括数据竞争、数组边界外的内存访问、有符号整数溢出、空指针解引用,表达式中对同一标量进行多次修改,没有任何中间序列点(C++11 之前)且未序列化(C++11 之后),通过不同类型的指针访问对象等。
  • 不要求实现诊断未定义行为(尽管许多简单情况会被诊断),并且编译后的程序不要求执行任何有意义的操作。
  • 运行时未定义行为 (runtime-undefined behavior) - 除在求值表达式作为核心常量表达式时发生之外,其余情况为未定义行为。
(C++11 起)

[编辑] UB 和优化

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

例如,

[编辑] 有符号整数溢出

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

可能被编译为 (演示)

foo(int):
        mov     eax, 1
        ret

[编辑] 越界访问

int table[4] = {};
bool exists_in_table(int v)
{
    // return true 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 true;
    return false;
}

可能被编译为 (演示)

exists_in_table(int):
        mov     eax, 1
        ret

[编辑] 未初始化的标量

std::size_t f(int x)
{
    std::size_t a;
    if (x) // either x nonzero or UB
        a = 42;
    return a;
}

可能被编译为 (演示)

f(int):
        mov     eax, 42
        ret

所示输出是在旧版 gcc 上观察到的

#include <cstdio>
 
int main()
{
    bool p; // uninitialized local variable
    if (p)  // UB access to uninitialized scalar
        std::puts("p is true");
    if (!p) // UB access to uninitialized scalar
        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;
    // 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 = nullptr;
    return *p; // Unconditional UB
}

可能被编译为 (演示)

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

[编辑] 访问传递给 std::realloc 的指针

选择 clang 以观察所示输出

#include <cstdlib>
#include <iostream>
 
int main()
{
    int* p = (int*)std::malloc(sizeof(int));
    int* q = (int*)std::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
        std::cout << *p << *q << '\n';
}

可能的输出

12

[编辑] 无副作用的无限循环

选择 clang 或最新版 gcc 以观察所示输出。

#include <iostream>
 
bool fermat()
{
    const int max_value = 1000;
 
    // Non-trivial infinite loop with no side effects is UB
    for (int a = 1, b = 1, c = 1; true; )
    {
        if (((a * a * a) == ((b * b * b) + (c * c * c))))
            return true; // disproved :()
        a++;
        if (a > max_value)
        {
            a = 1;
            b++;
        }
        if (b > max_value)
        {
            b = 1;
            c++;
        }
        if (c > max_value)
            c = 1;
    }
 
    return false; // not disproved
}
 
int main()
{
    std::cout << "Fermat's Last Theorem ";
    fermat()
        ? std::cout << "has been disproved!\n"
        : std::cout << "has not been disproved.\n";
}

可能的输出

Fermat's Last Theorem has been disproved!

[编辑] 需要诊断消息的格式错误

请注意,编译器允许以赋予格式错误程序意义的方式扩展语言。在这些情况下,C++ 标准唯一要求的是诊断消息(编译器警告),除非程序是“格式错误但不需要诊断”。

例如,除非通过 --pedantic-errors 禁用语言扩展,否则 GCC 将只带一个警告来编译以下示例,即使它出现在 C++ 标准中作为“错误”的示例(另请参阅GCC Bugzilla #55783

#include <iostream>
 
// Example tweak, do not use constant
double a{1.0};
 
// C++23 standard, §9.4.5 List-initialization [dcl.init.list], Example #6:
struct S
{
    // no initializer-list constructors
    S(int, double, double); // #1
    S();                    // #2
    // ...
};
 
S s1 = {1, 2, 3.0}; // OK, invoke #1
S s2{a, 2, 3}; // error: narrowing
S s3{}; // OK, invoke #2
// — end example]
 
S::S(int, double, double) {}
S::S() {}
 
int main()
{
    std::cout << "All checks have passed.\n";
}

可能的输出

main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
     ^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
     ^
     static_cast<int>( )
1 error generated.

[编辑] 参考

扩展内容
  • C++23 标准 (ISO/IEC 14882:2024)
  • 3.25 格式错误的程序 [defns.ill.formed]
  • 3.26 实现定义行为 [defns.impl.defined]
  • 3.66 未指定行为 [defns.unspecified]
  • 3.68 格式良好的程序 [defns.well.formed]
  • C++20 标准 (ISO/IEC 14882:2020)
  • 待定 格式错误的程序 [defns.ill.formed]
  • 待定 实现定义行为 [defns.impl.defined]
  • 待定 未指定行为 [defns.unspecified]
  • 待定 格式良好的程序 [defns.well.formed]
  • C++17 标准 (ISO/IEC 14882:2017)
  • 待定 格式错误的程序 [defns.ill.formed]
  • 待定 实现定义行为 [defns.impl.defined]
  • 待定 未指定行为 [defns.unspecified]
  • 待定 格式良好的程序 [defns.well.formed]
  • C++14 标准 (ISO/IEC 14882:2014)
  • 待定 格式错误的程序 [defns.ill.formed]
  • 待定 实现定义行为 [defns.impl.defined]
  • 待定 未指定行为 [defns.unspecified]
  • 待定 格式良好的程序 [defns.well.formed]
  • C++11 标准 (ISO/IEC 14882:2011)
  • 待定 格式错误的程序 [defns.ill.formed]
  • 待定 实现定义行为 [defns.impl.defined]
  • 待定 未指定行为 [defns.unspecified]
  • 待定 格式良好的程序 [defns.well.formed]
  • C++98 标准 (ISO/IEC 14882:1998)
  • 待定 格式错误的程序 [defns.ill.formed]
  • 待定 实现定义行为 [defns.impl.defined]
  • 待定 未指定行为 [defns.unspecified]
  • 待定 格式良好的程序 [defns.well.formed]

[编辑] 另请参阅

[[assume(expression)]]
(C++23)
指定 expression 在给定点将始终评估为 true
(属性说明符)[编辑]
(C++26)
指定对象如果未初始化则具有不确定值
(属性说明符)[编辑]
标记不可达的执行点
(函数) [编辑]
C 文档,关于 未定义行为

[编辑] 外部链接

1.  LLVM 项目博客:每个 C 程序员都应该知道的关于未定义行为 #1/3
2.  LLVM 项目博客:每个 C 程序员都应该知道的关于未定义行为 #2/3
3.  LLVM 项目博客:每个 C 程序员都应该知道的关于未定义行为 #3/3
4.  未定义行为可能导致时间旅行(以及其他事情,但时间旅行最离奇)
5.  理解 C/C++ 中的整数溢出
6.  空指针的乐趣,第 1 部分(Linux 2.6.30 中因空指针解引用导致的 UB 引起的本地漏洞利用)
7.  未定义行为和费马大定理
8.  C++ 程序员未定义行为指南