未定義行為

未定義行為

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

簡介

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

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

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

作用

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

例如這樣的C語言代碼:

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

如果有符號整數的溢出有明確的“環繞”行為,那么這樣的程式轉化就是非法的。

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

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

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

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

嘗試修改字元串字面量會產生未定義行為:

防止這一點的方法之一是將它定義為數組而不是指針:

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

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

某些指針操作可能導致未定義行為:

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

《C程式設計語言》在第2.12節引用下面的代碼作為未定義行為的例子:

以及

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

相關詞條

熱門詞條

聯絡我們