未定义行为

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

若违反语言的某些规则,则令整个程序失去意义。

解释

C++ 标准为不被归入下列分类之一的每个程序都精确定义了其可观察行为

  • 非良构 - 程序拥有语法错误或可诊断的语义错误。
  • 遵从标准的 C++ 编译器必须为此给出诊断,即使它定义了为这种代码赋予了含义的语言扩展(例如用可变长度数组)也应如此。
  • 标准文本用 shall(应当)shall not(不应当)ill-formed(非良构)给出了这些要求。
  • 非良构而不要求诊断 - 程序拥有通常情况下可能无法诊断的语义错误(例如 ODR 的违规或者其他只能在链接时检测的错误)。
  • 执行这种程序的行为未定义。
  • 由实现定义的行为 - 程序的行为随实现而变动,遵从标准的实现必须为每个这样的行为的效果提供文档。
  • 未指明的行为 - 程序的行为随实现而变动,且遵从标准的实现不需要为每个行为的效果提供文档。
  • 例如求值顺序,等同的字符串字面量是否为不同的对象,数组分配的开销,等等。
  • 每个未指明行为均产生合法结果集合中的一个结果。
  • 错误行为 - 建议实现进行诊断的具有良好定义(但不正确)的行为。
  • 错误行为一定是由错误的程序代码导致的。
  • 对常量表达式的求值不会导致错误行为。
  • 如果执行包含了具有错误行为的操作,那么允许且建议实现发布诊断信息,也允许实现在该操作后的任意时间中止执行。
  • 在由实现确定的一套对程序行为的假设下,实现也可以在有触发错误行为的可能性的情况下直接发布诊断信息,即使实际上不会触发。
错误行为的示例
#include <cassert>
#include <cstring>
 
void f()
{   
    int d1, d2;       // d1, d2 有错误值
    int e1 = d1;      // 错误行为
    int e2 = d1;      // 错误行为
    assert(e1 == e2); // 成立
    assert(e1 == d1); // 成立,错误行为
    assert(e2 == d1); // 成立,错误行为
 
    std::memcpy(&d2, &d1, sizeof(int)); // 没有错误行为,但 d2 有错误值
 
    assert(e1 == d2); // 成立,错误行为
    assert(e2 == d2); // 成立,错误行为
}
 
unsigned char g(bool b)
{
    unsigned char c;     // c 有错误值
    unsigned char d = c; // 没有错误行为,但 d 有错误值
    assert(c == d);      // 成立,两次整数提升都具有错误行为
    int e = d;           // 错误行为
    return b ? d : 0;    // 当 b 为 true 时具有错误行为
}
(C++26 起)
  • 未定义行为 - 对程序的行为无任何限制。
  • 一些未定义行为的例子包括:数据竞争,数组边界外的内存访问,有符号整数溢出,空指针的解引用,在一个表达式中对同一标量对象修改多于一次而彼此间无序列点 (C++11 前)无序 (C++11 起),通过类型错误的指针访问对象,等等。
  • 实现不需要诊断未定义行为(尽管许多简单情形确实会得到诊断),而且所编译的程序不需要做任何有意义的事。

未定义行为与优化

由于正确的 C++ 程序不存在未定义的行为,因此在启用优化后编译实际存在未定义行为的程序时,编译器可能会产生意想不到的结果。

例如,

有符号溢出

int foo(int x)
{
    return x+1 > x; // 要么为 true,要么因为有符号溢出导致未定义行为
}

可能编译为(演示

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; // 未初始化的局部变量
    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

空指针解引用

以下示例演示读取解引用空指针的结果。

int foo(int* p)
{
    int x = *p;
    if (!p)
        return x; // 要么如上产生未定义行为,要么不可能采用此分支
    else
        return 0;
}
 
int bar()
{
    int* p = nullptr;
    return *p; // 无条件 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; // 未定义行为:访问已传递给 realloc 的指针
    *q = 2;
    if (p == q) // 未定义行为:访问已传递给 realloc 的指针
        std::cout << *p << *q << '\n';
}

可能的输出:

12

无副作用的非平凡 (C++26 起)无限循环

作为 C++ 向前进展保证的一部分,如果平凡的无限循环以外的 (C++26 起)没有可观察行为的循环不会终止,那么它的行为未定义。编译器可以移除这种循环。

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

#include <iostream>
 
bool fermat()
{
    const int max_value = 1000;
 
    // 无副作用的非平凡无限循环是未定义行为
    for (int a = 1, b = 1, c = 1; true; )
    {
        if (((a * a * a) == ((b * b * b) + (c * c * c))))
            return true; // 证伪 :()
        a++;
        if (a > max_value)
        {
            a = 1;
            b++;
        }
        if (b > max_value)
        {
            b = 1;
            c++;
        }
        if (c > max_value)
            c = 1;
    }
 
    return false; // 未能证伪
}
 
int main()
{
    std::cout << "费马大定理";
    fermat()
        ? std::cout << "被证伪!\n"
        : std::cout << "未被证伪。\n";
}

可能的输出:

费马大定理被证伪!

平凡的无限循环不是未定义行为。

(C++26 起)

带诊断消息的非良构

要注意的是,允许编译器以赋予非良构程序一定含义的方式来扩展语言。这种情况下 C++ 标准所规定的仅是一条诊断消息(编译器警告),除非程序是“非良构但无需诊断的”。

例如,除非通过 --pedantic-errors 禁用了语言扩展,GCC 都会编译以下示例代码而只给出一条警告,即使它在 C++ 标准中是作为“错误”的示例给出的(另请参见 GCC Bugzilla #55783

#include <iostream>
 
// 示例代码,请勿使用常量
double a{1.0};
 
// C++23 标准, §9.4.5 List-initialization [dcl.init.list], 示例 #6:
struct S
{
    // 无初始化式列表构造函数
    S(int, double, double); // #1
    S();                    // #2
    // ...
};
 
S s1 = {1, 2, 3.0}; // OK,调用 #1
S s2{a, 2, 3}; // 错误:窄化
S s3{}; // OK,调用 #2
// — end example]
 
S::S(int, double, double) {}
S::S() {}
 
int main()
{
    std::cout << "已通过所有检查。\n";
}

可能的输出:

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

引用

延伸内容
  • C++23 标准(ISO/IEC 14882:2024):
  • 3.25 ill-formed program [defns.ill.formed]
  • 3.26 implementation-defined behavior [defns.impl.defined]
  • 3.66 unspecified behavior [defns.unspecified]
  • 3.68 well-formed program [defns.well.formed]
  • C++20 标准(ISO/IEC 14882:2020):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++17 标准(ISO/IEC 14882:2017):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++14 标准(ISO/IEC 14882:2014):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++11 标准(ISO/IEC 14882:2011):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++98 标准(ISO/IEC 14882:1998):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]

参阅

[[assume(表达式)]](C++23) 指示 表达式 在给定的位置永远为 true
(属性指示符)
[[indeterminate]](C++26) 指示对象在未初始化时具有不确定值
(属性指示符)
标记执行的不可抵达点
(函数)

外部链接

1.  The LLVM Project Blog: 每个 C 程序员都应该知道的关于未定义行为的事 #1/3
2.  The LLVM Project Blog: 每个 C 程序员都应该知道的关于未定义行为的事 #2/3
3.  The LLVM Project Blog: 每个 C 程序员都应该知道的关于未定义行为的事 #3/3
4.  未定义行为能导致时间旅行(在所有事项中,时间旅行可是最惊人的)
5.  了解 C/C++ 中的整数溢出
6.  空指针的趣事,第一部分(Linux 2.6.30 中空指针解引用所致的未定义行为所引发的局部滥用)
7.  未定义行为与费马大定理