一文搞懂補碼
前言
在學習計算機組成原理時經常接觸到補碼,我們知道計算機使用補碼來表示負數,並且負數的補碼按位取反再加一是他對應的正數。但是我們往往知道的也就僅限於此,其原理和原因似乎不被重視。像這樣對一些概念浮於表面的後果就是,當涉及到這個概念更深層次的問題時,我們原有的理解就會變得促膝見肘,一些現象無法從已有的知識解釋。而當我們再次深層次的去挖掘原理時,就會發現,其背後往往是數學在施展魔法。
如何用加法代替減法
首先,為什麼計算機不直接計算減法?如果計算機在底層可以直接進行兩個數的減法計算,那麼就不需要補碼什麼事了。原因也很簡單,純粹的二進位制減法實現起來相當複雜。而使用補碼可以將減法通通轉化成加法計算,CPU 對於執行加法運算非常快速且簡單,而且如此一來計算機就可以透明的計算加減法。
錶盤模型
那麼,怎麼實現加法代替減法呢?我們可以參考時鐘的錶盤。
假如我們有 0~9 共 10 個數,他們被均勻刻在一個錶盤上:
我們可以在上面取到 0 ~ 9 共 10 個刻度(每個刻度表示某個範圍內的每個整數,比如0~255,這裡簡便起見只考慮 0~9),錶盤上的指標可以從一個刻度撥動到下一個刻度。從任意一個刻度朝固定方向連續移動 D = 10 次,指標將再次指向出發刻度。現在我們討論如何在上面表示負數。假設在圓盤上從 0 刻度順時針移動一次,有 a = 1 (a 指向刻度 1)如下圖:
現在我們想知道,如何表示 \(-a\) (即 -1)?因為這裡的指標移動是有方向的(順時針、逆時針),所以你肯定會說,從 0 反方向移動一個刻度,即為 \(-a\) 。沒錯,不過我們暫時不這樣考慮。我們先來想這樣一件事,若有一個值屬於 0 ~9 能代表 \(-a\) ,那麼它與 a 的加和是為 0 的(注:加和為 0 在這裡的體現是 a 再次移動某次後,指向 0 刻度),現在我們觀察這個錶盤,a 已經移動了一個刻度,如果想讓它移動到 0 刻度,有兩種方法,一是之前說過的反向移動一個刻度。另一種就是繼續向前移動 9 個刻度,回到 0 刻度。兩種方法都可以回到 0 ,不過我們不考慮第一種,因為它不方便表示。
示意圖:
現在考慮如何表達第二種移動方法,很顯然,我們可以將它表述為:\(a + (錶盤總共可移動次數 - a) = 0\)(刻度) ;即 \(a + (D-a) = 0(刻度)\) ;可以看到,我特意強調了這裡的 0 是刻度,如果你將 \(D = 10\) 帶入上式,你會得到一個結果 10,但這卻和上式矛盾。原因是上式計算的是最後指標指向的刻度,而不是移動的次數。如果將上式做一下化簡,就會得到式:\(D = 0(刻度)\) 。可能看起來有些奇怪,再對其變型:\(0 + D = 0(刻度)\) ;該式含義為從 0 刻度移動 D 次(10次),將再次回到 0 刻度。雖然這種說法很合理,但是計算機可沒有給我們提供關於刻度的計算,\(0 + D\) 從數值上就是等於 D,即為 10。所以為了讓數值計算上滿足我們的刻度計算的結果,我們可以使用 取模 運算來模擬這種在錶盤上的刻度移動運算:\[[a + (D - a)] \% D = (D) \% D = 0\] ;
注意到這裡的 \(D-a\) 數值上等於 10 -1 = 9,帶入上式後:\(a + (-a) = [a + (D-a)]\%D = (a + 9)\%D = (10)\%10 = 0\) ; 即規定了這種運算後,我們就可以用 9 來代表\(-1\),用 \((D-a)\%D\) 來代表 \(-a\)。然後我們也可以驚喜的發現,由於我們使用一個正數表示某個數的負數,在我們規定的運算規則下,兩個數的減法運算似乎被我們變成了兩個數的加法運算OVO!。
注意,一旦我們使用 9 來代表 \(-1\) ,那麼 9 便只能作為 \(-1\) 參與運算,我們無法再取得 9 ,因為他已經有其他用途了。所以,原取值範圍將縮一半,0~9 的範圍可能就變成了 0~4 的取值範圍,剩下的數 5~9 用來表示 0~4 對應的負數 。 接下來計算在 0~9 範圍內的每個數的負數表示:
對於這個錶盤來說,當 a 取 0 時,0 無正負(補碼 0 無符號),所以 0 仍代表 0;
當 a 取 1 時,\(-a = (D-a)\%D = (10-1)\%10 = 9;即 -1 = 9\) ;
當 a 取 2 時,\(-a = (D-a)\%D = (10-2)\%10 = 8;即 -2 = 8\) ;
當 a 取 3 時,\(-a = (D-a)\%D = (10-3)\%10 = 7;即 -3 = 7\) ;
當 a 取 4 時,\(-a = (D-a)\%D = (10-4)\%10 = 6;即 -4 = 6\) ;
當 a 取 5 時 ,\(-a = (D-a)\%D = (10-5)\%10 = 5;即 -5 = 5\) ; (5 使用 5 本身來表示自己的負數?先跳過)
a 無法再取更大的值,因為 6 已經被作為 \(-4\) 看待,我們的正數範圍只能取到 5 。分配完正負角色後的錶盤如下圖所示(黑色 0 非正非負,紅色為正數,藍色為負數,綠色可正可負):
初始時我們可以在錶盤上取到的值是 0~9,當使用一部分的值作為負數使用後,原來的值的絕對值範圍將縮小,這裡在分配完正負後,其範圍為 \([0,5] \cup [-1,-4]\) ;\([0,4] \cup [-1,-5]\)。為表述方便,我們之後稱這些用來表示另一個正數的負數的數 為對應正數的 補數。
現在我們來簡單驗證一下,在規定模運算下,減法運算到加法運算轉換的正確性:
\(1-1=(1+9)\%10 = 0\);
\(2-2 = (2+8)\%10 = 0\);
\(3-3=(3+7)\%10 = 0\);
\(...\)
\(3-2=(3+8)\%10=1\);
\(4-1=(4+9)\%10=3\);
\(...\)
工作的很好:)。
二進位制的情形
現在來考慮計算機中二進位制數的情形,並考慮在此情形下如何將減法等效為加法。
現設有 8 位二進位制數,它的取值個數有 \(2^8\) 個,所以取值範圍為:即 \(0 ~ 255\) 。同我們之前討論的一樣,如果我們將這 256 個數均勻分佈到一個錶盤上,那麼同樣可以得到一個滿是刻度的錶盤:
這裡 D 取 \(2^8=256\),根據之前的討論,我們可以很容易得出在這個範圍上,可以使用 128 ~ 255 這 128 個數表示 0 ~ 127 對應的 128 個負數,並通過取模運算從而實現減法變為加法運算。你可能注意到,之前在我們的 0~9 錶盤上,5 的位置是綠色的,我也提過,5 是可正可負的,因為它的正負並沒有嚴格要求,因為我們 透明 的處理加法和減法,無論是加 5,還是減 5,作為加法取模運算後,結果都是正確的。\(5-5\) 和 \(5+5\) 最後都會變成 \((5+5)\%D\) 來計算。而現在我們的例子,128 被認為是代表一個負數(即一個正數的補數),至於為什麼,稍後會解釋。
接下來讓我們考慮一下當這些數為二進位制的形式時的情況。
通過之前的結論,我們可以如此舉例: 45 的負數為 \(D-45 = 256 - 45 = 211\) ,將他們分別用二進位制形式表示:
十進位制 | 二進位制 |
---|---|
45 | 0010 1101 |
211 | 1101 0011 |
因為 \(45 - 45 = 0 = (45+211)\%D\) ;使用二進位制形式表示(下面過程沒有使用模運算):
0010 1101 - 0010 1101 = 0000 0000
= 0010 1101 + 1101 0011 =
0010 1101
+ 1101 0011
-------------
10000 0000
注意這個結果,它有 9 位,顯然已經超出 8 為二進位制數,所以得到溢位後的結果:0000 0000。沒錯,無需任何附加運算,只需要簡單的將一個數與它的補數相加,CPU 將得到兩個數的差值。這就是計算機中,實現負數的方式。而一旦有了負數的正數表示形式,那麼減法自然可以轉化為加法進行計算。
二進位制補數計算
那麼緊接著的另一個問題也就隨之而來,怎麼知道每個數的補數呢?我們之前是通過這種方式計算的:
\(-a = (D-a)\%D\) ;對於給定範圍內的每個數都帶入這個公式計算一次嗎?從演算法上來講,是的。但是從實現上來講,計算機有更高效的方法來實現這個操作。找到一個二進位制數的補數,等價於找到一個數與原數加和後,最高位發生溢位,其餘位為 0。那我們自然想到,一個二進位制數,如果全為 1 的話,再將其加 1,那麼最高位就自然溢位,並清零了所有的位。那麼怎麼獲得全 1 的二進位制數呢?考慮二進位制數:
0010 1101;想要該數與一個數加和為全 1 ,那麼該數必然將原數所有的 0 都變為 1,而原來的1 保持不變,所以我們得到一個二進位制數:1101 0010;仔細一看,沒錯,原數的反碼。此時將兩數加和得到的全 1 二進位制數加 1,即得到 8 位全 0 的結果。上述過程就是計算機求解一個二進位制數__補碼__ 的過程,而計算機使用補碼來表示一個二進位制正數的負數形式。
表示範圍
接下來我們討論 0~255 這個範圍中,哪些作為正數,哪些作為補數存在。
毫無疑問,正數是要從 1 開始列舉的(0是正數),每次列舉一個數,我們相應的可以計算出他的一個補數,直到我們遇到了正負數形式相同的 128。(對應之前 0~9 範圍的 5)
128 應該劃分為正數還是負數(正數的補數)?之前我們直接給出了結論,現在來說明為什麼。
如果你將 0 ~ 255 這 256 個數使用二進位制形式全部列舉出來,你將得到兩堆數,第一堆是最高位 0 開始的:0000 0000 ~ 0111 1111,表示範圍 0~127,而另一堆是最高位以 1 開始:1000 0000 ~ 1111 1111,對應範圍 128 ~ 255;計算機似乎發現了一個絕妙的方法用來辨識一個二進位制數的正負,那就是檢視最高位,如果一個(有符號)二進位制數最高位為 1 ,那麼就認為它代表一個負數,如果最高位為 0 ,那麼它就為一個正數。這種規則簡單且高效。唯一的問題就是,一旦應用這種規則,那麼我們之前疑惑的 128 (1000 0000),就不得不成為一個補數了。不過這樣也很優雅,將表數範圍正好分為兩個相等的部分,
0 和 正數共 128 個數,範圍 \(0~127\) (0000 0000 ~ 0111 1111);
補數(表示負數)128 個,範圍 \(-128~-1\) (1000 0000 ~ 1111 1111);
將他們分佈到一個錶盤上:
從 0000 0000 (表示 0) 順時針旋轉,正數取值不斷變大,直到與 1000 0000 右側緊鄰的 0111 1111 (表示正數 127)。
從 1000 0000 (表示 -128)順時針旋轉,負數取值不斷變大,直到 0000 0000 左側緊鄰的 1111 1111(表示負數 -1)。
後記
寫這篇文章之前,我以為我真的懂了補碼的概念,可當我真正想努力給別人闡述清楚我是怎樣想的和事實應該是怎樣的時候,我發現,我知道的與我以為我知道的,相差很多。我不能簡潔明瞭的表達我所想的,可能是文筆不行,也有可能是我只是多知道了一點,卻以為是一個飛躍,然後試圖將這個飛躍真實的描繪出來。而實際呢,恐怕是一個殘缺、粗糙的模型,在腦海中豐滿無比,一旦將其一絲不苟的描述出來,就會發現竟是漏洞百出。可能我們總是喜歡樂觀的高估自己吧(能力錯覺)。當然完成這篇文章後,我也收穫了比預期更多的東西。
去寫作也許就是基於這樣的目的,能真切的讓人認清自己,與自己對話,沉澱自己,變得凝實,避免虛浮。
修修改改寫了好幾天,總覺得邏輯似乎有些問題。如果有什麼問題,歡迎指正。
作者:Skipper
出處:http://www.cnblogs.com/backwords/p/9826773.html
本部落格中未標明轉載的文章歸作者 Skipper 和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。