C語言整數的取值范圍以及數值溢出


在現代操作系統中,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最直接的計算方法就是:

2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 5 + 2 6 + 2 7 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255

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

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

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

0B1111 1111 + 0B1 = 0B 1 0000 0000 = 2 8 = 256

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

2 9-1 = 2 8 = 256

然后再減去 1:

2 8 - 1 = 256 - 1 = 255

加上 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 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 為(個人:無符號類型,沒有符號位,在內存中存儲的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 的原碼為

1111 1111 …… 1111 1111

這也是 b 在內存中的存儲形式

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM