在現代操作系統中,short、int、long 的長度分別是 2、4、4 或者 8,它們只能存儲有限的數值,當數值過大或者過小時,超出的部分會被直接截掉,數值就不能正確存儲了,我們將這種現象稱為溢出(Overflow)。要想知道數值什么時候溢出,就得先知道各種整數類型的取值范圍。
無符號數的取值范圍
計算無符號數(unsigned 類型)的取值范圍(或者說最大值和最小值)很容易,將內存中的所有位(Bit)都置為 1 就是最大值,都置為 0 就是最小值。
以 unsigned char 類型為例,它的長度是 1,占用 8 位的內存,所有位都置為 1 時,它的值為 28 - 1 = 255,所有位都置為 0 時,它的值很顯然為 0。由此可得,unsigned char 類型的取值范圍是 0~255。
前面我們講到, char 是一個字符類型,是用來存放字符的,但是它同時也是一個整數類型,也可以用來存放整數,請大家暫時先記住這一點,更多細節我們將在《C語言中的字符(char)》一節中介紹。
有讀者可能會對 unsigned char 的最大值有疑問,究竟是怎么計算出來的呢?下面我就講解一下這個小技巧。
將 unsigned char 的所有位都置為 1,它在內存中的表示形式為1111 1111,最直接的計算方法就是:
這種“按部就班”的計算方法雖然有效,但是比較麻煩,如果是 8 個字節的 long 類型,那足夠你計算半個小時的了。
我們不妨換一種思路,先給 1111 1111 加上 1,然后再減去 1,這樣一增一減正好抵消掉,不會影響最終的值。
給 1111 1111 加上 1 的計算過程為:
可以發現,1111 1111 加上 1 后需要向前進位(向第 9 位進位),剩下的 8 位都變成了 0,這樣一來,只有第 9 位會影響到數值的計算,剩下的 8 位對數值都沒有影響。第 9 位的權值計算起來非常容易,就是:
然后再減去 1:
加上 1 是為了便於計算,減去 1 是為了還原本來的值;當內存中所有的位都是 1 時,這種“湊整”的技巧非常實用。按照這種巧妙的方法,我們可以很容易地計算出所有無符號數的取值范圍(括號內為假設的長度):
有符號數的取值范圍
有符號數以補碼的形式存儲(無符號也以補碼形式存儲,但是無符號的補碼和原碼相同,不需要轉換),計算取值范圍也要從補碼入手。我們以 char 類型為例,從下表中找出它的取值范圍:
我們按照從大到小的順序將補碼羅列出來,很容易發現最大值和最小值(個人:也就是最大值、最小值就是這樣子觀察出來的)。
淡黃色背景的那一行是我要重點說明的。如果按照傳統的由補碼計算原碼的方法,那么 1000 0000 是無法計算的,因為計算反碼時要減去 1,1000 0000 需要向高位借位,而高位是符號位,不能借出去,所以這就很矛盾。
是不是該把 1000 0000 作為無效的補碼直接丟棄呢?然而,作為無效值就不如作為特殊值,這樣還能多存儲一個數字。計算機規定,1000 0000 這個特殊的補碼就表示 -128。
為什么偏偏是 -128 而不是其它的數字呢?
- 首先,-128 使得 char 類型的取值范圍保持連貫,中間沒有“空隙”。
- 其次,我們再按照“傳統”的方法計算一下 -128 的補碼:
- -128($2^{8-1}=128$) 的數值位的原碼是 1000 0000,共八位,而 char 的數值位只有七位,所以最高位的 1 會覆蓋符號位(個人:其實就是截斷,符號位加上最小的符合條件的數值位,而我們只有8位,所以會從低到高截取8位,這樣原來的符號位被丟棄了,數值位的最高位充當新的符號位),數值位剩下 000 0000。最終,-128 的原碼(個人:也就是在原碼這個階段時,就發生了截斷)為 1000 0000。
- 接着很容易計算出反碼,為 1111 1111。
- 反碼轉換為補碼時,數值位要加上 1,變為 1000 0000,而 char 的數值位只有七位,所以最高位的 1 會再次覆蓋符號位(個人:其實還是發生了截斷,符號位1位加上這里產生的數值位8位,而我們總共只有8位,所以會從低到高截取8位,此時新產生的數值位的最高位充當符號位),數值位剩下 000 0000。最終求得的 -128 的補碼是 1000 0000。
-128 從原碼轉換到補碼的過程中,符號位被 1 覆蓋了兩次,而負數的符號位本來就是 1,被 1 覆蓋多少次也不會影響到數字的符號。
你看,雖然從 1000 0000 這個補碼推算不出 -128,但是從 -128 卻能推算出 1000 0000 這個補碼,這么多么的奇妙,-128 這個特殊值選得恰到好處。
負數在存儲之前要先轉換為補碼,“從 -128 推算出補碼 1000 0000”這一點非常重要,這意味着 -128 能夠正確地轉換為補碼,或者說能夠正確的存儲。
關於零值和最小值
仔細觀察上表可以發現,在 char 的取值范圍內只有一個零值,沒有+0和-0的區別,並且多存儲了一個特殊值,就是 -128,這也是采用補碼的另外兩個小小的優勢。
如果直接采用原碼存儲,那么
- 0000 0000和1000 0000將分別表示+0和-0,這樣在取值范圍內就存在兩個相同的值,多此一舉。
- 另外,雖然最大值沒有變,仍然是 127,但是最小值卻變了,只能存儲到 -127,不能存儲 -128 了,因為 -128 的原碼為 1000 0000,這個位置已經被-0占用了。
按照上面的方法,我們可以計算出所有有符號數的取值范圍(括號內為假設的長度):
上節我們還留下了一個疑問,[1000 0000 …… 0000 0000]補這個 int 類型的補碼為什么對應的數值是 -231,有了本節對 char 類型的分析,相信聰明的你會舉一反三,自己解開這個謎團。
數值溢出
char、short、int、long 的長度是有限的,當數值過大或者過小時,有限的幾個字節就不能表示了,就會發生溢出。發生溢出時,輸出結果往往會變得奇怪,請看下面的代碼:
#include <stdio.h> int main() { unsigned int a = 0x100000000; int b = 0xffffffff; printf("a=%u, b=%d\n", a, b); return 0; }
變量 a 為(個人:無符號類型,沒有符號位,在內存中存儲的01序列都是數值位) unsigned int 類型,長度為 4 個字節,能表示的最大值為 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用33位,已超出 a 所能表示的最大值,所以發生了溢出,導致最高位的 1 被截去,剩下的 32 位都是0。也就是說,a 被存儲到內存后就變成了 0,printf 從內存中讀取到的也是 0。
變量 b 是 int 類型的有符號數,在內存中以補碼的形式存儲(個人:由於b為一個正數,所以,它的補碼與原碼相同,即它的原碼形式就是它在內存中存儲時的補碼形式),0xffffffff 的數值位的原碼為 1111 1111 …… 1111 1111,共 32 位,而 int 類型的數值位只有 31 位,所以最高位的 1 會覆蓋符號位,數值位只留下 31 個 1,所以 b 的原碼為:
這也是 b 在內存中的存儲形式。
當 printf 讀取到 b 時,由於最高位是 1,所以會被判定為負數,要從補碼轉換為原碼:
= [1111 1111 …… 1111 1110] 反
= [1000 0000 …… 0000 0001] 原
= -1
最終 b 的輸出結果為 -1。