C/C++本身存在着大量的未定義行爲,可能很多人都被這樣的包含未定義行爲的書或者考試坑過。。。比如:

printf("%d %d %d %d
", i++, ++i, ++i++, power(2, i));

個人認爲,給出一段代碼而沒有一個確定的結果,是一個很可怕的事。

很多現代計算機語言,如Java等,都對很多未定義行爲進行了規定。那麼,在C/C++語言誕生的那個年代,爲什麼標準的編寫者,不稍稍下一點氣力,把標準裏的那些undefined去掉呢?換句話說,是什麼樣的原因,比如歷史原因,使得C/C++標準放任那麼多的未定義行爲而不去管呢?


主要原因:性能考慮,比如說一個簡單例子,數組下標越界是ub,如果規定不是ub,而是一個固定的表現,比如拋異常,那麼就像其他很多語言一樣,每次操作時候檢查,而且要知道,這個越界是針對數組而言的,比如你定義一個a[10],然後p=a[5],那麼p[6]一樣是越界,要想catch住所有類似情況,就比較麻煩了,你是選擇窮遍所有platform來選擇具體方式(比如利用訪問非法內存產生中斷信號,但這其實也不100%保證,萬一越界後的內存還是合法的呢),還是選擇將p實現爲一個僞指針對象?實際上正常程序是不會越界的,越界時候明確異常只是方便調試,所以如果你寫一個C編譯器,可以將debug和release分開實現。很多ub的確可以從語言設計上確定其行爲,只不過這樣一來是以性能天花板的降低爲代價的,這跟其他語言比就沒啥優勢了,C++只是選擇將責任扔給程序員,這類ub還有strict-aliasing之類的

其他一些原因:歷史原因,平臺差異等,其他人也說的差不多了


有很多原因,例如說你問的這個主要是從性能優化的角度考慮的。例如說,你這例子,如果必須要有確定行爲結果,那麼就可能會產生額外的內存讀寫。而如果ub,那就允許編譯器有更大的靈活性,例如說把i先加載到寄存器中進行計算,語句結束再統一回寫。尤其是在古老的年代,計算機硬件水平還不發達,各種寄存器數量少而且五花八門的年代,硬性規定某種做法,會損害在某些平臺上編譯出來的代碼的執行效率。


要不考慮下,如果嚴格定義所有指針值的讀寫行爲會發生什麼:

一般平臺上的 C/C++ 形式上可以訪問任意地址(把任意值的 uintptr_t 轉型成指針後讀寫)。如果規定對非法指針值的讀寫不是 UB ,實現就必須準備好合法地址的記錄(這可能導致內存佔用多出數倍),並且在解引用指針時查詢記錄(只要指針來源分析斷了,就導致不小的運行時開銷),分配/釋放時修改記錄。

而且 C/C++ 有 strict aliasing 規則。這導致合法地址記錄裏還要能查到地址中的某個字節屬於哪幾種類型的對象(或是無對象的未初始化內存),是否首字節等。

這還只是指針帶來 UB 的一角。可以繼續想象下在 C 標準基礎上直接消除 UB 會需要怎樣的實現。

(另外 C/C++ 還規定某些無可觀察副效應的無限循環是 UB ,其診斷等價於停機問題。不過可以直接改成不是 UB )

C++ 還有個 ill formed no diagnostic required ,有時其中的 "diagnostic" 只能通過逐個驗證所有可能存在的類型才能完成,如 constexpr 配合模板時的情況。


完全去掉ub,我只能說你想多了,舉個例子,你現在在寫串口驅動,這個串口在硬件上直接接在了crossbar的某個地址段上,你想訪問這個串口的控制寄存器,就得硬編碼地址,這種行爲是不可能良定義的。


因爲程序本身是無法窮盡所在機器的一切狀態的(停機問題),所以編譯器也無法窮盡生成的程序所在目標機器上的一切狀態。

C語言是工程師設計的,不是數學家。現實世界上的絕大多數問題是無法用算法解決的,工程師只想解決工程範圍內的功能,而不是那麼在乎全面的formal proof。


推薦閱讀:
相關文章