如今計算機的抽象級別越來越高,越發少人關注在計算機底層發生了什麼事情,其實底層也有些很有意思的東西。這篇文章主要想科普一下整型在計算機硬體中的相關實現,它是以什麼方式來儲存的?如何區別正負數?硬體會怎麼去解釋相關的位串?
無符號數
在現代程式語言中,支援無符號數的語言已經比較少了。常見的就只有C/C++,它們可以通過unsigned
關鍵字定義無符號相關的資料型別。無符號整型與有符號整型最大的區別就是對相同的二進位制位串解讀方式不一樣。所有的無符號數都是非負數,因為它是沒有符號的。如果我們要用位長為w
的空間來儲存整型,以無符號的方式來解讀的話第k
位的權重都是2 ^ k
,其中最高有效位的權重為2 ^ (w - 1)
。
舉個具體點的例子,假設二進位制串11000001
所代表的是一個無符號數,由於它是8位長,所以最高有效位的權重是2 ^ (8 - 1) = 128
,第0位的權重是2 ^ 0 = 1
, 第一位的權重是2 ^ 1 = 2
,以此類推。最終代入公式可得
2 ^ 7 * 1 + 2 ^ 6 * 1 + 2 ^ 5 * 0 + 2 ^ 4 * 0 + 2 ^ 3 * 0 + 2 ^ 2 * 0 + 2 ^ 1 * 0 + 2 ^ 0 * 1 = 193
複製程式碼
因此,二進位制串11000001
所對應的無符號數為193
。還可以猜測出在8位的空間內最小的無符號數是0,它的二進位制表示為00000000
,最大的無符號數是255,它的二進位制表示為11111111
。
雖說,在日常的業務邏輯中用到無符號數的機會已經不多了,不過在計算機底層你還是可以經常看到它的身影。假設我們要用以下的16位的二進位制串來代表整型
11100000 10000000
複製程式碼
其中高8位為11100000
,低8位為10000000
。不管這個16位的位串最終會被解讀成有符號數還是無符號數,低8位始終都可以看作一個無符號數128
。同理,如果是一個代表整型的32位二進位制串
11100000 10000000 11100000 10000000
複製程式碼
那麼它的低24位就可以看作一個無符號數了。從這個角度來看,雖說我們平時不會直接操作無符號數,但實際上無符號數在計算機底層還是“大量存在”的。
有符號數-原碼,反碼,補碼
如果在整型的世界中只有無符號數的話,那麼似乎難以構建一個龐大的系統,畢竟有許多資料或者運算都要藉助負數。接下來我想簡單介紹一下在計算機底層要表示一個有符號的整型主要有哪些策略。
1. 原碼(Sign-Magnitude)
這種編碼方式比較好理解,前面所說的無符號數之所以無法表示負數,那是因為我們並沒有留下一個特殊的位置用於標識它到底是大於0的還是小於0的。而原碼的基本原理其實就是分配一個位用來標識該數到底是正數還是負數。
同樣用8位空間來舉例子,採用最高有效位來作為符號位,其他位用於權重計算,那麼不難看出它所能表示的最大的數就是01111111
,最小的數就是11111111
。它們的值分別為127
和-127
,不過如今的大多數機器都不是以這種方式來表示有符號整型的。而且這種方式還有個比較突出的特性,如果要表示數字0,將有兩種二進位制編碼方式,+0會表示為00000000
,-0會表示為10000000
。也許這種不唯一性也是它沒有在整型的領域被廣泛採用的原因吧。不過原碼這種編碼方式在浮點數裡面會用到,這個以後有機會再說。
2. 反碼(Ones' complement)
記得是小學數學裡面就已經出現了減法運算了,但是我是到了初中才接觸到負數的概念。一個負數其實就是某個特定正數的相反數。正數56
的取反結果就是-56
,那麼在二進位制裡面怎麼取反呢?一個可行的辦法就是按位取反,這也是反碼的最直觀的特徵。00111000
所代表的數值是56
,如果要用反碼來表示-56
直接把56
的二進位制串按位取反即可11000111
。
不過上面所說的只是反碼給人最直觀的感覺,實際上它還是需要一定的數學支援的。在反碼裡面最高有效位的權重為- (2 ^ (w - 1) - 1)
,如果是一個8位的位串,最高有效位的權重為-127
,其他位的計算方式跟無符號數一樣。那麼把前面所得的11000111
二進位制串代入公式可得
-(2 ^ 7 - 1) * 1 + 2 ^ 6 * 1 + 2 ^ 5 * 0 + 2 ^ 4 * 0 + 2 ^ 3 * 0 + 2 ^ 2 * 1 + 2 ^ 1 * 1 + 2 ^ 0 * 1 = -56
複製程式碼
結果剛好是-56
。這個時候,不難推斷出一個位元組所能表示的最大值為127
(01111111),最小值為-127
(10000000)。反碼所能夠表示的數值範圍跟原碼一樣,只是某些數值在底層的編碼方式會有所不同。此外,它跟原碼都有同樣的問題,就是對數字0有兩種不同的二進位制表示方式。在反碼中+0表示為00000000
,-0表示為11111111
。
3. 補碼(Two's complement)
理論上,原碼和反碼都能夠用來表示有符號數,不過他們都有奇怪的屬性,就是對數字0分別會有兩種不同的位模式。而這裡要說的補碼就能很好地解決這種問題。
與原碼,反碼類似,補碼也是要靠最高有效位來影響數值的正負。對於長度為w位的補碼錶示,最高有效位為w - 1
,最高有效位的權重為2 ^ (w - 1)
。不難推斷出在一個位元組長的空間內,補碼所能夠表示的有符號數最大值為127
(011111111),最小值為-128
(11111111)。
如果要用補碼來表示整型56
,它所對應的二進位制串依然是00111000
。如果要表示-56
則有
- 2 ^ 7 * 1 + 2 ^ 6 * 1 + 2 ^ 5 * 0 + 2 ^ 4 * 0 + 2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 0 + 2 ^ 0 * 0 = -56
複製程式碼
因此,-56
的補碼錶示為11001000
。在補碼下-56
與56
兩個數值對應的位模式之間有以下關係
11001000 + 00111000 = 1 00000000
複製程式碼
它們相加恰好等於1 00000000
,然而在底層我們只用了一個位元組的空間來儲存它們相加的結果,因此需要對最終結果進行截斷處理。最終得到的結果是00000000
。是不是有點意思?截斷之後恰好就是56 + (-56) = 0
,這也是補碼優雅之處。
與原碼,反碼相比,補碼較大的好處就是對映具有唯一性,對於數值0不會再有兩種不同的表示方式了。在位長為w
的空間中,補碼能夠多表示一個數值- 2 ^ (w - 1)
。此外,它能表示的數值恰好一半是非負數0 ~ 2 ^ (w - 1) - 1
,另一半是負數- (2 ^ (w - 1)) ~ -1
。對8位二進位制串來說則是非負數範圍是0 ~ 127
,負數範圍是-128 ~ -1
。
無符號與補碼的關係
在相同的位模式下,無符號表示與補碼錶示最大的區別就是最高有效位的權重不同。位長為w
的無符號表示中,最高有效位的權重為2 ^ (w - 1)
,而補碼錶示的最高有效位權重為-2 ^ (w - 1)
。當最高有效位為0時,它們所表示的數值相同。只有在最高有效位為1的時候,兩者之間所表示的數值才會有所不同,這個時候它們所表示的數值之間相差|2 ^ (w - 1) - (- 2 ^ (w - 1))| = 2 ^ w
。
簡單舉兩個例子
a. 最高位為0
以無符號的方式來解讀二進位制位模式00011110
,代入公式可得
2 ^ 7 * 0 + 2 ^ 6 * 0 + 2 + 2 ^ 5 * 0 + 2 ^ 4 * 1 + 2 ^ 3 * 1 + 2 ^ 2 * 1 + 2 ^ 1 * 1 + 2 ^ 0 * 0 = 30
複製程式碼
以補碼的方式來解讀可得
-2 ^ 7 * 0 + 2 ^ 6 * 0 + 2 + 2 ^ 5 * 0 + 2 ^ 4 * 1 + 2 ^ 3 * 1 + 2 ^ 2 * 1 + 2 ^ 1 * 1 + 2 ^ 0 * 0 = 30
複製程式碼
此時最高有效位為0,因此它的權重對大局來說沒有任何影響。二進位制位模式00011110
無論是以無符號的方式去解讀還是以補碼,原碼,反碼的方式去解讀,它所代表的數值都是30
。
b. 最高位為1
當最高有效位為1的時候兩者之間差別就比較大了。這個時候補碼所表示的總是負數。
對二進位制位模式10001001
,以無符號的形式進行轉換,代入公式可得
2 ^ 7 * 1 + 2 ^ 6 * 0 + 2 ^ 5 * 0 + 2 ^ 4 * 0 + 2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 0 + 2 ^ 0 * 1 = 137
複製程式碼
而以補碼的方式去解讀則會有
- 2 ^ 7 * 1 + 2 ^ 6 * 0 + 2 ^ 5 * 0 + 2 ^ 4 * 0 + 2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 0 + 2 ^ 0 * 1 = -119
複製程式碼
兩種換算方式所得到的數值相差甚遠,前面推導過兩者相差2 ^ w
。這個例子中就是137 - (-119) = 256
,恰好是2 ^ 8
。
總結
這篇文章主要簡單介紹一下在要用二進位制來表示整數有哪幾種不同的編碼策略,雖說大多數情況下我們都不需要關心底層到底發生了什麼(除非你是在用C/C++來編寫程式碼),不過當作簡單的科普知識來了解一下還是挺有趣的。