1、數值的符號
之前所提到的二進制數,沒有考慮到符號問題,所指的都是無符號數。但實際上數字是有正、負符號的。
以數字6為例,按照習慣的數學表示方法,正數6用+6表示,二進制為+110;負數6用-6表示,二進制數為-110.但在數字系統中,符號“+”、“-”也要數字化,一般將所對應的二進制數最高位增加多一位用來設為符號位,用“0”表示“+”、用“1”表示“-”。
為了區分一個符號數的“+”、“-”符號數字化前后的兩種表示方法,引入真值和機器數兩個術語。
真值:在一個二進制數前面用“+”、“-”表示正、負數的這種二進制數叫做真值。
機器數:將“+”、“-”符號用二進制碼“0”、“1”表示的二進制數叫做機器數。數據最后存到計算機中就是用機器數來表示的
如下:
+6 -> +110 -> 0110
-6 -> -110 -> 1110
(十進制數) (真值) (機器數)
在計算機中最小基本的計算單位是字節,1字節=8位二進制數,由此可見最后存放到計算機中的機器數是8位二進制數,不夠補0,符號位占據了1一個位置,所以到了最后只有7位數可以使用。
在c語言中使用 unsigned 關鍵字可以定義一個無符號的變量,可將變量的存儲范圍變大。
機器數是由符號位+二進制數組成的,機器數實際上是個大概念,意指這種類型的數據能存進去計算機,機器數在計算機中又有三種不同的表示方法,分別是:原碼、補碼、反碼。下面逐個列舉
2、原碼
將二進制數的真值中的正符號用0表示,負數符號用1表示,叫做數原碼形式,簡稱原碼。
例如:十進制為9的數,它的真值形式和原碼形式如下所示:
+9 -> +0001001 -> 0 0001001
-9 -> - 0001001 -> 1 0001001
(十進制數) (真值) (原碼)
原碼用8位數碼表示,最高位為符號位。
原碼的優點是易於辨認,因為它的數值部分就是該數的絕對值,而且與真值和十進制數的轉換十分方便。但是在采用原碼進行計算時,運算比較是復雜的。
在計算機中加法和減法是最基本的運算,計算機時時刻刻都離不開它們,所以它們由硬件直接支持。為了提高加減法的運算效率,硬件電路要設計得盡量簡單。
在使用原碼進行加減運算時,會出現這樣的情況,當兩個數相加時,如果符號是相同的,則兩個數值使用加法器直接相加;如果符號位不同時,則要進行減法運算,而且要用減法器來進行減法運算,
對於有符號數,內存要區分符號位和數值位,對於人腦來說,很容易辨別,但是對於計算機來說,就要設計專門的電路,這無疑增加了硬件的復雜性,增加了計算的時間。要是能把符號位和數值位等同起來,讓它們一起參與運算,不再加以區分,只用加法器進行運算,這樣硬件電路就變得簡單了。
為了減少硬件設備量,就引進了反碼和補碼的數值表示法。
3、反碼
加法和減法也可以合並為一種運算,就是加法運算,因為減去一個數相當於加上這個數的相反數,例如,5 - 3 等價於 5 + (-3),10 - (-9) 等價於 10 + 9。
相反數是指數值相同,符號不同的兩個數,例如,10 和 -10 就是一對相反數,-98 和 98 也是一對相反數。
如果能夠實現上面的兩個目標,那么只要設計一種簡單的、不用區分符號位和數值位的加法電路,就能同時實現加法和減法運算,並且非常高效。實際上,這兩個目標都已經實現了,真正的計算機硬件電路就是如此簡單。
然而,簡化硬件電路是有代價的,這個代價就是有符號數在存儲和讀取時都要進行轉化。
反碼是在數碼左邊加上一個符號位,0代表正數,1代表負數。
對於正數,它的反碼就是其原碼(原碼和反碼相同);負數的反碼是將原碼中除符號位以外的所有位(數值位)取反,也就是 0 變成 1,1 變成 0。
+9 -> +0001001 -> 0 0001001 -> 0 0001001
-9 -> - 0001001 -> 1 0001001 -> 1 1110110
(十進制數) (真值) (原碼) (反碼)
由此可以看出:正數的反碼與原碼相同,負數的反碼為其數碼位按位取反。
4、補碼
假設 6 和 18 都是 short 類型的,現在我們要計算 6 - 18 的結果,根據運算規則,它等價於 6 + (-18)。
如果采用原碼計算,那么運算過程為:
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]原 + [1000 0000 0001 0010]原
= [1000 0000 0001 1000]原
= -24
直接用原碼表示整數,讓符號位也參與運算,對於類似上面的減法來說,結果顯然是不正確的。
於是人們開始繼續探索,不斷試錯,后來設計出了反碼。下面就演示了反碼運算的過程:
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]反 + [1111 1111 1110 1101]反
= [1111 1111 1111 0011]反
= [1000 0000 0000 1100]原
= -12
這樣一來,計算結果就正確了。
然而,這樣還不算萬事大吉,我們不妨將減數和被減數交換一下位置,也就是計算 18 - 6 的結果:
18 - 6 = 18 + (-6)
= [0000 0000 0001 0010]反 + [1111 1111 1111 1001]反
= [1 0000 0000 0000 1011]反
= [0000 0000 0000 1011]反
= [0000 0000 0000 1011]原
= 11
按照反碼計算的結果是 11,而真實的結果應該是 12 才對,它們相差了 1。
加粗的 1 是加法運算過程中的進位,它溢出了,內存容納不了了,所以直接截掉。
6 - 18 的結果正確,18 - 6 的結果就不正確,相差 1。按照反碼來計算,是不是小數減去大數正確,大數減去小數就不對了,始終相差 1 呢?我們不妨再看兩個例子,分別是 5 - 13 和 13 - 5。
5 - 13 的運算過程為:
5 - 13 = 5 + (-13)
= [0000 0000 0000 0101]原 + [1000 0000 0000 1101]原
= [0000 0000 0000 0101]反 + [1111 1111 1111 0010]反
= [1111 1111 1111 0111]反
= [1000 0000 0000 1000]原
= -8
13 - 5 的運算過程為:
13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]原 + [1000 0000 0000 0101]原
= [0000 0000 0000 1101]反 + [1111 1111 1111 1010]反
= [1 0000 0000 0000 0111]反
= [0000 0000 0000 0111]反
= [0000 0000 0000 0111]原
= 7
這足以證明,剛才的猜想是正確的:小數減去大數不會有問題,而大數減去小數的就不對了,結果始終相差 1。
相差的這個 1 要進行糾正,但是又不能影響小數減去大數,怎么辦呢?於是人們又絞盡腦汁設計出了補碼,給反碼打了一個“補丁”,終於把相差的 1 給糾正過來了。
對於正數,它的補碼就是其原碼(原碼、反碼、補碼都相同);負數的補碼是其反碼加 1。
可以認為,補碼是在反碼的基礎上打了一個補丁,進行了一下修正,所以叫“補碼”。
原碼、反碼、補碼的概念只對負數有實際意義,對於正數,它們都一樣。
+9 -> +0001001 -> 0 0001001 -> 0 0001001 -> 0 0001001
-9 -> - 0001001 -> 1 0001001 -> 1 1110110 -> 1 1110111
(十進制數) (真值) (原碼) (反碼) (補碼)
在計算機內存中,整數一律采用補碼的形式來存儲。這意味着,當讀取整數時還要采用逆向的轉換,也就是將補碼轉換為原碼。
將補碼轉換為原碼也很簡單:先減去 1,再將數值位取反即可。