名稱空間
變體
操作

未定義行為

來自 cppreference.com
< 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++ 程式設計師未定義行為指南