你知道原碼、反碼、補碼的真正含義嗎?點進來讓你不在迷惑!!!
我們知道日常生活中使用的數分為整數和實數,整數的小數點固定在數的最右邊,可以省略不寫,而實數的小數點則不固定。在計算機中隻能識别和表示“0”和“1”,而無法識别小數點,是以要想使得計算機能夠處理日常使用的資料,小數點的問題是不可避免的。
關于計算機系統中實數的表示,在下篇文章中會講解。本篇部落格我們講解的是整數在計算機系統中如何表示。
在各種大學教材,各種網站論壇中,對于整數編碼表示方法的正确打開姿勢(姿勢要帥)如下:

1、機器數
機器數(computer number)是數字在計算機中的二進制表示形式。機器數有2個特點:
①、符号數字化。因為計算機硬體隻認識兩種實體狀态(用0和1表示),是以資料的正負号在機器裡就用一位二進制0或者1來區分。在計算機用一個數的最高位存放符号, 0代表符号“+”,以1代表符号“-”。
②、機器數的大小受機器字長的限制。機器内部裝置一次能表示的二進制位數叫機器的字長,一台機器的字長是固定的。字長8位叫一個位元組(Byte),機器字長一般都是位元組的整數倍,如字長8位、16位、32位、64位。
比如在字長為8的計算機中,十進制數+5,其機器數為00000101;十進制數-5,其機器數為10000101。
2、真值
計算機機器數真正的值稱為真值。因為機器數的最高位是符号位,是以我們在計算真值的時候要分區分開。
比如上面講的機器數10000101,單純作為一個二進制數,我們轉換為十進制是133。但是其真值是不計算符号位的,其最高位的1表示"-"。是以10000101的真值為-5。
3、機器數的原碼、反碼、補碼三種形式
前面我們講過機器數是在計算機中的二進制表示形式,但是在計算機中,這種表現形式又分為原碼、反碼、補碼等三種最常用的形式。
ps:下面舉例都是字長為8。
①、原碼
原碼=符号位+真值
比如:
[+5]原碼=0 0000101
[-5]原碼=1 0000101
原碼表示與真值對應直覺,而且轉換也簡單。但是用原碼進行加減運算的時候,會出現以下問題:
使用原碼計算表達式:1 - 1 = 0
1 - 1 = 1 + (-1)= [00000001]原 + [10000001]原 = [10000010]原 = -2
注意:計算機是沒有減法器,隻有加法器,減法運算可以轉換為加上那個數的負數。
我們發現通過原碼計算1 - 1 表達式結果居然是 -2。是以早期計算機機器數采用原碼編碼的時候,在進行原碼加減運算時,必須先判定是否是兩個異号數相加或兩個同号數相減,若是,則必須判定兩個數的絕對值大小,根據判斷結果決定運算結果符号,并用絕對值大的數減去絕對值小的數。也就是說用這樣一種形式進行加運算時,負數的符号位不能與其數值部分一道參加運算,而必須利用單獨的線路确定符号位。很顯然,這樣設計電路就很複雜,這是不經濟實用的,為了解決這個問題,反碼産生了。
②、反碼
反碼:正數的反碼與其原碼相同;負數的反碼是對其原碼逐位取反,但符号位除外。
我們用反碼來計算 1 - 1
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
看上去結果好像是正确的了,但是大家發現沒,結果是-0,雖然對于0的符号沒有什麼實際意義。但是在計算機中,0如果用原碼和反碼表示會有兩種形式:
[+0]=[0000 0000]原=[0000 0000]反
[-0]=[1000 0000]原=[1111 1111]反
兩種編碼就兩種編碼吧,隻不過是多占用一個計算機表示數的編碼形式。隻要結果是正确的,我們還是能夠忍受的,然而。。。。
請用反碼計算表達式 2 -1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反 = [0000 0000]反 = [0000 0000]原 = +0
是不是很奇怪,原碼計算 2 - 1 得到的結果居然是 0 。其實稍微分析計算過程我們也知道,再用反碼進行加法運算的時候發生了進位,而由于字長為8,進位就直接省略了,便造成了錯誤。這肯定是不被允許的,是以采用反碼的計算機解決辦法如下:
反碼的符号位相加後,如果有進位出現,則要把它送回到最低位去相加(循環進位)。
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反+[0000 0001]循環進位 = [0000 0001]反 = [0000 0001]原 = +1
13 - 6 = 13 + (-6)= [0000 1101]原 + [1000 0110]原 = [0000 1101]反 +[1111 1001]反+[0000 0001]循環進位=[0000 0111]反 = [0000 0111]原 =+7
采用反碼運算雖然較好的解決了原碼運算所遇到的困難或問題,但由于循環進位需要二次算術相加,延長了計算時間,這同樣給電路帶來麻煩。這時候補碼登場了。
③、補碼
補碼:正數的補碼與原碼相同,負數的補碼等于其反碼的末位加1
我們來看下面這個例子:
2 - 1 = 1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反 = [0000 0010]補 + [1111 1111]補 = [0000 0001]補 = [0000 0001]原 =+1
9 + 12 = 21
9 + 12 = [0000 1001]原 + [0000 1100]原 = [0000 1001]補 + [0000 1100]補 = [0001 0101]補 = [0001 0101]原 = 21
我們發現補碼運算就很簡單了,産生的進位直接舍去,而且不做多餘的操作也解決了進位的問題。還有 +0 和 -0 的表示,在原碼和反碼都有兩種形式,但是補碼卻隻有一種:
[+0]=[0000 0000]原=[0000 0000]反=[0000 0000]補
[-0]=[1000 0000]原=[1111 1111]反=[0000 0000]補
就這樣我們完美的解決了計算機中整數運算的問題。計算機的機器數采用補碼的形式,我們在做算術運算的時候,既不需要額外的判斷,又能得到準确的結果。
看上去本文應該結束了,然而......
請求出 127+1 的值
4、溢出
接着上面抛出的問題,127+1的值,我們現在程式中看看:
public static void main(String[] args) {
byte x = 127;
byte y = 1;
byte k = (byte) (x+y);
System.out.println(k); //-128
System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127
}
byte在計算機正好是一個位元組,也就是8位二進制序列。我們發現127+1結果不是128,反而是-128,這就是結果發生了溢出。因為byte表示數的範圍是-128-127,128超出了這個範圍。用補碼計算如下:
1 + 127 = [0000 0001]原 + [0111 1111]原 = [0000 0001]補 + [0111 1111]補 = [1000 0000]補
我們發現這個數的符号位沒有發生進位,但是數值最高位發生了進位。在看前面的2-1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]補 + [1111 1111]補 = [0000 0001]補 = [0000 0001]原 =+1
這個表達式符号位和數值最高位發生了進位,但是結果卻是正确的。總結如下:
隻有一個高位進位或者符号位進位就為溢出的規則。
溢出是每種編碼在運算時都不可避免的,一般來講結果超過字長所表示數的範圍都會發生溢出。而判斷機器是正常進位還是溢出的基本依據,在微型機中可用異或電路來實作上述的判斷。在實際編碼中解決辦法也很簡單,就是将結果用更大範圍的編碼形式接收即可。比如兩個byte類型的數相加,我們用 int 來接收即可。
public static void main(String[] args) {
byte x = 127;
byte y = 1;
int k = x+y;
System.out.println(k); //128
System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127
}
是以我們可以說用補碼進行運算,在不考慮溢出的情況下,結果都是正确的。确實也是這樣,但是......
請求出 -128 的補碼?
5、劇情反轉
上面的給出的問題,-128 的補碼,我們首先想到去求它的原碼,嗯,原碼應該是 [1000 0000],不對,第一位不是符号位嗎,[1000 0000]應該表示 -0。那應該怎麼用原碼表示 -128呢,我們發現字長為 8 的計算機用原碼是無法表示的,反碼也是一樣。我們看看補碼,用 -127- 1 的表達式結果來計算 -128 的補碼:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]補 + [1000 0001]補 = [1000 0000]補
-128的補碼形式為 [1000 0000],我們能通過算術表達式得到某個數的補碼形式,但是為什麼直接就求不出來?那麼計算機自己是怎麼實作的呢?
再來看這樣一個問題:我們日常使用的鐘表,比如現在鐘表指向的是 10點鐘,我要将鐘表調整到 6 點鐘,則有兩種撥法:
①、順時針将時針撥動 8 格
②、逆時針将時針撥動 4 (12-8) 格
由此給大家普及一個概念叫 “模”,鐘表便是一個典型的模運算系統,其模數為12。
同理,對于十進制兩位數,在将結果百位舍掉的情況下,50可以用60-10得到,或者60+90得到。這裡的90也就是100-10得來的,那麼我們就說十進制兩位數運算系統的模數為100。
我們判定:兩個相加等于模的數互為補數。
在模表示的範圍内做減法運算,可以将“X-Y”的減法變更為“X+Y的補數“的加法,當然這裡不考慮結果溢出。
上面我們舉的例子都是大數減小數,如果是小數減大數會怎樣?
如果是10-80,結果應該是-70。但是如果按照 10+(100-80)的說法,結果是30。很明顯,30和-70不是同一個結果,而且也沒有産生百位進位。那我們應該怎麼辦呢?
解決辦法很簡單,就是讓這兩個數相等,而且這正好解決了負數的表示方法,-70的絕對值的補數正好是30。
但是問題又來了,這裡的30已經表示正數30了,現在又表示負數-70,那我們怎麼知道它到底表示哪個數?
為了解決這個問題,我們給這套規則規定一個範圍,原來是0~99的正數,現在既然要用部分正數來代替負數了,那就要規定一個範圍來使得一個數隻代表一個含義,正好一人一半,0~49這個區間就代表正數,50~99的區間就用來代表各自補數的負值,例:98就代表-2
是以0-99的編碼數可以表示的數的範圍為 -50-49。
我們解決了十進制兩位數的減法運算,那麼在字長為 8 的計算機系統中,我們又該如何呢?
8位二進制數可以表示的數為2的8次方,0-255,一共 256 個數,0也要占用一位數。是以我們說 256 是8 位二進制數的模,這和上面說的十進制兩位數0-99,模為100是一樣的。
我們按照前面講的邏輯,一半的數0-127,代表其正數本身,另一半的數128-255表示其補數的負值,即-1~-128。
是以而 “X-Y”的減法 就用 “X+Y的補數” 的加法來表示,即将減法的形式轉換為加法的形式了,而且計算結果還是正确的。
注意:這裡還是一樣,不考慮結果的溢出,也就是計算值和結算結果都必須在-128~127之間,一旦超過這個範圍,結果就不準了,這也是程式員日常編碼說的int=int+int,如果結果大于int類型表示的範圍,那得出來的結果肯定不準。
由此我們得出來的結論是:
計算機編碼其實并沒有什麼所謂的符号位,但是由于計算機沒有減法運算,為了将負數變為某個可以運算的編碼來進行加法運算,補碼産生了。這也間接說明了正數的補碼是不變的,而負數的解決辦法是首位不變,其餘的取反再加1。
我們上面說的補碼怎麼得來的,就是 模-絕對值 。
是以這個時候我說讓你求 -128的補碼,你馬上就會知
256 - |-128|=128 而128的二進制補碼是不是就是 [1 0 0 0 0 0 0 0]
讓你求 -1 的補碼,你馬上就會知
256 - |-1| = 255,其255的二進制補碼形式就是[1111 1111]
注意:關于這樣求補碼的具體數學證明,請參考《計算機組成與系統結構》。
6、總結
本篇文章你可以直接從第 5 點開始看,忘掉計算機編碼的什麼首位符号位,忘掉計算機補碼是由原碼取反加1,回歸簡單直白的了解。計算機是機器,硬體能了解的隻有高低電平,也就是0或者1。它知道什麼是符号位嗎?這些規則隻不過是為了更好的完成減法運算所yy出來的。
大學也學過這些編碼方式,但是都是背書式記憶,希望這篇文章能給大家帶來一些幫助。
個人見解,如有錯誤歡迎大家抛磚!!!
參考書籍:《計算機組成與系統結構》
參考文章: https://www.zhihu.com/question/20458542?sort=created
作者:IT可樂
出處:http://www.cnblogs.com/ysocean/
資源:微信搜【IT可樂】關注我,回複 【電子書】有我特别篩選的免費電子書。
本文版權歸作者所有,歡迎轉載,但未經作者同意不能轉載,否則保留追究法律責任的權利。