1. 程式人生 > 其它 >C語言整數的取值範圍以及數值溢位

C語言整數的取值範圍以及數值溢位

計算無符號數(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,最直接的計算方法就是:

20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255

這種“按部就班”的計算方法雖然有效,但是比較麻煩,如果是 8 個位元組的 long 型別,那足夠你計算半個小時的了。

我們不妨換一種思路,先給 1111 1111 加上 1,然後再減去 1,這樣一增一減正好抵消掉,不會影響最終的值。

給 1111 1111 加上 1 的計算過程為:

0B1111 1111 + 0B1 = 0B1 0000 0000 = 28
 = 256

可以發現,1111 1111 加上 1  後需要向前進位(向第 9 位進位),剩下的 8 位都變成了 0,這樣一來,只有第 9 位會影響到數值的計算,剩下的 8 位對數值都沒有影響。第 9 位的權值計算起來非常容易,就是:

29-1 = 28 = 256

然後再減去 1:

28 - 1 = 256 - 1 = 255

加上 1 是為了便於計算,減去 1 是為了還原本來的值;當記憶體中所有的位都是 1 時,這種“湊整”的技巧非常實用。按照這種巧妙的方法,我們可以很容易地計算出所有無符號數的取值範圍(括號內為假設的長度):

有符號數的取值範圍

有符號數以補碼的形式儲存,計算取值範圍也要從補碼入手。我們以 char 型別為例,從下表中找出它的取值範圍:

我們按照從大到小的順序將補碼羅列出來,很容易發現最大值和最小值。

淡黃色背景的那一行是我要重點說明的。如果按照傳統的由補碼計算原碼的方法,那麼 1000 0000 是無法計算的,因為計算反碼時要減去 1,1000 0000 需要向高位借位,而高位是符號位,不能借出去,所以這就很矛盾。

是不是該把 1000 0000 作為無效的補碼直接丟棄呢?然而,作為無效值就不如作為特殊值,這樣還能多儲存一個數字。計算機規定,1000 0000 這個特殊的補碼就表示 -128。

為什麼偏偏是 -128 而不是其它的數字呢?

首先,-128 使得 char 型別的取值範圍保持連貫,中間沒有“空隙”。

其次,我們再按照“傳統”的方法計算一下 -128 的補碼:

  • -128 的數值位的原碼是 1000 0000,共八位,而 char 的數值位只有七位,所以最高位的 1 會覆蓋符號位,數值位剩下 000 0000。最終,-128 的原碼為 1000 0000。
  • 接著很容易計算出反碼,為 1111 1111。
  • 反碼轉換為補碼時,數值位要加上 1,變為 1000 0000,而 char 的數值位只有七位,所以最高位的 1 會再次覆蓋符號位,數值位剩下 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 00001000 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=0, b=-1

變數 a 為 unsigned int 型別,長度為 4 個位元組,能表示的最大值為 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,佔用33位,已超出 a 所能表示的最大值,所以發生了溢位,導致最高位的 1 被截去,剩下的 32 位都是0。也就是說,a 被儲存到記憶體後就變成了 0,printf 從記憶體中讀取到的也是 0。

變數 b 是 int 型別的有符號數,在記憶體中以補碼的形式儲存。0xffffffff 的數值位的原碼為 1111 1111 …… 1111 1111,共 32 位,而 int 型別的數值位只有 31 位,所以最高位的 1 會覆蓋符號位,數值位只留下 31 個 1,所以 b 的原碼為:

1111 1111 …… 1111 1111

這也是 b 在記憶體中的儲存形式。

當 printf 讀取到 b 時,由於最高位是 1,所以會被判定為負數,要從補碼轉換為原碼:

[1111 1111 …… 1111 1111]
= [1111 1111 …… 1111 1110]
= [1000 0000 …… 0000 0001]
= -1

最終 b 的輸出結果為 -1。