現(xiàn)在我們需要一個程序從控制臺讀入一個 INT 型整數(shù)(輸入確保是INT)资溃,然后輸出其絕對值即舌,你可能閉著眼睛就會寫出下面的代碼:
#include <iostream>
int main()
{
int n;
std::cin >> n;
std::cout << abs(n) << std::endl;
}
等下啦粹,好好思考兩分鐘拼岳,然后寫幾個測試例子跑一下程序枝誊。那么你找出程序存在的問題了嗎?好了惜纸,歡迎走進未定義行為 (Undefined Behavior) 的世界叶撒。
什么是未定義行為
文章一開始的程序中用到了 abs 求絕對值函數(shù),當n為 INT_MIN 時耐版,函數(shù)返回什么呢祠够?C++ 標準中有這么一條:
If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.
在一個2進制系統(tǒng)中,當 n 是 INT_MIN 時粪牲,int abs(int n)
返回的值超出了 int 的范圍古瓤,所以這將導致一個未定義行為。很多時候,標準過于精煉落君,不便于我們快速查找穿香,因此我們可以在 cppreference 找到需要的信息,以 abs函數(shù)為例绎速,cppreference 明確指出可能導致未定義行為:
Computes the absolute value of an integer number. The behavior is undefined if the result cannot be represented by the return type.
那么到底什么是未定義行為呢皮获?簡單來說,就是某個操作邏輯上是不合法的纹冤,比如越界訪問數(shù)組等洒宝,但是C++ 標準并沒有告訴我們遇到這種情況該如何去處理。
我們知道在大部分語言(比如 Python 和 Java)中,一個語句要么按照我們的預期正確執(zhí)行,要么立即拋出異常狭归。但是在 C++ 中,還有一種情況就是将宪,某條語句并沒有按照預期執(zhí)行(邏輯上已經(jīng)出錯了)绘闷,但是程序還是可以繼續(xù)執(zhí)行(C++標準沒有告訴怎么繼續(xù)執(zhí)行)橡庞。只不過程序的行為已經(jīng)不可預測了,也就是說程序可能發(fā)生運行時錯誤印蔗,也可能給出錯誤的結(jié)果扒最,甚至還可能給出正確的結(jié)果。
有一點需要注意的是华嘹,對于有的未定義行為吧趣,現(xiàn)代編譯器有時候可以給出警告,或者是編譯失敗的提示信息耙厚。此外强挫,不同編譯器對于未定義行為的處理方式也不同。
常見的未定義行為
C++ 標準中有大量的未定義行為薛躬,如果在標準中查找 undefined behavior
俯渤,將會看到幾十條相關(guān)內(nèi)容。如此眾多的未定義行為型宝,無疑給我們帶來了許多麻煩八匠,下面我們將列出一些常見的未定義行為,寫程序時應該盡量避免趴酣。
指針相關(guān)的常見未定義行為有如下內(nèi)容:
- 解引用 nullptr 指針梨树;
- 解引用一個未初始化的指針;
- 解引用 new 操作失敗返回的指針岖寞;
- 指針訪問越界(解引用一個超出數(shù)組邊界的指針)抡四;
- 解引用一個指向已銷毀對象的指針;
解引用一個指向已銷毀對象的指針仗谆,有時候很容易就會犯這個錯誤指巡,例如在函數(shù)中返回局部指針地址跨释。 一些簡單的錯誤代碼如下:
#include <iostream>
int * get(int tmp){
return &tmp;
}
int main()
{
int *foo = get(10);
std::cout << *foo << std::endl; // Undefined Behavior;
int arr[] = {1,2,3,4};
std::cout << *(arr+4) << std::endl; // Undefined Behavior;
int *bar=0;
*bar = 2; // Undefined Behavior;
std::cout << *bar << std::endl;
return 0;
}
其他常見未定義行為如下:
- 有符號整數(shù)溢出(文章開頭的例子);
- 整數(shù)做左移操作時厌处,移動的位數(shù)為負數(shù)鳖谈;
- 整數(shù)做移位操作時,移動的位數(shù)超出整型占的位數(shù)阔涉。(int64_t i = 1; i <<= 72)缆娃;
- 嘗試修改字符串字面值或者常量的內(nèi)容;
- 對自動初始化且沒有賦初值的變量進行操作瑰排;(int i; i++; cout << i;)
- 在有返回值的函數(shù)結(jié)束時不返回內(nèi)容贯要;
更完整的未定義行為列表可以在這里找到。
為什么存在未定義行為
C++ 程序經(jīng)常因為未定義行為而出現(xiàn)各種千奇百怪的 Bug椭住,調(diào)試起來也十分困難崇渗。相反,其他很多語言中并沒有未定義行為京郑,比如 python宅广,當訪問 list 越界時會拋出 list index out of range
,這些語言中不會因為未定義行為出現(xiàn)各種奇怪的錯誤些举。那么為什么 C++ 標準為什么要搞這么多未定義行為呢跟狱?
原因是這樣可以簡化編譯器的工作,有時候還可以產(chǎn)生更加高效的代碼户魏。舉個例子來說驶臊,如果我們想讓解引用指針的操作行為變的明確起來(成功或者拋出異常),就需要在編譯期知道指針使用是否合法叼丑,那么編譯器至少需要做下面這些工作:
- 檢查指針是否為 nullptr关翎;
- 通過某種機制檢查指針保存的地址是否合法;
- 通過某種機制拋出錯誤
這樣的話編譯器的實現(xiàn)會復雜很多鸠信。此外纵寝,如果我們有一個循環(huán)需要對大量的指針進行操作,那么編譯生成的代碼就會因為做各種附加檢查而導致效率低下症副。
實際上店雅,很多未定義行為,都是因為程序違反了某一先決條件而導致的贞铣,比如賦給指針的地址值必須是可訪問的闹啦,數(shù)組訪問時下標在正確的范圍內(nèi)。對 C++來說辕坝,語言設(shè)計者認為這是程序員(大家都是成年人了)需要保證的內(nèi)容窍奋,語言層面并不會去做相應的檢查。
不過,好消息是現(xiàn)在很多編譯器已經(jīng)可以診斷出一些可能導致未定義行為的操作琳袄,可以幫我們寫出更加健壯的程序江场。
其他一些行為
C++ 標準還規(guī)定了一些 Unspecified Behavior,一個簡單的例子(一個大公司曾經(jīng)的筆試題目)如下:
#include <iostream>
using namespace std;
int get(int i){
cout << i << endl;
return i+1;
}
int Cal(int a, int b) {
return a+b;
}
int main() {
cout << Cal(get(0), get(10)) << endl;
return 0;
}
程序輸出多少窖逗?答案是視編譯器而定址否,可能是0 10 12,也可能是 10 0 12碎紊。這是因為函數(shù)參數(shù)的執(zhí)行順序是 Unspecified Behavior佑附,引用C++標準對 Unspecified Behavior 的說明:
Unspecified behavior use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance.
此外,C++標準中還有所謂的 implementation-defined behavior
仗考,比如C++標準說需要一個數(shù)據(jù)類型音同,然后具體的編譯器去選擇該類型占用的字節(jié)數(shù),或者是存儲方式(大端還是小端)秃嗜。
一般情況下权均,我們需要關(guān)心的只有未定義行為,因為它通常會導致程序出錯锅锨。而其他的兩種行為叽赊,不需要我們?nèi)リP(guān)心。
更多閱讀
Cppreference:Undefined behavior
What are all the common undefined behaviors that a C++ programmer should know about?
What are the common undefined/unspecified behavior for C that you run into?
function parameter evaluation order
A Guide to Undefined Behavior in C and C++, Part 1
A Guide to Undefined Behavior in C and C++, Part 2
Why is there so much undefined behavior in C++?
Cplusplus: abs
What Every C Programmer Should Know About Undefined Behavior
Undefined behavior and sequence points
Undefined, unspecified and implementation-defined behavior
Where do I find the current C or C++ standard documents?