1. 程式人生 > >【計算機基礎】在0和1的世界裡來來回回

【計算機基礎】在0和1的世界裡來來回回


事物的正反兩面被哲學家討論了幾千年。計算機裡的0和1也照舊玩出了各種花樣。


二進位制數 VS 十進位制數


本小節講二進位制寫法,以及到十進位制的轉換方法,如果已熟悉這些內容可以直接跳到下一小節。

我們生活在一個十進位制的世界中。10個一毛就是一塊,10個一兩就是一斤。在數學上有滿十進一或借一當十。

十進位制數的基數就是0到9,因此所有的十進位制數都是由0到9這九個數字組合出來的。

計算機底層處理的都是二進位制數,可以對比十進位制數來看看二進位制數的特點:

滿二進一或借一當二,基數是0和1,就是說所有的二進位制數都是由0和1這兩個數字組合出來的。

就十進位制而言,十個1已經達到“滿十”的條件,所以要“進一”,於是就是10,這是個十進位制數,它的值就是十,因為是十個1合在了一起。


就二進位制而言,兩個1已經達到“滿二”的條件,所以要“進一”,於是就是10,這是個二進位制數,它的值就是二,因為是兩個1合在了一起。

如果剛剛這個明白了,結合十進位制和二進位制的特點,接下來就非常容易理解了:

1 + 1 = 2 -> 10。

1 + 1 + 1 = 3 = 2 + 1 -> 10 + 1 -> 11。

1 + 1 + 1 + 1 = 4 = 3 + 1 -> 11 + 1 -> 100。

照此類推,列出幾個十進位制和對應的二進位制:

0 -> 000

1 -> 001

2 -> 010

3 -> 011

4 -> 100

5 -> 101

接下來嘗試找出二進位制和十進位制之間的換算關係。

首先,十進位制數是怎麼使用每個位置上的數字表示出來的呢?相信所有人都很熟悉。如下面示例:

123 -> 100 + 20 + 3

123 -> 1 * 100 + 2 * 10 + 3 * 1

因十進位制滿十進一,要想辦法和十聯絡起來,100就是10的2次方,10是10的1次方,1是10的0次方,於是:

123 -> 1 * 10 ^ 2 + 2 * 10 ^ 1 + 3 * 10 ^ 0;

進而,我們發現百位的位置是3,但次方卻是2,正好是3減去1,十位的位置是2,但次方是1,正好是2減去1,個位就是1減去1,也就是0次方了。

於是,這個公式就出來了,太簡單了,大家都知道,就不寫了。

然後,我們把這個“套路”搬到二進位制數裡試試看吧,只不過二進位制數是滿二進一,因此要用2的次方。

000 -> 0 * 2 ^ 2 + 0 * 2 ^ 1 + 0 * 2 ^ 0
000 -> 0 * 4 + 0 * 2 + 0 * 1 -> 0
000 -> 0

001 -> 0 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 0
001 -> 0 * 4 + 0 * 2 + 1 * 1 -> 1
001 -> 1

010 -> 0 * 2 ^ 2 + 1 * 2 ^ 1 + 0 * 2 ^ 0
010 -> 0 * 4 + 1 * 2 + 0 * 1 -> 2
010 -> 2

011 -> 0 * 2 ^ 2 + 1 * 2 ^ 1 + 1 * 2 ^ 0
011 -> 0 * 4 + 1 * 2 + 1 * 1 -> 3
011 -> 3

100 -> 1 * 2 ^ 2 + 0 * 2 ^ 1 + 0 * 2 ^ 0
100 -> 1 * 4 + 0 * 2 + 0 * 1 -> 4
100 -> 4

101 -> 1 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 0
101 -> 1 * 4 + 0 * 2 + 1 * 1 -> 5
101 -> 5

我們發現算出來的正好都是其對應的十進位制數。這是巧合嗎?當然不是了。其實:

這就是二進位制數向十進位制數的轉化方法。

我們也可以模仿數學,推匯出個公式來:

d = b(n) + b(n - 1) + ... + b(1) + b(0)

b(n) = a * 2 ^ n,(a = {0、1},n >= 0)

就是把二進位制數的每一位轉化為十進位制數,再加起來即可。


負數的二進位制 VS 正數的二進位制


上一小節都是以正數舉例。除了正數之外,還有負數和零。

因此,計算機界規定,在需要考慮正負的時候,二進位制的最高位就是符號位。

即這個位置上的0或1是用來表示數值符號的,而非用來計算數值的,且規定:

0表示為正數,1表示為負數。

那0既不是正數也不是負數,該怎麼表示呢?把0的二進位制輸出一下:

0 -> 00000000

發現全是0,最高位也是0,因此0是一種特殊情況。

接下來開始講解負數的二進位制表示,保證看完後有一種“恍然大悟”的感覺(如果沒有,那我也沒辦法),哈哈。

長期以來受數學的影響,要把一個正數變成對應的負數,只需在前面加一個負號“-”即可。

基於此,再結合上面計算機界的規定,我們很容易想當然的認為,一個正數只要把它的最高位由0設定為1就變成了對應的負數,像這樣:

因為 1的二進位制是,00000001

所以-1的二進位制是,10000001

鄭重宣告,這是錯誤的。繼續往下看就知道原因了。

首先會從官方的角度給出正確結果(裝b用的),然後會從個人的角度給出正確結果(恍然大悟用的)。

站在官方(或學術)的角度,先引入三個概念:

原碼:把一個數當作正數(負數的話把負號去掉即可),它的二進位制表示就叫原碼。

反碼:把原碼中的0變成1、1變成0(即0和1對調),所得到的就叫反碼。

補碼:反碼加上1,所得到的就叫補碼。

(這是學術界的名詞,不要糾結為什麼,記住即可)

還以-1為例,進行一下推導:

把 -1當作 1,原碼是,00000001

把0和1對調,反碼是,11111110

然後加上 1, 補碼是,11111111

於是-1的補碼是,11111111。再使用類庫中的工具類輸出一下-1的二進位制形式,發現竟然還是它。這也不是巧合,因為:

在計算機中,負數的二進位制就是用它的補碼形式表示的。

這就是官方的說法,總喜歡整一些名詞來把大家弄得一懵一懵的。

下面就站在個人角度,以最“土鱉”的方式來揭祕。

首先,-1的二進位制是11111111這種形式一下子確實不容易接受。

反倒是把-1的二進位制假設為10000001更容易讓人接受,因為與它對應的1的二進位制是00000001。

這樣從數值的大小上(即絕對值)來看都是1,從符號上來看一個是1一個是0恰好表示一負一正,簡直“堪稱完美”。

那為什麼這種假設的形式卻是錯的呢?

因為從十進位制的角度來說,1 + (-1) = 0。

再按假設的形式把它們轉換為對應的二進位制,

00000001 + 10000001 = 10000010,

依照假設,這個結果的值是-2。

可見,一個是0,一個是-2,這顯然是不對的。雖然是採用不同的進位制,但結果應該是一樣的才對。

很顯然,二進位制這種計算方式的結果是錯誤的,錯誤的原因是,-1的二進位制形式不能按照我們假設的那種方式進行。

那-1的二進位制應該按什麼邏輯去計算呢?相信你已經猜到了。

因為,-1 = 0 - 1,所以,

-1 = 00000000 - 00000001 = 11111111。

因此,-1的二進位制就是11111111。這樣一來,

-1 + 1 = 11111111 + 00000001 = 00000000 = 0。

這樣是不是一下子就明白了-1的二進位制為什麼全是1了。因為這種形式滿足了數值計算上的需要。

同理可以算下-2的二進位制,

-2 = -1 - 1 = 11111111 - 00000001 = 11111110。

其實原碼/反碼/補碼之間的轉換關係也是基於正數和負數的和為零而設計出來的。仔細體會下便可明白。

可見,官方角度和個人角度的本質是一樣的,只不過一個陽春白雪、一個下里巴人。

這讓我想起來了雅和俗,很多人標榜著追求雅,其實他們需要的恰恰是俗。

下面是一些正數和對應負數的例子:

 2,00000010
-2,11111110

 5,00000101
-5,11111011

 127,01111111
-127,10000001

可以看到十進位制數的和是0,對應二進位制數的和也是0。

這才是正確的負數的二進位制表示,雖然看起來的跟感覺起來的不太一樣。

就十進位制來說,當位數固定後,所有位置上都是9時,數值達到最大,如最大的四位數就是9999。

對於二進位制來說也是一樣的,除去最高位0表示正數外,剩餘的位置全部是1時,數值達到最大,如最大的八位數就是01111111,對應的十進位制數就是127。

一個位元組的長度就是8位,因此一個位元組能表示的最大正數就是127,即一個0帶著7個1,這是正向的邊界值了。

通過觀察負數,除去最高位1表示負數外,後面7位全部為0時,應該是負數的最小值,即一個1帶著7個0,對應的十進位制數是-128,這是負向的邊界值了。

而且正向和負向的邊界值是有關係的,你發現了嗎?就是正向邊界值加上1之後的相反數即為負向邊界值。


二進位制的常規操作


這些內容應該都非常熟悉了,瞄一眼即可。

位操作

與(and):

1 & 1 -> 1
0 & 1 -> 0
1 & 0 -> 0
0 & 0 -> 0

或(or):

0 | 0 -> 0
0 | 1 -> 1
1 | 0 -> 1
1 | 1 -> 1

非(not):

~0 -> 1
~1 -> 0

異或(xor):

0 ^ 1 -> 1
1 ^ 0 -> 1
0 ^ 0 -> 0
1 ^ 1 -> 0

移位操作

左移(<<):

左邊丟棄(符號位照樣丟棄),右邊補0。

移完後,最高位是0為正數,是1為負數。

左移一位相當於乘2,二位相當於乘4,以此類推。

當左移一個週期時,回到原點。即相當於不移。

超過一個週期後,把週期部分除掉,移動剩下的。

移動的位數和二進位制本身的長度相等時,稱為週期。如8位長度的二進位制移動8位。

右移(>>):

右邊丟棄,正數左邊補0,負數左邊補1。

右移一位相當於除2,二位相當於除4,以此類推。

在四捨五入時,正數選擇舍,負數選擇入。

正數右移從都丟棄完開始往後數值都是0,因為從左邊補進來的都是0,直到到達一個週期時,回到原點,即回到原來的數值。相當於不移。

負數右移從都丟棄完開始往後數值都是-1,因為從左邊補進來的都是1,直到到達一個週期時,回到原點,即回到原來的數值。相當於不移。

超過一個週期後,把週期部分除掉,移動剩下的。

無符號右移(>>>):

右邊丟棄,無論正數還是負數左邊都是補0。

因此對於正數來說和右移(>>)沒有什麼差別。

對於負數來說會變成正數,就是使用原來的補碼形式,丟棄右邊後當作正數來計算。

為什麼沒有無符號左移呢?

因為左移時,是在右邊補0的,而符號位是在最左邊的,右邊補的東西是影響不到它的。

可能有人會想,到達一個週期後,再移動的話不就影響到了嘛,哈哈,在一個週期的時候是會進行歸零的。


二進位制的伸/縮


以下內容都假定高位位元組在前低位位元組在後的順序。

伸:

如把一個位元組伸長為兩個位元組,則需要填充高位位元組。(等於把byte型別賦給short型別)

其實就是這個位元組原樣不動,在它的左邊再接上一個位元組。

此時符號和數值大小都保持不變。

正數符號位是0,伸長時高位位元組填充0。

00000110 -> 00000000,00000110

負數符號位是1,伸長時高位位元組填充1。

11111010 -> 11111111,11111010

縮:

把兩個位元組壓縮為一個位元組,需要截斷高位位元組。(等於把short型別強制賦給byte型別)

其實就是左邊位元組直接丟棄,右邊位元組原樣不動的保留。

此時符號和數值大小都可能發生改變。

如果壓縮後的位元組仍能放得下這個數,則符號和數值大小都保持不變。

具體來說就是如果正數的高位位元組全是0,同時低位位元組的最高位也是0。或負數的高位位元組全是1,同時低位位元組的最高位也是1。截斷高位位元組不會對數造成影響。

00000000,00001100 -> 00001100

11111111,11110011 -> 11110011

如果壓縮後的位元組放不下這個數,則數值大小一定改變。

具體說就是如果正數的高位位元組不全是0,負數的高位位元組不全是1,截斷高位位元組肯定會對數的大小造成影響。

至於符號是否改變取決於原符號位和壓縮後的符號位是否一樣。

例如,壓縮後大小發生改變,符號不變的如下:

00001000,00000011 壓縮為 00000011,還是正數
11011111,11111101 壓縮為 11111101,還是負數

例如,壓縮後大小和符號都發生改變的如下:

00001000,10000011 壓縮為 10000011,正數變負數。
11011111,01111101 壓縮為 01111101,負數變正數。


整數的序列化和反序列化


一般來說,一個int型別是由四個位元組組成的,在序列化時,需要將這四個位元組一一拆開,按順序放入到一個位元組陣列中。

在反序列化時,從位元組陣列中拿出這四個位元組,把它們按順序接在一起,重新解釋為一個int型別的數字,結果應該保持不變。

在序列化時,主要用到的就是移位和壓縮。

首先將要拆出來的位元組移到最低位(即最右邊),然後強制轉換為byte型別即可。

假如有一個int型別數字如下:

11111001,11001100,10100000,10111001

第一步,右移24位並只保留最低八位,

byte b3 = (byte)(i >> 24);

11111111,11111111,11111111,11111001

11111001

第二步,右移16位並只保留最低八位,

byte b2 = (byte)(i >> 16);

11111111,11111111,11111001,11001100

11001100

第三步,右移8位並只保留最低八位,

byte b1 = (byte)(i >> 8);

11111111,11111001,11001100,10100000
<