整數溢位 Integer Overflow
從電腦的數字系統開始,討論電腦如何儲存整數,並探討為何會產生整數溢位

這是我查到一些資料後的理解,希望能多一點對整數溢位後為何會正負循環的了解。

數字系統

首先要了解的是數字系統,因為一般電腦是採用二進制 (Binary) 來作數字計算,而不是我們習慣的十進制 (Decimal)。

我們一般人都是用十進制,十進制就是用 0、1、2、3、4、5、6、7、8、9 十個數字來表達,一個位數就有十種狀態,從 0 開始往上累積,超過 9 就進一位。

而二進制是什麼呢?二進制就是用 0、1 兩個數字來表達,一個位數就只有兩種狀態表達,從 0 開始往上累積,超過 1 就進一位。

以十進制來說,13 這個數字其實應該寫成 1*10^1 + 3*10^0 這樣。10^01,也就是代表個位數,乘以幾就是這個位數累積到幾,超過 9 就進一位到 10^1 這一位,也就是十位數;再累積超過 9,就進位到 10^2 百位數,以此類推。

那二進制怎麼表達呢?例如有一個二進制的數字長這樣 1101,如同十進制,我們可以表達成 1*2^3 + 1*2^2 + 0*2^1 + 1*2^0。這樣公式算完的答案其實就是十進制的 13,大家可以算一下試試看。其實其他如八進制、十六進制也是相同原理,只不過超過十進制的進制系統,因為阿拉伯數字無法在單一位數表達 10 以上的數字大小,所以會配合英文字母 A、B、C、D 等等。

另外複習一下國小(國中?)數學,小數點的部分,以我們常用的十進制來說,小數點後第一位是 10^-1、第二位是 10^-2,以此類推;相對的二進制也是一樣規則,小數點後第一位是 2^-1、第二位是 2^-2 這樣。

電腦基本運算方式

前面說到一般電腦是用二進制運算,這是什麼意思?

舉個例子,今天假設一台電腦,我們拉了第一條電線在裡面,一條電線怎麼表達不同的狀態呢?對,就是通電,有通電是一種狀態,沒通電是一種狀態。當然比較實際的作法是透過電壓,高電壓是一種狀態,低電壓是一種狀態,這樣來表達兩種狀態。

因此一條電線就能表達兩種狀態,剛好符合二進制只用 0、1 兩個數字來表達所有數字的狀態,而這樣能表達兩種狀態(1 或 0)的一條電線就是 1 bit。順便講一下因為某些原因,8 bits 被規定為 1 byte。bit 就是我們常聽到的位元,byte 就是我們更常聽到的位元組。

那 1 bit 可以表達兩種狀態,以二進制來說就是可以表達 0 跟 1;那 2 bits 呢?是四種;那 3 bits 呢?是六種?錯,是八種。

我們用列舉的方式說明,3 bits 意思是有三條電線各自有 0 跟 1 兩種狀態,因此我們可以列出有下列八種狀況:

狀況 第一條電線 第二條電線 第三條電線
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1

簡略來說就是其實是有 2*2*2=8,也就是 23 種狀況;以此類推,n bit(n 條電線)就有 2n 種狀況。

那 1 byte 可以表達幾種狀況?

前面說過 1 byte = 8 bit,所以 1 byte 就可以表達 28 = 256 種狀況。

前面也講過二進制怎麼換算十進制,也就代表了二進制的每一種狀況都對應到一個十進制的數字,至於對應到的是哪一個,就是用前面教過的換算方法換算。

因此,1 byte 可以表達 256 種狀況,每一種狀況剛好表達了一個二進制數字;再以二進制對應十進制來考慮,也就是 1 byte 可以表達從 0 開始,最多到 255 的各種十進制的正整數數字。

資料型態

到這邊可能就會有人問,那負數呢?

沒錯,講到這邊我們都只處理了正整數(包括 0),還沒講到負整數。講負整數之前,順便先講一下資料型態。

一般電腦要存這些數字,也是以這樣 1 個 byte、1 個 byte 的存,就想像記憶體或硬碟裡面就是一大堆小格子,每個小格子裡面通常就是 8 條線,也就是 8 bits,因此一個小格子就是 1 byte,也就是一個小格子就能存 256 種不同的狀態,以正整數來說,就是能存 0-255 這個範圍內的任何一個數字。

但 0-255 這樣的範圍很顯然太小了啊,儲存喝一杯可樂、吃一頓麥當勞的價格可能還可以,但要是我去麥當勞想點個六塊炸雞餐台幣 288 元怎麼辦?

對,就只好用兩個小格子 2 bytes 來存啊,兩個 256 加一起總可以了吧?

這句話只對了一半。

的確是要用 2 bytes 來存,但請記住 2 bytes 是有 28*28 = 216 = 65,536 種狀況,而不是 256 + 256 = 512 種狀況唷!就跟 2 bits = 2*2 種狀況一樣,2 bytes 也是 28*28 種狀況唷!至於有指數的數字相乘則指數相加這件事,請翻數學課本或上網谷哥。

接著,我們已經知道 2 bytes 有 65,536 種狀態,以正整數來說就是 2 bytes 就可以存 0-65535 這個範圍內任一整數,但某些狀況下,這個數字可能也不夠大怎麼辦?

就只好多用幾個 byte 啊,至於要用幾個 byte 才夠,大家的意見就不太一樣了。

舉例來說,在 Java 裡面,對於整數就有分成 byteshortintlong 四種資料型態,名稱不重要,重要的是 Java 規定 byte 型態整數佔 1 byte、short 佔 2 bytes、int 佔 4 bytes、long 佔 8 bytes。

整數儲存方式

byte 這個整數型態來舉例,儲存 1 這個正整數就是先轉成二進制,也就是 0000 0001 來存進 byte 的 8 bytes 空間,大概可以表達成這樣:

| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |

八個位元,剛好一個位元存一個二進位數字。

接著就可以來考慮負整數該怎麼辦了。

一般來說,不論是 byteshortint 還是 long,都是用最左邊的那一個位元來儲存正負號,0 是正號,1 是負號。所以如果是負整數 -1 就要表達成這樣:

| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |

1’s 補數

但是,這樣表達會有另外一個問題。

用最左邊的位元來儲存正負號,所以實際上八位元的空間只有七位元的空間能用來儲存數字,也就是只表示 215 種狀況,加上我們現在已經考慮到負整數,所以理論上範圍就會變成 -215~215 這個範圍。

這邊就產生一個問題,照前面的邏輯,0 換成八位元的二進制應該表達為 0000 0000,但是在第一個位元用來表示正負號的情況下,那豈不是會出現 0000 0000 代表 +0 以及 1000 0000 代表 -0 這種好像會有兩種 0 的奇怪的狀況??

有人想到解決的辦法,就是取補數。

補數是什麼呢?

我覺得比較簡單的講法就是兩個加起來會剛好開始進位的數字,互相之間就是補數,譬如在十進制中 19 互為補數、2377 互為補數這樣。

但是電腦是二進制啊,二進制補數怎麼算?二進制補數最簡單了,就是把 01 通通反過來就好,譬如 1001 翻成 0110 就變成補數啦,不信可以自己加加看是不是剛好會進位。

然後我們就可以利用補數來表達負整數的部分,所以像是 0110 換算成十進制是 6,而 -6 就利用補數法表達為 1001,剛好連最左邊正負號的欄位都同時翻轉成正確的正負號,是不是很方便呢?

所以取 1’s 補數後,以四位元來表達的整數組合就會變成如下:

一般十進制正整數 對應二進制 換算十進制整數 1’s補數法
0 0000 +0 +0
1 0001 +1 +1
2 0010 +2 +2
3 0011 +3 +3
4 0100 +4 +4
5 0101 +5 +5
6 0110 +6 +6
7 0111 +7 +7
8 1000 -0 -7
9 1001 -1 -6
10 1010 -2 -5
11 1011 -3 -4
12 1100 -4 -3
13 1101 -5 -2
14 1110 -6 -1
15 1111 -7 -0

這個方法就被稱作 1’s 補數法,但要注意,是對二進制取補數來表達十進制的負整數,而不是對十進制取補數。

2’s 補數

但是仔細想想,這樣還是沒有解決會同時有 +0-0,的問題啊,只是原本 -0 表達成 1000,現在 -0 變成用 1111 來表達而已啊?

所以為了徹底解決這個問題,於是就有了所謂的 2’s 補數法來彌補這個問題。

2’s 補數法就是針對 1’s 補數法會從 -0 開始排列而做的調整,原理就是利用 1’s 補數法將正整數的二進制取補數變成負整數的二進制後,對非代表正負號的二進制部分加 1 來代表原本 1’s 補數法代表的十進制數字。

一樣用十進制的 6 來當例子,十進制的 6 換成二進制變成 0110,而 -6 就是取 1’s 補數變成 1001,然後對 1001 非代表正負號的二進制部分加 1,也就是 001 + 1 = 010,再將原本代表正負號的第一位 1 加回去,所以 1010 才代表原本 1’s 補數所代表的 -6

在這樣的方式下,-0 會自然地消失,只剩下 +0 代表 0,也因此負整數會多一個,在 4 bits 的情況下,2’s 補數法的負整數就會多出 -8

如果我們看四個位元的整數組合,可以表列如下,讓大家直觀明白 2’s 補數法的對應關係。

一般十進制正整數 對應二進制 1’s補數法 2’s補數法
0 0000 +0 +0
1 0001 +1 +1
2 0010 +2 +2
3 0011 +3 +3
4 0100 +4 +4
5 0101 +5 +5
6 0110 +6 +6
7 0111 +7 +7
8 1000 -7 -8
9 1001 -6 -7
10 1010 -5 -6
11 1011 -4 -5
12 1100 -3 -4
13 1101 -2 -3
14 1110 -1 -2
15 1111 -0 -1

所以 2’s 補數法其實只是針對 1’s 補數法的負整數部分調整其對應的十進制數字。

整數溢位 Integer Overflow

終於要講到溢位了,yeah~

所謂整數溢位就是超過整數資料型態能儲存的範圍,例如剛才提的例子是用 4 bits 來儲存整數,所以有 24 也就是 16 種狀況,對應到利用 2’s 補數法調整過的十進制整數範圍就是 -2323-1,也就是 -8+7。

那如果我利用這樣 4 bits 的整數變數儲存正整數 7,但是變數在參與運算的時候不小心又加了一怎麼辦?這就會超出這個整數變數能儲存的範圍,那實際這個變數裡面的整數會怎麼樣?會變成 -8

為什麼?我們來算算看。

+7 對應的二進制是 0111,不小心再加了一會變多少?用二進制運算就是 1000。然後讓我們看一下上面的表格,1000 對應的 2’s 補數法那格是多少?就是 -8

反過來說,-8 對應的二進制是 1000,不小心又多減了一,1000 減一就變成 0111,那 0111 對應的十進制數字是多少?就是 +7

其實整數溢位會正反循環的道理就在這邊,不知道這樣講我十年後看不看得懂……orz


Last modified on 2017-10-01