未定义行为
来自 cppreference.cn
如果违反了语言的某些规则,则使整个程序失去意义。
目录 |
[编辑] 解释
C++ 标准精确定义了所有不属于以下任一类别的 C++ 程序的可观察行为:
- 格式错误 (ill-formed) - 程序存在语法错误或可诊断的语义错误。
- 即使符合标准的 C++ 编译器定义了语言扩展来赋予此类代码意义(例如可变长数组),它也必须发出诊断。
- 标准文本使用 shall、shall not 和 ill-formed 来指示这些要求。
- 格式错误,无需诊断 (ill-formed, no diagnostic required) - 程序存在语义错误,这些错误在一般情况下可能无法诊断(例如违反 ODR 或其他仅在链接时才能检测到的错误)。
- 如果执行此类程序,其行为是未定义的。
- 实现定义行为 (implementation-defined behavior) - 程序的行为在不同实现之间有所不同,符合标准的实现必须记录每种行为的效果。
- 例如,std::size_t 的类型或字节中的位数,或 std::bad_alloc::what 的文本。
- 实现定义行为的一个子集是区域设置特定行为 (locale-specific behavior),它取决于实现提供的区域设置。
- 未指定行为 (unspecified behavior) - 程序的行为在不同实现之间有所不同,符合标准的实现无需记录每种行为的效果。
|
(C++26 起) |
- 未定义行为 (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 上观察到的
运行此代码
可能的输出
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.
[编辑] 参考
扩展内容 |
---|
|
[编辑] 另请参阅
[[assume(expression)]] (C++23) |
指定 expression 在给定点将始终评估为 true (属性说明符) |
[[indeterminate]] (C++26) |
指定对象如果未初始化则具有不确定值 (属性说明符) |
(C++23) |
标记不可达的执行点 (函数) |
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++ 程序员未定义行为指南 |