負數為什么要用補碼來表示?
今天,發生一件非常有趣的事情。
公司同事問了我一個問題:為什么 2.0 - 1.1 = 0.89999999 呢?不應該是 0.9嗎?
原來是,他問了周圍一圈的同事,都給他的是同一個回答,說這是精度問題。他百思不得其解,怎么就會產生精度問題呢。再問,就沒人知道原因了。
然后,我就看到了他抱着一本厚厚的書在看。拿過來一看,是一本Java書,厚厚的六百多頁,這還僅是第一卷。喲呵,這是准備大干一場啊。
看在他這么努力學習的份上,還有他那對知識極度渴望的眼神。我決定,把我畢生所學傳授與他。
於是,就給他詳細講解了,計算機中是怎么存儲一個數的,十進制是怎么在轉二進制的過程中丟失精度的,以及浮點數是怎么遵循IEEE 754 規范的,在浮點數進行加減運算的過程中會經歷對階、移位運算等過程,以及在此過程中是怎么丟失精度的。(這些問題在之前的文章中都有解答,參看“為什么0.1+0.2=0.30000000000000004”)
然后,成功的把他徹底搞懵逼了。怎么這么難啊。
原來,他的計算機基礎比我還匱乏,不知道什么是位運算,不知道什么是原碼、反碼和補碼。
本着我的熱心腸,我就給他普及了一下這些知識 ---- 負數的補碼形式和位移運算。
我們知道,一個數分為有符號和無符號。對於,有符號的數來說,最高位代表符號位,即最高位1代表負數,0代表正數。
在計算機中,存儲一個數的時候,都是以補碼的形式存儲的。而正數和負數的補碼表示方式是不一樣的。正數的補碼就等於它的原碼,而負數的補碼是原碼除符號位以外都取反,然后 + 1 得來的。以一個int類型為例(4個字節即32位)
14的原碼為:
0000 0000 0000 0000 0000 0000 0000 1110
它的反碼、補碼和原碼都是一樣的。
-14的原碼為:
//最高位1為符號位,代表此數為負數 1000 0000 0000 0000 0000 0000 0000 1110
反碼為原碼除了符號位以外的其他位都取反(即0變為1,1變為0),
1111 1111 1111 1111 1111 1111 1111 0001
補碼為反碼 + 1 ,注意二進制中是滿二進一。
1111 1111 1111 1111 1111 1111 1111 0010
位的左移,右移運算就是分別向左和向右移動N位。移位的規則是:
- 不管有沒有符號位,左移都是在低位補0
- 帶符號右移,是在高位補符號位,即正數補0,負數補1
- 無符號右移,無論該數是正數還是負數都在高位補0
因左移就在右邊低位補0就可以了,比較簡單,我就以負數的右移來舉例,是怎么計算無符號右移和帶符號右移的。還是以 -14 為例。
// -14的補碼 1111 1111 1111 1111 1111 1111 1111 0010 // 帶符號右移用 >> 表示,即右移一位 -14>>1,高位補符號位1,低位舍去 1111 1111 1111 1111 1111 1111 1111 1001 // 無符號右移用 >>> 表示,即右移一位 -14>>>1,最高位補0 0111 1111 1111 1111 1111 1111 1111 1001
我們可以通過程序來驗證一下 -14>>1和 -14>>>1的結果是否正確。
1. -14>>1 = -7
//我們算出來 -14>>1的補碼為: 1111 1111 1111 1111 1111 1111 1111 1001 //那它具體代表的數值是多少呢? //首先,補碼 -1 得到反碼 1111 1111 1111 1111 1111 1111 1111 1000 //然后,反碼取反得到原碼,最高位符號位不變 1000 0000 0000 0000 0000 0000 0000 0111
這結果不就是 -7 嗎,然后通過程序計算一下結果:
public class TestMove { public static void main(String[] args) { System.out.println( -14>>1); } }
結果同樣也是-7 。說明了我們位移操作沒問題。
2. -14>>>1=2147483641
我們通過一段程序去驗證:
package com.test.binary;
/** * @Author zwb * @DATE 2019/12/3 15:49 */ public class TestBinary { public static void main(String[] args) { //我們自己計算出來的 -14>>>1 結果 String bin = "01111111111111111111111111111001"; double res = binToDec(bin); System.out.println(res); //通過計算機計算的結果 System.out.println(-14>>>1); } //二進制轉為十進制 public static double binToDec(String bin){ int index = bin.indexOf("."); int len = bin.length(); double res = 0; //index為-1說明沒有小數 if(index == -1){ for(int i = 0; i< len; i++){ res += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(len-1-i))); } }else{ //整數部分 int partA = 0; for(int i = 0; i< index; i++){ partA += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(index-1-i))); } //小數部分 double partB = 0; for(int j = index + 1; j < len; j++){ partB += Math.pow(2,index - j) * Integer.parseInt(String.valueOf(bin.charAt(j))); } res = partA + partB; } return res; } }
運行之后的結果,可以在控制台打印看到:
上邊第一個是我們自己通過推算它的補碼,然后通過二進制轉十進制的一個算法算出來的最終結果,第二個就是直接通過位運算算出來的結果。可以看到結果是一模一樣的。
至此,是不是對原碼,反碼,補碼以及位運算左移右移,有了比較清晰的認識了呢?
今天,我們深入探討一下,為什么計算機中要用補碼來表示負數?
首先,我們應該清楚,原碼是方便給人看的。看到一個數的原碼,我們就能根據符號位和后邊的二進制位,計算出這個數的實際值。為了簡單起見,我以一個字節8位來舉例,如
// 1 的原碼 ,最高位0代表正數 0000 0001 // -1 的原碼, 最高位1代表負數 1000 0001
可以看到,1和 -1 的原碼只有符號位不同。然后,思考一個問題,1 - 1 = ?
是的,我們可以直接通過減法去計算,得出1-1=0 。但是,做減法運算時,可能會遇到不夠減而需要借位的情況,這顯然是比較麻煩的。我們換一種思路。 1-1 在數學中等同於 1+(-1)。這樣,把減法轉換為加法就簡單的多了,只需要考慮進位就可以了。(其實,計算機中只有加法器,沒有減法器,因此減法是通過加法器來計算的。)
於是,我們看下,把1和-1的原碼相加等於多少(需要讓符號位也參與運算)
0000 0001
+ 1000 0001 1000 0010
結果是 -2 ,這顯然不符合我們的預期。
為了解決原碼減法的問題,於是,出現了反碼。使用反碼,再來計算一下。
// 1的反碼,同原碼 0000 0001 // -1的反碼,符號位不變,其他取反 1111 1110
相加之后,得 1111 1111 ,這是反碼,轉為原碼為 1000 0000 ,即為 -0 。
但是,這又有問題了,在數學中0就是0,怎么到這還有 -0,+0之分。按照原碼的概念來算,+0的原碼為0000 0000 , -0 的原碼為 1000 0000 。問題就出在這了,如果遇到0的計算,是應該用 +0 還是用 -0 計算呢,這就會產生分歧。於是,補碼出現了,解決了0的符號問題 。
// 1的補碼,同原碼 0000 0001 // -1的補碼,反碼 +1 1111 1111
相加得 1 0000 0000 ,最高位進位之后,超過了8位,於是舍去,即為0000 0000。此為補碼,轉為原碼也是0000 0000 ,這不就是0 嗎。
這樣一來,用補碼0000 0000來表示0,就解決了+0和-0在原碼上的分歧,統一了0的二進制表示方法。
那,又有疑問了,-0跑哪去了呢? 其實,-0即1000 0000在這用來表示 -128。但是,注意表示的是 -128的補碼,因此 -128沒有原碼和反碼。
那為什么用 1000 0000表示 -128呢 ?
先看下 -127 的原碼、反碼和補碼:
原碼: 1111 1111
反碼: 1000 0000
補碼: 1000 0001
我們知道數學中 -127 -1 = -128 ,所以 -127的補碼 -1 也應該等於 -128的補碼,即
1000 0001 -1 = 1000 0000。因此1000 0000就是 -128的補碼。
在一個字節8位中,如果用原碼來表示值的大小范圍,只能是 1111 1111 ~ 0111 1111,即-127~127 。但是,如果用補碼就可以表示 -128~127,正好是2^8,256個數。
因此,-0可以表示一個最低數。在8位二進制中它是1000 0000 ,在32位中,它就是 1000 0000 0000 0000 0000 0000 0000 0000 ,int的最小值。(32位數值大小范圍為 -2^31 ~ 2^31 -1)
總結:補碼的存在解決了0的符號問題,同時統一了計算機的加減法運算。