未定義行為
來自 cppreference.com
如果違反了語言的某些規則,則使整個程式失去意義。
目錄 |
[編輯] 解釋
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++ 程式設計師未定義行為指南 |