1. 程式人生 > >二進位制:基礎、正負數表示、儲存與運算

二進位制:基礎、正負數表示、儲存與運算

一、概述

眾所周知,計算機是由各種電子元器件組成的,其中半導體可以通過它的開關狀態來傳遞和處理資訊,所以我們可以用1和0分別表示開和關兩種狀態。邏輯電路通常只有接通和斷開兩種狀態,所以我們以二進位制來處理會非常方便。所謂二進位制表示從0開始,“逢二進一”(N進位制則逢N進一)。比如十進位制的0、1、2的二進位制表示為0、1 、10。

二、進位制轉換

網上有很多進位制轉換的方法,我這裡就不多做闡述,只介紹一個我自己常用的簡便方法(自認為簡便o(╯□╰)o)。都知道,二進位制表示中,1:表示2^0(即十進位制1),10:表示2^1(即十進位制2),100:表示2^2(即十進位制4)......依次類推,所以111=100+10+1=2^2+2^1+2^0=4+2+1=7。於是,我們得出如下方法:

二進位制->十進位制: \sum_{i=0}^{n}x{2^n},其中 n表示從低位往高位數的位數(從0開始),x表示對應位數上的值(1或者0),示例:101101=>1 * 2^0 + 0 * 2^1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 = 1+0+4+8+0+32=45

十進位制->二進位制:相當於二進位制轉換十進位制的反推,比如要將十進位制數N轉換為二進位制:第一步,找到一個最接近且小於等於N的為2的乘方的數m(即m=2^x,x為整數),記錄下來,二進位制對應x位上即為1,;第二步,對N-m重複步驟一,知道N-m==1或0。示例,將十進位制數168轉換為二進位制:

1、如果等於1或0,則直接輸出,反之進行下一步

2、找到最接近且小於等於168的2的乘方的數為128(2^7),所以第7位為1,記錄下來(10000000);

3、然後對40(即168-128)重複0、1步驟,找到32(2^5),所以第5位為1,記錄下來(10100000);

4、然後對8(即40-32)重複0、1步驟,找到8(2^3),所以第3位為1,記錄下來(10101000);

5、然後對0重複1步驟(此時為0,直接輸出0,表示第0位為0)

所以最終結果為:128+32+8 -> 10101000

運用熟練後,對不是特別大(這個多大才算大就因人而異了o(╯□╰)o)的十進位制數都能做到快速的轉換,比如237,可以不用動筆計算快速的得出結論:

237 -> 128+64+32+8+4+1 -> 然後從右往左直接寫出結果:11101101

三、原碼、反碼、補碼和儲存

1、闡述

首先,前面我們講了,計算機中只由1和0表示,當然包括我們常見的表示負數的符號"-",規定:一個有符號二進位制數,其最高位為符號位,1表示負,0表示正,剩餘位才是數值域。所以一個位元組(8位),如果無符號可以表示2^8個(其範圍為:0~2^8-1)不同的數(在計算機中可能是對應2^8個不同的狀態),但是如果是有符號數,則需要犧牲最高位來表示符號位,所以雖然同樣可以表示2^8個數,但是表示範圍成了:-2^7 ~ 2^7-1。在此,對於原碼、反碼、補碼,我先給出概念,下面再詳細闡述,

正數:原碼、反碼、補碼都是其本身

負數:原碼=本身;反碼=原碼符號位不變,其它位取反;補碼=反碼+1,例:10010001:原=10010001 反=11101110 補:11101111

系統中二進位制均以補碼形式存在。

2、詳解

我們用最高位表示符號位,的確能夠在一定程度解決負數在計算機中的儲存問題,但是運算時又發現了另一個問題。我們就簡單的加減法運算舉個例子:1 - 1 = 0,這個是毋庸置疑的,如果換成二進位制(假設我們用8位儲存),那麼,-1的二進位制為:10000001,1的二進位制為:00000001,所以1 - 1 = -1 + 1 = 10000001 + 00000001 = 10000010 = -2,明顯結果不對,所以符號位不能直接參與數的運算,而要處理又會非常麻煩;另外一方面,現在0這個數尷尬的出現了兩種表現形式:00000000和10000000(分別為+0和-0),這沒有什麼意義。為了解決問題,人們機智的提出了補碼的概念,接下來我們一步步闡述。

我們的程式設計師朋友在開發過程中,都會涉及到各式各樣的資料型別,比如java中的byte、int、long等等(java中均為有符號數)。而且大家也熟知,byte佔1個位元組,int佔4個位元組,long佔8個位元組,這表示什麼含義呢?舉個例子,如果我們要把一個int型別的數強轉為byte,可能會造成精度缺失,比如:130(int)在系統中的表示為:00000000 00000000 00000000 100000010,現在把130強轉為byte,即執行如下操作:(byte)130,由於byte型別只能儲存8位,所以結果只剩下低8位:100000010,而最高位表示符號位,這個明顯已經是一個負數,現在我們按照進位制轉換規則求解:注意這個是補碼形式,需要轉換為對應的原碼我們才好看出來是多少,按照原碼-補碼轉換規則執行一次即可:100000010(補)->111111101(不是反碼,只是一箇中間數)->11111110(原碼),對應十進位制數為:-126,所以 ,System.out.println((byte) 130)的輸出結果為:-126。

有了上面的介紹,我們再來看二進位制的加法運算。一個8位二進位制數,暫時先不考慮符號位,其能表示的最大的值為:11111111(255),如果現在在上述值的基礎上加1,按照逢二進一的原則,則成了100000000,由於最多儲存8位,所以結果成了:00000000,如果再加1,就成了00000001,依次類推。所以,在8位儲存的背景下,如果我們從0開始一直不停的往上加1,其過程就會是這種形式:0,1,2...255,0,1,2,...255,0,1...,這樣一直重複,是不是感覺一直在轉圈圈?其實這個就和我們做模運算是一個道理。現在拋開二進位制,舉個簡單的例子,咱們小學數學常見的分數,分母為4的分數:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}\frac{5}{\displaystyle 4}\frac{6}{\displaystyle 4}\frac{7}{\displaystyle 4}......等等可以表示為:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}1\frac{1}{\displaystyle 4}1\frac{2}{\displaystyle 4}1\frac{3}{\displaystyle 4}......,其中的1\frac{3}{\displaystyle 4}則表示:1 + \frac{3}{\displaystyle 4},即以4為週期過了1個週期,第二個週期到了四分之三處,表示餘數,所以讀作:一又四分之三,後面還會有二又四分之三、三又四分之三等等。

我們前面舉的二進位制例子和這個是同樣的道理,11111111+1表示一個255週期滿了,開始下一個週期,即"一個255又0",但是我們只能儲存8位,前面那個表示1個255的1,已經到了第9位,被直接丟棄了,所以結果為00000000。接著分數的例子,如果我們模擬丟棄高位的處理邏輯,把前面的係數丟棄,只保留餘數,則成了:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}.......。明顯看出來這是一個“迴圈”,然後我們再來看這個“迴圈”,我們要把\frac{3}{\displaystyle 4}通過運算得到\frac{2}{\displaystyle 4},有哪些方案?方案1:\frac{3}{\displaystyle 4} - \frac{1}{\displaystyle 4} ,即向後移動1步;方案2:\frac{3}{\displaystyle 4} +\frac{3}{\displaystyle 4} ,即向前移動3步。之前在其他地方看到過一個例子:時鐘。時鐘的小時計數都是1~12,。假設我們要把指標從5點轉到6點,可以順時針轉1個小時單位,也可以逆時針轉11個小時單位。時鐘裡的12在數學中表示模,而1和11則互為補數。所以,這類結構資料的減法都可以使用加法來代替。

再談補碼:

還是假設使用8位儲存,參照上面的例子(無符號數),其能表示的最大的數為255,如果再加1,最高位被丟棄,回到了0,所以其模為2^8=256,這樣我們就可以把模的概念運用到二進位制的運算中來,具體就是運用補數。再看我們之前遇到的問題,1-1則可以看做是1+(1的補數),而1的補數=256-1=255即二進位制形式的:11111111,所以1-1=00000001+11111111=100000000,最高位丟棄,結果為:00000000=0,這樣計算就沒有問題了。這裡我們用11111111表示了-1,你可能已經看出來了,這就是-1(10000001)的補碼。所以我們可以說,負數的補碼,其實就可以看做是對應正數在當前模(n位,其模為2^n)下的補數(二進位制形式)。

接下來我們回到有符號數

注:如果我們對一個正數的原碼取反加一,就可以得到這個正數對應負數的補碼,比如還是8位儲存,十進位制正數4,其二進位制:00000100,取反:11111011,再加一:11111100,其 為 10000100(-4)的補碼。

因此,一個8位二進位制正數的補碼範圍為:00000001~01111111(1~127),負數的補碼範圍(可通過對應正數的原碼取反加一求得)為:10000001~11111111(-127~-1),再加上00000000,所以我們能表示的範圍成了10000001~01111111(-127~127)。這裡等等,細心的朋友可能發現了,上面表示的補碼範圍中,10000000呢?明顯不可能直接丟棄。其實,我們可以先按照二進位制運算規則來算一算:-127-1等於多少,當然從數學的角度明顯等於 -128,那麼從8位二進位制的角度考慮呢?即 -127 -1 = 10000001(-127的補碼)+11111111(-1的補碼)= 110000000需要捨棄最高位,最終也得到10000000。所以我們可以把10000000表示為-128。 這樣還同時解決了+0 和-0的問題,保證了資料的完整性,運算邏輯也沒有問題。所以執行:System.out.println((byte)(127+1)),你得到的結果會是:-128,執行:System.out.println((byte) -(127 + 1)),結果也是:-128。就像我們把時鐘從12點開始,順時針轉動12個小時單位和逆時針轉動12個小時單位得到的結果是一樣的。

如果上面的還是不好理解,可以看看下面的例子(僅僅用作輔助理解):

一個8位儲存的二進位制數,我們用最高位表示符號位,數值域也就只剩下7位,能表示的最大的數就成了127(2^7-1),即模=2^7=128。我們定義從0開始,往前走一步為+1,往後退一步為-1,形成迴圈0,1,2...127,0,1,2...,週期為128步。然後,我們就可以開心的舉例子了:

8 - 6 = 8 + (6的補數) = 8 + (128-6) = 8 + 122 = 130:表示在8的基礎上往前走121步,得到1個128又2:結果=2(等價於8往後退2步可以得到6,即8-2=6);

6 - 8 = 6 + (8的補數) = 6 + (128 - 8) = 6 + 120 = 126:表示在6的基礎上往前走120步,得到差2步到1個128:結果= -2(等價於6往前走2步可以得到8,即6+2=8),-2怎麼表示呢?最高位表示為1即可表示負數:即10000010(原)。

這當然是非常“粗俗”的解釋,我們把8-6和6-8的操作對應到計算機的實際運算過程:

1、8 - 6

首先,求得-6的補碼,演示兩種方式:

方式一(通過反碼求補碼):-6->10000110(原)->11111001(反)->11111010(補)

方式二(通過正數原碼取反加一):6->00000110(原)->全部取反加一 ->11111010

然後和8(00001000)做加運算:00001000(補)+11111010(補)=1 00000010(補),最高位丟棄:00000010=2(正數,原碼==補碼)

2、6-8

首先,求得-8的補碼:8->00001000(原)->全部取反加一->11111000

然後和6(00000110)做加運算:00000110(補)+11111000(補)=11111110(補),轉換為對應的原碼為:10000010(原) = -2

以上為本人個人理解,希望能幫到廣大朋友,如有錯誤之處,懇請指正,謝謝!