【計算機基礎】在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

10100000

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

byte b0 = (byte)(i >> 0);

11111001,11001100,10100000,10111001

10111001

這樣就產生了四個字節,把它們放入字節數組就可以了。

byte[] bytes = new byte[]{b3, b2, b1, b0};

在反序列化時,主要用到的就是伸長和移位。

首先從字節數組中拿出一個字節,將它轉換為int類型,然后再處理符號問題,接着再左移到適合位置。

第一步:

取出第一個字節,

11111001

然后伸長為int,

11111111,11111111,11111111,11111001

因為它的符號位就表示了原來整數的符號位,因此不用處理符號,直接左移24位,

11111001,00000000,00000000,00000000

第二步:

取出第二個字節,

11001100

然后伸長為int,

11111111,11111111,11111111,11001100

因為它的符號位是處在原來整數的中間位置的,因此它不表示符號而表示數值,需要處理符號位,就是執行一個與操作,

如下,上面兩行相與得到第三行,

11111111,11111111,11111111,11001100
00000000,00000000,00000000,11111111


00000000,00000000,00000000,11001100

接着左移16位

00000000,11001100,00000000,00000000

第三步,

取出第三個字節,

10100000

然后伸長為int,

11111111,11111111,11111111,10100000

然后處理符號位,

00000000,00000000,00000000,10100000

接着左移8位,

00000000,00000000,10100000,00000000

第四步,

取出第四個字節,

10111001

然后伸長為int,

11111111,11111111,11111111,10111001

然后處理符號位,

00000000,00000000,00000000,10111001

接着左移0位,

00000000,00000000,00000000,10111001

這樣四步就產生了四個結果,如下:

11111001,00000000,00000000,00000000
00000000,11001100,00000000,00000000
00000000,00000000,10100000,00000000
00000000,00000000,00000000,10111001

可以看到四個字節都已經位於自己應該在的位置上了。

最后來一個加法操作就可以了,其實或操作也是可以的。

i = i4 + i3 + i2 + i0

i = i4 | i3 | i2 | i0

這樣我們就將字節數組中的四個字節合成為一個int類型的數字了。


模擬實現無符號數


無符號數,即最高位不是符號位而是數值位。

有一些語言如Java不支持無符號數,所以需要使用有符號數來模擬實現。

因為同一個類型作為無符號數時的范圍會大於作為有符號數時的范圍,因此會用更長的類型存放短類型的無符號數。

如byte類型是一個字節,作為有符號數時范圍是-128到127,作為無符號數時范圍是0到255,所以至少需要用兩個字節的short類型來存放。

處理方法很簡單,只需兩步,伸長和處理符號位

假如有一個字節是,10101011,這是一個byte類型的負數。

第一步,伸長,此時變成兩個字節了,但還是一個負數

11111111,10101011

第二步,處理符號,即執行一個與操作

11111111,10101011
00000000,11111111

00000000,10101011

這就已經處理完了,由一個字節的負數變成了兩個字節的正數。

其實就是將原來的字節前面(即左邊)接上去一個全0的字節。

當byte作為無符號數,取到最大值255時,二進制是這樣的

00000000,11111111

此時也只不過才剛剛使用完低位置。

因此使用長類型表示短類型的無符號數,對長類型的字節利用效率最高也就百分之五十了。

對於這種情況,在序列化時,其實只需寫入低半部分的字節即可。

在反序列化時,一是要用長類型來承接,二是所有字節都要處理符號,作為無符號數對待。

PS:這次算是認認真真的復習了十年前在大學里的專業課基礎知識。

其實我是在寫“品Spring”系列文章時,發現最好能熟悉Java的字節碼(.class)文件內部結構。

在嘗試解析字節碼文件時,發現它里面存儲的都是無符號數,所以需要寫一個把字節數組反序列化為無符號數的工具。

在寫工具時看了一點JDK相關部分的源碼,就索性把二進制的基本知識和操作都親自寫代碼測試了一遍。

於是就整理出了這篇文章,呵呵。

文中所有代碼示例:
https://github.com/coding-new-talking/java-code-demo.git



(END)


作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

 

       



免責聲明!

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



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