Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

目录

说明

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

  • ill-formed - 程序存在语法错误或可诊断的语义错误。
  • 符合规范的C++编译器必须发出诊断信息,即使它定义了某种语言扩展来为此类代码赋予含义(例如可变长度数组)。
  • 标准文本使用 不应 非良构 来指明这些要求。
  • 格式错误, 无需诊断 - 程序存在语义错误,在一般情况下可能无法诊断(例如违反 ODR 规则或其他仅在链接时才能检测到的错误)。
  • 若执行此类程序,则行为未定义。
  • 实现定义行为 - 程序行为在不同实现间存在差异,符合规范的实现必须记录每种行为的具体影响。
  • 未指定行为 - 程序行为在不同实现间存在差异,且符合规范的实现无需对每种行为的效果进行文档说明。
  • 例如, 求值顺序 、相同的 字符串字面量 是否具有区分度、数组分配的开销大小等。
  • 每个未定义行为会导致一组有效结果中的某一个结果。
  • 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
}
(since C++26)
  • 未定义行为 - 对程序的行为没有任何限制。
  • 未定义行为的一些示例包括数据竞争、数组边界外的内存访问、有符号整数溢出、空指针解引用、 同一表达式内 对同一标量的多次修改 (无任何中间序列点) (C++11 前) (且操作未排序) (C++11 起) 、通过 不同类型指针 访问对象等。
  • 实现不要求诊断未定义行为(尽管许多简单情况会被诊断),且编译后的程序不要求执行任何有意义操作。
(since C++11)

UB 与优化

由于正确的C++程序应当避免未定义行为,当实际存在UB的程序在启用优化的情况下编译时,编译器可能产生预期之外的结果:

例如,

有符号整数溢出

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

可能被编译为 ( 演示 )

foo(int):
        mov     eax, 1
        ret

越界访问

int table[4] = {};
bool exists_in_table(int v)
{
    // 在前4次迭代中返回true,否则因越界访问导致未定义行为
    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非零或导致未定义行为
        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;
    // 现在读取 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 = nullptr;
    return *p; // 必然的未定义行为
}

可能被编译为 ( 演示 )

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 将仅以警告方式编译以下示例 with only a warning ,尽管该示例在 C++ 标准中 作为“错误”示例出现(另请参阅 GCC Bugzilla #55783

#include <iostream>
// 示例调整,请勿使用常量
double a{1.0};
// C++23 标准 §9.4.5 列表初始化 [dcl.init.list],示例 #6:
struct S
{
    // 无初始化列表构造函数
    S(int, double, double); // #1
    S();                    // #2
    // ...
};
S s1 = {1, 2, 3.0}; // 正确:调用 #1
S s2{a, 2, 3}; // 错误:存在窄化转换
S s3{}; // 正确:调用 #2
// — 示例结束]
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):
  • TBD 病式程序 [defns.ill.formed]
  • TBD 实现定义行为 [defns.impl.defined]
  • TBD 未指定行为 [defns.unspecified]
  • TBD 良式程序 [defns.well.formed]
  • C++17 标准 (ISO/IEC 14882:2017):
  • TBD 病式程序 [defns.ill.formed]
  • TBD 实现定义行为 [defns.impl.defined]
  • TBD 未指定行为 [defns.unspecified]
  • TBD 良式程序 [defns.well.formed]
  • C++14 标准 (ISO/IEC 14882:2014):
  • TBD 病式程序 [defns.ill.formed]
  • TBD 实现定义行为 [defns.impl.defined]
  • TBD 未指定行为 [defns.unspecified]
  • TBD 良式程序 [defns.well.formed]
  • C++11 标准 (ISO/IEC 14882:2011):
  • TBD 病式程序 [defns.ill.formed]
  • TBD 实现定义行为 [defns.impl.defined]
  • TBD 未指定行为 [defns.unspecified]
  • TBD 良式程序 [defns.well.formed]
  • C++98 标准 (ISO/IEC 14882:1998):
  • TBD 病式程序 [defns.ill.formed]
  • TBD 实现定义行为 [defns.impl.defined]
  • TBD 未指定行为 [defns.unspecified]
  • TBD 良式程序 [defns.well.formed]

参见

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

外部链接

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