未定義行為

程序语言标准中没有规定的代码所产生的的结果

電腦程式設計中,未定義行為(英語:undefined behavior)是指執行某種電腦代碼所產生的結果,這種代碼在當前程式狀態下的行為在其所使用的語言標準英語Programming_language_specification中沒有規定。常見於翻譯器原始碼存在某些假設,而執行時這些假設不成立的情況。

一些程式語言中,某些情況下存在未定義行為,以CC++最為著名[1]。在這些語言的標準中,規定某些操作的語意是未定義的,典型的例子就是程式錯誤的情況,比如越界訪問陣列元素。標準允許語言的具體實現做這樣的假設:只要是符合標準的程式碼,就不會出現任何類似的行為。具體到 C/C++ 中,編譯器可以選擇性地給出相應的診斷資訊,但沒有對此的強制要求:針對未定義行為,語言實現作出任何反應都是正確的,類似於數字邏輯中的無關項英語Don't-care term。雖然編譯器實現可能會針對未定義行為給出診斷資訊,但保證編寫的代碼中不引發未定義行為是程式設計師自己的責任。這種假設的成立,通常可以讓編譯器對代碼作出更多最佳化,同時也便於做更多的編譯期檢查和靜態程式分析

有時候也可能存在對於未定義行為本身的限制性要求。例如,在CPU指令集說明中可能將某些形式的指令定為未定義,但如果該CPU支援主記憶體保護,說明中很可能會還會包含一條兜底的規則,要求任何用戶態的指令都不會讓作業系統的安全性受損;這樣一來,在執行未定義行為的指令時,就允許CPU破壞用戶暫存器,但不允許發生諸如切換到監控模式的操作。

未指定行為英語unspecified behavior(unspecified behavior)不同,未定義行為強調基於不可移植或錯誤的程式構造,或使用錯誤的數據。一個符合標準的實現可以在假定未定義行為永遠不發生(除了顯式使用不嚴格遵守標準的擴充)的基礎上進行最佳化,可能導致原本存在未定義行為(例如有符號數溢位)的程式經過最佳化後顯示出更加明顯的錯誤(例如無窮迴圈)。因此,這種未定義行為一般應被視為bug。

好處

如果某一操作在文件中被定為未定義行為,編譯器就可以假設該操作在符合標準的程式中永遠不會發生。這樣,編譯器就可以得到更多的資訊,獲得更多最佳化程式的機會。

例如這樣的C語言代碼:

int foo(unsigned char x)
{
     int value = 2147483600; /* 假设 int 是 32 位 */
     value += x;
     if (value < 2147483600)
        bar();
     return value;
}

因為 xunsigned char 不可能為負數,而C語言中有符號整數的溢位又是未定義行為,編譯器就可以假設執行 if 陳述式時 value 不可能小於 2147483600。因為這裏的 if 沒有副作用,條件也永遠不成立,所以編譯器就可以直接忽略 if 陳述式和對函數 bar 的呼叫。於是,上述代碼在語意上就等價於:

int foo(unsigned char x)
{
     int value = 2147483600;
     value += x;
     return value;
}

如果有符號整數的溢位有明確的「環繞」行為,那麼這樣的程式轉化就是非法的。

代碼越複雜,類似的最佳化就越難被人類發現。如果代碼同時還有其它方面的最佳化,例如行內展開,就更難發現了。

讓有符號整數溢位未定義還有另一個好處:儲存、操作變數的值時,可以在比變數本身更大的暫存器中進行。假設原始碼中變數的類型比原生暫存器的寬度要窄(比如常見的在64位元機器上的int類型),那麼編譯器就可以在生成機械碼時把這個變數當作64位元有符號數,對代碼的語意沒有任何影響。反之,如果32位元有符號整數的溢位有明確定義,那麼在針對64位元機器編譯時,編譯器就必須插入額外的邏輯確保行為符合預期,因為大多數機械碼指令在溢位時行為與暫存器的寬度有關。[2]

更重要的一點是,有符號整數溢位的行為未定義,允許在編譯期檢查、靜態程式分析、執行期檢查時捕捉這類錯誤的情況;如果溢位行為有明確定義,就無法進行編譯期檢查。

C和C++的未定義行為的一些例子

嘗試修改字串字面量英語string literal會產生未定義行為:[3]

char * p = "wikipedia"; // C++11中错误,C++98/C++03不推荐使用
p[0] = 'W'; // 未定义行为

防止這一點的方法之一是將它定義為陣列而不是指標

char p[] = "wikipedia"; /* 正确 */
p[0] = 'W';

在C++可以使用標準模板庫中的string類型,如下所示:

std::string s = "wikipedia"; /* 正确 */
s[0] = 'W';

除以零會導致未定義行為。根據 IEEE 754,float、double和long double類型的值除以零的結果是無窮大或NaN[4]

return x/0; // 未定义行为

某些指標操作可能導致未定義行為:[5]

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5;  // 未定义行为

到達返回數值的函數(除main函數以外)的結尾,而沒有一個return陳述式,會導致未定義行為:

int f()
{
}  /* 未定义行为 */

C程式語言》在第2.12節參照下面的代碼作為未定義行為的例子:

printf("%d %d\n", ++n, power(2, n));    /* 未定义行为 */

以及

a[i] = i++; /* 未定义行为 */

標準庫可能指定未定義行為,例如:

int x = 1;
printf("%d\n", &x);    /*未定义行为:%d预期int类型的实际参数*/
printf("%p\n", &x);    /*未定义行为:%p预期void*类型的实际参数*/
printf("%p\n", (void*)&x); /*%p和void*类型的实际参数匹配,不在此引发未定义行为*/

參考資料

  1. ^ Lattner, Chris. What Every C Programmer Should Know About Undefined Behavior. LLVM Project Blog. LLVM.org. May 13, 2011 [May 24, 2011]. (原始內容存檔於2014-10-30). 
  2. ^ 存档副本. [2018-06-21]. (原始內容存檔於2018-07-09). 
  3. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §2.13.4 String literals [lex.string] para. 2
  4. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.6 Multiplicative operators [expr.mul] para. 4
  5. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.7 Additive operators [expr.add] para. 5

外部連結