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

  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相關部分的原始碼,就索性把二進位制的基本知識和操作都親自寫程式碼測試了一遍。