一個案例搞懂原碼、反碼、補碼,不懂得請看過來

2021-01-10 酷扯兒

本文轉載自【微信公眾號: 五角錢的程式設計師 ,ID:xianglin965】

一起學習、成長、溫情的熱愛生活

圖丨pexels

1. 預備知識

認識二進位,十六進位。會二進位與十進位的相互轉化運算由計算機的硬體決定,任何存儲於計算機中的數據,其本質都是以二進位碼存儲。

根據馮~諾依曼提出的經典計算機體系結構框架。一臺計算機由運算器,控制器,存儲器,輸入和輸出設備組成。其中運算器,只有加法運算器,沒有減法運算器(據說一開始是有的,後來由於減法器硬體開銷太大,被廢了 )所以,計算機中的沒法直接做減法的,它的減法是通過加法來實現的。你也許會說,現實世界中所有的減法也可以當成加法的,減去一個數,可以看作加上這個數的相反數。當然沒錯,但是前提是要先有負數的概念。這就為什麼不得不引入一個該死的符號位。

1. 而且從硬體的角度上看,只有正數加負數才算減法。2. 正數與正數相加,負數與負數相加,其實都可以通過加法器直接相加。原碼,反碼,補碼的產生過程,就是為了解決,計算機做減法和引入符號位(正號和負號)的問題。

2. 正數位移運算

有三個位移運算:

<<:左移>>:右移>>>:無符號右移

我們來舉一個慄子:

public class test {public static void main(String[] args){ System.out.println(3 <<1);// 6 System.out.println(3 >>1);// 1 System.out.println(3 >>>1);// 1 System.out.println(-3 <<1);// -6 System.out.println(-3 >>1);// -2 System.out.println(-2 >>>1);// 2147483647 }}

是不是一眼看到上面慄子的列印結果,是不是很懵逼,下來我來解釋一下這個結果到底是如何運算出來的。

上面的慄子中有「3」和「-3」,這是兩個十進位數,並且是int類型的(java中佔四個字節),位運算是基於二進位bit來的,所以我們需要將十進位轉換為二進位之後再進行運算:

3 >> 1:十進位「3」轉換成二進位為「00000000 00000000 00000000 00000011」,再將二進位右移一位,低位丟棄,高位補0,所以結果為「00000000 00000000 00000000 00000001」,換算成十進位則為「1」3 << 1:十進位「3」轉換成二進位為「00000000 00000000 00000000 00000011」,再將二進位左移一位,高位丟棄,低位補0,所以結果為「00000000 00000000 00000000 00000110」,換算成十進位則為「6」對於這兩種情況非常好理解,那什麼是無符號右移,以及負數是怎麼運算的呢?我們先來看-3 << 1與-3 >> 1,這兩個負數的左移與右移操作其實和正數類似,都是先將十進位數轉換成二進位數,再將二進位數進行移動,所以現在的關鍵是負數如何用二進位數進行表示。3. 原碼、反碼、補碼

接下來我們主要介紹十進位數用二進位表示的不同方法,所以為了簡潔,我們用一個字節,也就是8個bit來表示二進位數。

1. 原碼

最高位為符號位,0代表正數,1代表負數,非符號位為該數字絕對值的二進位表示。

原碼其實是最容易理解的,只不過需要利用二進位中的第一位來表示符號位,0表示正數,1表示負數,所以可以看到,一個數字用二進位原碼表示的話,取值範圍是-111 1111 ~ +111 1111,換成十進位就是-127 ~ 127

2. 反碼

正數的反碼與原碼一致;負數的反碼是對原碼按位取反,只是最高位(符號位)不變。

如果計算機內部採用原碼來表示數,那麼在進行加法和減法運算的時候,需要轉化為兩個絕對值的加法和減法運算;計算機既要實現加法器,又要實現減法器,代價有點大,那麼可不可以只用一種類型的運算器來實現加和減的遠算呢?

很容易想到的就是化減為加,對於計算機來說最好只有加法這樣計算機會更加簡單高效,我們知道在數學中5-3=2,其實可以轉換成5+(-3)=2,這就表示減法可以用加法表示,而乘法是加法的累積,除法是減法的累積,所以在計算機中只要有加法就夠了。

一個數字用原碼表示是容易理解的,但是需要單獨的一個bit來表示符號位。並且在進行加法時,計算機需要先識別某個二進位原碼是正數還是負數,識別出來之後再進行相應的運算。這樣效率不高,能不能讓計算機在進行運算時不用去管符號位,也就是說讓符號位也參與運算,這就要用到反碼。

正數的反碼和原碼一樣,負數的反碼就是在原碼的基礎上符號位保持不變,其他位取反。

那麼我們來看一下,用反碼直接運算會是什麼情況,我們以5-3舉例。

5 - 3等於5 + (-3)

5-3= 5+(-3)= 0000 0101(反碼) + 1111 1100(反碼)= 0000 0001(反碼)= 0000 0001(原碼)= 1

這不對呀?!! 5-3=1?,為什麼差了1?

我們來看一個特殊的運算:

1-1= 1+(-1)= 0000 0001(反碼) + 1111 1110(反碼)= 1111 1111(反碼)= 1000 0000(原碼)= -0

我們在來看一個特殊的運算:

0+0= 0000 0000(反碼) + 0000 0000(反碼)= 0000 0000(反碼)= 0000 0000(原碼)= 0

我們可以看到1000 0000表示-0,0000 0000表示0,雖然-0和0是一樣的,但是在用原碼和反碼表示時是不同的,我們可以理解為在用一個字節表示數字取值範圍時,這些數字中多了一個-0,所以導致我們在用反碼直接運算時符號位可以直接參加運算,但是結果會不對。

3. 補碼

為了解決反碼的問題就出現了補碼。

正數的補碼與原碼一致;

負數的補碼是該數的反碼加1。

正數的補碼和原碼、反碼一樣,負數的補碼就是反碼+1。

5-3= 5+(-3)= 0000 0101(補碼) + 1111 1101(補碼)= 0000 0010(補碼)= 0000 0010(原碼)= 2

5-3=2!!正確。

再來看特殊的:

1-1= 1+(-1)= 0000 0001(補碼) + 1111 1111(補碼)= 0000 0000(補碼)= 0000 0000(原碼)= 0

1-1=0!!正確

再來看一個特殊的運算:

0+0= 0000 0000(補碼) + 0000 0000(補碼)= 0000 0000(補碼)= 0000 0000(原碼)= 0

0+0=0!!也正確。

所以,我們可以看到補碼解決了反碼的問題。

所以對於數字,我們可以使用補碼的形式來進行二進位表示。

總結一下就是:

正數的原碼、反碼、補碼是一致的;負數的補碼是反碼加1,反碼是對原碼按位取反,只是最高位(符號位)不變;計算機數字運算均是基於補碼的。補碼有啥好?

如果計算機內部採用原碼來表示數,那麼在進行加法和減法運算的時候,需要轉化為兩個絕對值的加法和減法運算;

計算機既要實現加法器,又要實現減法器,代價有點大,那麼可不可以只用一種類型的運算器來實現加和減的遠算呢?

很容易想到的就是化減為加,舉一個生活中的例子來說明這個問題:

時鐘一圈是360度,當然也存在365度,但其實它和5度是一樣的;

相同的道理,-30度表示逆時針旋轉30度,其與順時針旋轉330度是一樣的;

這裡數字360表示時鐘的一圈,在計算機裡類似的概念叫模,它可以實現化減為加,本質上是將溢出的部分捨去而不改變結果。

易得,單字節(8位)運算的模為256=2^8。

在沒有符號位的情況下,127+2=129,即:

這時,我們將最高位作為符號位,計算機數字均以補碼來表示,則1000 0001的原碼為減1後按位取反得1111 1111,也就是-127。

也就是說,計算機裡的129即表示-127,相當於模256為一圈,順時針的129則和逆時針127即-127是一樣的。

故可以得到以下結論:

負數的補碼為模減去該數的絕對值。

如-5的補碼為:

-5=256-5=251=1111 1011(二進位)

同樣的,臨界值-128也可以表示出來:

-128=256-128=128=1000 0000(二進位)

但是正128就會溢出了,故單字節(8位)表示的數字範圍為-128--127。

最後,我們來看一下,補碼是如何通過模的溢出捨棄操作來完成化減為加的!

16-5=16+(-5)=11

1 0000 1011將溢出位捨去,得0000 1011(二進位)=11。

4. 負數位移運算

我們再來看

-3 << 1

-3 >> 1

-3用原碼表示為

10000000 00000000 00000000 00000011

-3用反碼表示為

11111111 11111111 11111111 11111100

-3用補碼表示為

11111111 11111111 11111111 11111101

-3 << 1

,表示-3的補碼左移一位後為

11111111 11111111 11111111 11111010

,該補碼對應的反碼為

11111111 11111111 11111111 11111010-1= 11111111 11111111 11111111 11111001

該反碼對應的原碼為:符號位不變,其他位取反,為

10000000 00000000 00000000 00000110

,表示-6。

所以

-3 << 1 = -6

同理

-3 >> 1

是一樣的計算方法,這裡就不演示了。

5. 無符號右移

上面在進行左移和右移時,我有一點沒講到,就是在對補碼進行移動時,符號位是固定不動的,而無符號右移是指在進行移動時,符號位也會跟著一起移動。

比如

-2 >>> 1

-2用原碼表示為

10000000 00000000 00000000 00000010

-2用反碼表示為

11111111 11111111 11111111 11111101

-2用補碼表示為

11111111 11111111 11111111 11111110

-2的補碼右移1位為:

01111111 11111111 11111111 11111111

右移後的補碼對應的反碼、原碼為:

01111111 11111111 11111111 11111111

(因為現在的符號位為0,表示正數,正數的原、反、補碼都相同)

所以,對應的十進位為2147483647。

也就是

-2 >>> 1 = 2147483647

6. 總結

文章寫的可能比較亂,希望大家能看懂,能有所收穫。這裡總結一下,我們可以發現:

3 << 1 = 6 = 3*2

3 << 2 = 12 = 3*2*2

3 << n = 3*2n

m << n = m * 2n

右移則相反,所以大家以後在源碼中再看到位運算時,可以參考上面的公式。

本文轉載自【微信公眾號: 五角錢的程式設計師 ,ID:xianglin965】

相關焦點

  • 二進位原碼/反碼/補碼詳解,不懂的請看過來
    其實並不是,這只是8和-8的原碼,而機器算計中的機器值是使用補碼存儲和計算的。計算機中,正數的原碼、反碼和補碼是一樣的,所以上面那個例子中,真值為8的8位整數的機器值確實是00001000,但是-8就不是這麼回事了。負數的首先將原碼數值位按位取反得到反碼,然後再將反碼數值位加1之後則得到補碼。
  • 計算機的原碼、反碼和補碼幹啥的?
    很多人都只知道計算機使用的是二進位,但很少有了解到計算機是以補碼的方式進行存儲數據的。不過補碼是通過原碼、反碼一步步演變而來的。原碼原碼是一種計算機對數字的二進位的定點表示方法。通常第一位符號位數為0是正,1是負。