文章目錄
1. 進制
1.1 概述
進位制是一種記數方式,亦稱進位計數法或位值計數法,以有限種數字符號來表示無限的數值。使用的數字符號的數目稱爲這種進位制的基數或底數。
常見的有:二進制,八進制,十進制,十六進制。
- 二進制:以0和1來組成的數。
- 八進制:以0~7來組成的數。
- 十進制:以0~9來組成的數。十進制是給我們看的,因爲容易理解。
- 十六進制:以0~9和A、B、C、D、E、F來組成的數。像內存地址,網卡的MAC地址。
1.2 進制的轉換
1.2.1 任意進制轉十進制
先來點基礎,十進制的構造可以這樣,比如1024,可以使用按權展開法構造:權值爲10,指數從0開始
雖然看起來沒什麼軟用,但是對於二進制,比如01101,使用按權展開法可以把它轉成十進制:權值爲2,指數從0開始
其實如果二進制數中有0的在展開時可以省略。類似的,其實八進制,十六進制都可以使用按權展開法轉換成十進制,因爲它們的權值:2、8、16及轉換方法本身就是轉10進制的方法。
1.2.2 十進制轉二進制
- 方法一:
只需要將以十進制表示的數不斷除以2,直到商爲0,得出的餘數逆着寫就可以得到,稱爲重複相除法。如下圖:
假設十進制爲:101,則:
類似的,十進制轉八進制只要一直除以8,十進制轉十六進制只要一直除以16。
- 方法二:(熟練後可以很快求出比較小的數)
這種方法每次十進制數減去一個與十進制相近的以2爲底的指數,能減的記爲1,不能減的記爲0,並記錄相減的結果,然後再把剛剛記錄的結果重複以上操作,直到整減。
看圖吧:
需要記一下:2^0 到 2^10 的值,不難記。當然如果十進制的數很大,不是很j推薦這種方法,因爲自己要直到以2爲底的指數的值,一般我只記到2^10。也可以使用這種方式來判斷十進制轉二進制大概有多少位二進制,因爲第一次的相減其實就是再求該二進制的最高位,把指數+1就可以得到。
1.2.3 小數點的十進制轉換二進制
使用的是重複相乘法,將小數部分乘以2(分數也可以),然後取整數部分,剩下的小數部分繼續乘以2,繼續取整數部分,剩一直取到小數部分爲零爲止(或者積爲1爲止)。如果永遠不能爲零,就同十進制數的四捨五入一樣,按照要求保留多少位小數時,就根據後面一位是0還是1,取捨,如果是零,舍掉,如果是1,向入一位。換句話說就是0舍1入。如下圖:
注意:小數點的二進制中的指數是從-1開始的,一直減小。整數的二進制中的指數是從0開始的,一直增大。
1.2.4 二進制轉其他進制
- 二進制轉八進制:只要把二進制中從右到左每3位,按權展開並相加,不足3位的補0,最後把這些結果組合起來即可。
- 二進制轉十六進制:只要把二進制中從右到左每4位,按權展開並相加(如果結果超過9,那麼用A、B、C、E、F表達),不足4位的補0,最後把這些結果組合起來即可。
2. 有符號數與無符號數
針對二進制來說可以分爲兩種:
- 有符號數:有正負值。使用最高二進制位來表示符號,這一位被稱爲符號位(Sign bit)。人們規定,符號位爲 0 表示正數,符號位爲 1 表示負數。範圍:-2^(n-1) ~ 2^(n-1)-1 (其中n爲二進制數的位數)
- 無符號數:無負值。範圍:0 ~ 2^(n-1)-1 (其中n爲二進制數的位數)
假設二進制以8位爲例,有符號數得把最高位作爲符號位,那麼其餘7位來表示二進制數,而無符號數8位都可以表示一個二進制數,所以有符號數可表示的最大值比無符號數可表示的最大值小:
-
有符號數可表示的最大值:01111111 -> 127(最左邊,也就是最高位爲符號位,所以可表示的範圍只有後7位)
-
無符號數可表示的最大值:11111111 -> 255(沒有符號位的概念)
對於有符號數可表示的最小值,比較特殊,通過上面的公式,有符號數可表示的最小值爲-128,這裏有個問題?爲什麼是-128而不是-127,不應該是11111111(最高位爲符號位)???等學了補碼和反碼在補充!因爲其實-128是用補碼錶示的。
Java中沒有無符號數,C++有。
有符號數可以分爲正值,原碼、反碼、補碼四種編碼實現。
正值:其實就是求出一個數的絕對值的二進制數,不看符號位。比如-3的正值:0011;4的正值:0100。後面要用該知識。
2.1 原碼
原碼其實就是有符號數:在符號位使用0表示正數、1表示負數。就是爲了能夠表示負數。
但是可以發現,對於十進制數0,按照原碼的表示,可以有00和10,這樣就有爭議。更重要的是,當兩個符號位不同的二進制進行運算時,會出錯,例如:1+(-1)=0,在計算機中是使用二進制運算的,假設二進制數有4位,所以變成:0001+1001=1010,換算成十進制爲-2,這明顯錯了。
當然也有另一種運算方式:判斷兩個操作數絕對值大小,使用絕對值大的數減去絕對值小的數,對於符號值,以絕對值大的爲準,但是這樣好麻煩啊!!而且計算機規定只會加法操作(因爲只有加法器,先不管其他操作怎麼處理)。
自從引入原碼,那以後給出的二進制轉換十進制,最高位究竟是符號位還是數字位?假設題目沒有明說,那就是沒有符號位的,假設要有說符號位,或者轉成原碼,那麼就肯定有符號位。
像平時的二進制運算,沒特殊說明,就直接相加沒問題。
用正數的原碼進行運算肯定沒問題,但用負數的原碼進行運算就有問題了,所以爲了讓原碼運算時不出錯,並且要消除減法操作,提出了反碼。
2.2 反碼
反碼在原碼的基礎上提出,定義:(別管怎麼來)
根據公式,對於正數的原碼跟反碼一樣,而對於負數,來看看下面的例子,假設有4位二進制數:
[-2]_原 = 1010
[-2]_反 = (2^(4+1)-1)+(-2) = (100000-0001)-(0010) = (011111)-(0010) = (1101)
本來按照上面的計算,-2的反碼應該是11101,但是現在我們只有4位二進制數,所以自然捨棄最高位。然後剩下的4位中最高位還是表示符號位。
按照上面的例子(下面:2.3 補碼 中得出的結論我先拿上來用),我就先直說了原碼和反碼的關係:對於正數,它的反碼和原碼是一樣的;對於負數,它的反碼等於原碼除符號位外其餘位取反(即0變1,1變0)。可以多用幾個數試試。
現在利用反碼來計算:1+(-1)=0,假設有4位二進制數:
[1]_原 = 0001,[-1]_原 = 1001
[1]_反 = 0001,[-1]_反 = 1110
[1]_反+[-1]_反=0001+1110=1111
1111是反碼,逆向思想轉成原碼,得:1000,即用十進制表示:-0。這樣運算就正確了,雖然對於0還是有+0和-0的爭議。
但真的運算就都正確了嗎?再來一例子:5+(-3)=2,假設有4位二進制數:
[5]_原 = 0101,[-3]_原 = 1011
[5]_反 = 0101, [-3]_反 = 1100
[5]_反+[-3]_反=0101+1100=10001
10001有5位,但是目前是使用4位二進制數,所以捨棄最高位,即:0001,轉爲十進制:1。可以看到跟結果還差1。 再來例子:6+(-2)= 4,假設有4位二進制數:
[6]_原 = 0110, [-2]_原 = 1010
[6]_反 = 0110, [-2]_反 = 1101
[6]_反+[-2]_反=0110+1101=10011
還是跟上面一樣,結果捨棄最高位得:0011,轉爲十進制:3。還是跟結果相差1。有兩個例子那麼就可以得到:反碼雖然在運算時沒有使用減法,但是再求反碼時使用了減法,並且還是不能運算正確,而且對於0還是有歧義,究竟是-0還是+0。
因此,提出了補碼。
2.3 補碼
補碼是按照下面的公式定義的:
可以看到,對於正數,它的補碼跟原碼一樣,對於負數,涉及到了減法操作。
此時利用補碼來運算剛剛的例子:1+(-1)=0,假設二進制數有8位,現在使用補碼來表示:
- 1的補碼:00000001。
- -1的補碼(根據公式):2^(8+1) + (-1) = 2^9 - 1 = 100000000 - 00000001 = 11111111 。
- 相加:00000001+11111111=100000000。注意結果有8個0,一共9位,首先我們規定二進制數有8位,就是一個可表示二進制數範圍(實際可表示的只有7位,第8位爲符號位),所以100000000其實進位了,但是目前只能用8位表示,會自動捨去最高位,即00000000,就是我們的最終結果。
通過上面的運算,發現對於0的歧義也解決了,補碼就是用00000000來表示0的。
記住,計算機如果要判斷符號位開銷比較大,所以在補碼中乾脆把符號位一起運算。
想必還是有很多問號???這樣還是有減法操作啊!!!畫張表來看看有沒有規律,一個數的原碼和補碼、反碼的聯繫:
解釋:
- 正數的原碼、補碼、反碼都是一樣的。
- 負數的原碼,把原碼中除符號位的0換成1,1換成0就變成了反碼;而反碼和補碼又有聯繫,就相差1,即負數的補碼等於反碼+1。
- 其實也可以通過原碼心算出補碼,仔細觀察負數的原碼和補碼有沒有什麼聯繫,我就直說了:從原碼的右邊(低位)開始,找到第一個二進制數:1,然後把第一個1之後(左邊,不包括第一個1)的0換成1,1換成0,符號位不變。 計算機不是用該操作。
所以,計算機中二進制的運算是以補碼來進行運算(當然也有說:計算機對正數是以原碼來進行運算,對負數是以補碼來進行運算,其實都可以,但一條句子是不是更方便記住),反碼的提出是爲了讓計算機求出補碼。在計算機中很容易實現,因爲有:非門,所以計算機的計算是:先求出正值,再全部用非門取反,得出反碼,再加1,就得出補碼,比如-3的補碼:
- -3的正值:0011;
- 全部用非門取反:1100,得出反碼;
- 反碼+1:1101,所以-3的補碼爲1101。
我們人工就不必使用減法(公式)去求什麼補碼,反碼,直接套用該規律,而計算機也巧妙地避開減法操作,把減法轉換成加法。
關於0的特殊性:
[+0]原 = 00000000, [-0]原 = 10000000
[+0]補 = 00000000, [-0]補 = 00000000
[+0]反 = 00000000, [-0]反 = 11111111
2.3.1 補碼的溢出
想到要是兩個負數轉成補碼相加的話,符號位產生進位,如果自然捨去,結果卻變成正數,是不是有很多問號啊???
到此,會對上面的進位(自然捨去),進位(自然捨去)是屬於正常的運算範圍,而還有一種就是溢出,溢出的話是屬於不正常的運算範圍。
談談溢出,加法運算時有以下情況:
- 正數+正數
- 正數+負數(或負數+正數)
- 負數+負數
減法有以下情況:有了補碼,用加法來替換
- 正數-正數:對應上面第一種情況,轉換爲:正數+(-正數)
- 正數-負數(或負數-正數):對應上面第二種情況,轉換爲:正數+(-負數)
- 負數-負數:對應上面第一種情況,轉換爲:負數+(-負數)
所以只看加法來判斷溢出:使用雙高位來判斷溢出,即用C1表示符號位是否進位,C2表示數值部分最高位記是否進位,假設有4位二進制數,有以下的情況:
- 當C1爲真,C2爲假:說明溢出了,因爲兩個符號位原本爲1,進位後變成0(進位後變10,而符號位取後一位),說明錯誤了,稱爲負溢出。
- 當C1爲真,C2爲真:比如5+(-3):轉爲補碼:0101+1101=10010,因爲只有4位二進制數,所以捨去最高位,變爲:0010。正確結果。
- 當C1爲假,C2爲假:比如-5+3:轉爲補碼:1011+0011=1110,正確結果。
- 當C1爲假,C2爲真:說明溢出了,因爲兩個符號位爲0,在C2進位後,符號位變成1,說明錯誤了,稱爲正溢出。
還有其他判斷方法,比如變形補碼。也就是雙符號位的形式(把原來單符號位變雙)去運算,當運算結果的符號位出現“01”或者“10”時,則表示產生溢出。 很簡單,可以試試。
總結:
- 正數和負數之間的相加一定不會溢出。
- 正數和正數相加有可能會溢出,溢出的情況是數值部分向符號位進位,即相加後符號位爲1,變成負數,說明溢出,稱爲正溢出。
- 負數和負數相加有可能會溢出,溢出的情況是符號位進位了,即相加後符號位爲1,變成正數,說明溢出,稱爲負溢出。你說:啊,那正常兩個負數相加的符號位都爲1,不都進位嗎?你可以試試,比如我使用-3+(-2):轉爲補碼:1101+1110=11011,捨去最高位,得:1011。所以負數加負數相加,如果不溢出的話符號位肯定還是1,但是需要自然捨去一位。
而計算機是怎麼處理溢出的,參考高級語言的基本數據類型,當兩個數相加後超過了基本數據類型能表示的範圍時發生了什麼?其實還跟下面的原理有點關係,就是使用同餘定理。
2.3.2 補碼的原理(瞭解)
補碼的原理是利用模運算和同餘定理。
模是指一個計量系統的計數範圍。如時鐘的計量範圍是0~11,模 = 12。“模”實質上是計量器產生“溢出”的量,它的值在計量器上表示不出來,計量器上只能表示出模的餘數。
模運算:比如 5 Mod 1 = 1,如果按照高級語言的寫法:5 % 1 = 1。這裏說個技巧:整數要是小於模的,直接輸出整數,要是等於模輸出0,要是大於模,則輸出:整數-模的結果。
同餘定理是模運算中的重要定理(要看證明的,點擊百度百科):**兩個整數除以同一個整數,若得相同餘數,則二整數同餘。**記作 a ≡ b (mod m),讀作 a 與 b 關於模 m 同餘。比如:1 mod 4 = 1,5 mod 4 = 1,所以1與5關於模4同餘。
找個例子並結合計算機來理解,時鐘就是天生的使用同餘定理的物品,時鐘可以顯示112(或者011)的數字,說明模爲12,假設現在指向6點,我們要調到4點,那麼按照公式:6-2=4,即回調到4點,這裏需要用到減法,想想有沒有另一種方法把減法替換成加法。
可能想到了,就是讓指針前調,即:(6+10)Mod 12 = 4。這裏就把減法轉變成加法,即可以把減2當成加10來看待,以下是證明:
- 對於正數的求餘很簡單,按上面模運算我說的技巧,其實你就可以知道對於正數的補碼爲什麼跟原碼一樣。因爲正數求餘後還是正數,比如 1 MOD 4 = 1,2 MOD 4 = 2。
- 對於負數的求餘需要一條公式:x mod y = x - y ⌊⌋(x / y ),意思是:x減去y乘以(x除以y的商然後向下取整,即取下界)。要理怎麼來去百度吧,並且高級語言對負數去模的結果可能不同。
⌊⌋:該符號表示向下取整,即比自己小的最大整數,比如 1.7,那麼向下取整後爲1
⌈⌉:該符號表示向上取整,即比自己大的最小整數,比如 1.7,那麼向上取整後爲2
跟二叉樹中的floor和ceil一樣。
來試試負數求餘的公式:
(-2) MOD 4 = -2 - 4 * (⌊⌋(-2/4))= -2 - 4 * (-1) = -2 + 4 = 2
再來一條:
(-3) MOD 2 = -3 - 2 * (⌊⌋(-3/2))= -3 - 2 * (-2) = -3 + 4 = 1
現在回到時鐘去,剛剛說減2當成加10來看待,那麼來看看
(-2)MOD 12 = -2 - 12 * (⌊⌋(-2/12))= -2 - 12 * (-1)= - 2 + 12 = 10
看到了吧,而:
10 MOD 12 = 10
所以-2和10是同餘的或者稱爲互爲補數,所以在12爲模的系統中,-2可以看成是+10。其實可以看成是-2加上一個12的週期,得出10。那麼把6記爲a,-2記爲b,12記爲mod,則用公式來表達:a - b = a - b + mod = a + (mod - b)。
總結一句話:在有模的系統中,減去一個數可變成加上它的補數。
現在回到計算機來,假設有4位的二進制數,除符號位外,其範圍爲-8~7,一共有16個數,那麼模爲2^3=16,能夠表示的最大二進制爲0111,那麼此時加1,得:1000,而1000其實是-8(對於有符號數來說,不懂看下面的對於-128的解釋),這也就跟時鐘一樣,形成一個環。
所以假設當前的數爲6,想要變成2的話,那麼我們就得減掉4,但是減法計算機不能運算啊,所以可對減4再加上一個16的週期(模),得出12,所以減4和加12其實是一樣的。你可以畫條水平線,然後把-8~7填上,然後在6的位置加12試試。此時按照8位的表示範圍,納悶12怎麼求,其實計算機只是學了思想,並不一定像它那樣操作,可看看下面的運算過程:參考:補碼原理——負數爲什麼要用補碼錶示,補碼的推導
# 按以上理論,減一個數等於加上它的補數,所以
6 - 2
# 等價於 其實 16 - 2 不就是上面補碼定義的公式求負數給出的 2^n + x 嗎?回去看看
6 + (16 - 2) // 算術運算單元將減法轉化爲加法
# 用二進制表示則爲:
0110 + (10000 - 0010)
# 等價於 10000 確實可轉爲 1 + 1111
0110 + ((1 + 1111) - 0010)
# 等價於
0110 + (1 + (1111 - 0010))
# 等價於 其實下面是求出反碼了,計算機是直接到這一步,因爲有非門的存在,根據正值直接全部按位取反。可能有人要說了,要是正數怎麼辦,最高位不是也變了嗎?:不對!正數求反碼不變啊。
0110 + (1 + 1101) // -2的正值(0010)的反碼(1101),然後+1,正是補碼的定義
// 其實不說正值,說出-2是由2(0010)的補碼(0010)全部包括符號位按位取反(1101)後+1得出的。
# 等價於
0110 + 1110
# 所以從這裏可以得到-2的補碼
-2 = [1110]補
# 即 `-2` 在計算機中的二進制表示爲 `1010`,正是“ -2 的正值 2(0010)的補碼(1110)”。這就是補碼的推導
# 最後一步 0110 + 1110 等於
10100 而只有4位,捨去最高位得:0100,轉爲十進制:4,正確!!
對於8位的二進制,-128用補碼1000 0000表示的解釋:其實是取消了原碼和反碼對於0的歧義問題,在補碼中就直接用1000 0000來表示-128。計算機就固定用1000 0000來表示-128,並且是不能轉換的,比如有人要把-128轉爲原碼和補碼,這是錯誤的。這也解釋了爲什麼最小值還會多出一位,因爲多出的一位用來消除0的歧義。
(看看計算機自帶的計算器)
可能你會問那如何表示128,那麼128肯定是要數據能夠表示的範圍啊!!對於8位表示不了128。我廢話了。。
補碼原理花了我好長時間,今天終於搞完了,雖然可不知道原理,但是現在複習階段碰到了就搞搞吧。
2.3.3 小數的補碼
定義:
前面在進制轉換中已經有說了小數的轉換,那麼求小數的原碼也很簡單。
更重要的是,小數的原碼,反碼,補碼怎麼求是跟整數的求法一樣的。
對於小數的符號位放哪?有些書放在小數點之前,比如:1.0101,0.10,也有額外再開個空間存儲符號位,比如:10.0101,00.10。這些都可以。
我直接拿老師的例子:他是額外開空間存儲符號位的
3. 定點數
計算機中處理的數據經常帶有小數點,而小數點在計算機中有兩種表示方式,一種就是定點數,另一種就是浮點數。
定點數:小數點固定在某個位置的數稱爲定點數。按照位置的不同,可分爲兩種:
- 定點小數:小數點默認位於數值部分的左邊,整數位則用於表示符號位,稱爲純小數(相對於二進制來說),但是小數點也是隱含的,即不佔位。比如(都是二進制表示):0.001(十進制數爲:0.125),1.0111(-0.45,假設保留4位小數點)等。
(網上還有老師都是上圖,但是我百度純小數時,比如百度百科中說:整數部分爲零的小數叫做純小數,其實它表示的是十進制,符號位是’+‘或’-’,比如:0.12,-0.2333等。)
百度百科,一般來說,如果最末位 1,前面各位都爲0,則數的絕對值最小,即|x|min= 2^(-n)。 如果各位均爲1,則數的絕對值最大,即|x|max=1-2^(-n)。所以定點小數的表示範圍是:(其中n爲二進制數的位數)
- 定點整數:小數點默認放在 有效數值部分之後,最高位還是符號位,稱爲純整數(相對於二進制來說),但是小數點是隱含的,即不佔位。比如(最高位爲符號位):0001(十進制:1),0010,1011等。而範圍就是上面有符號數那個範圍:-2^(n-1) ~ 2^(n-1)-1 (其中n爲二進制數的位數)
但計算機通常遇到既不是純整數又不是純小數。比如:10.2,3.53等。
對於計算機來說,需要把它統一稱純小數或純整數,怎麼搞??數據按比例因子縮小成純小數或擴大成純整數再參加運算,結果輸出時再按比例折算成實際值。不過有點麻煩,所以提出浮點數。
4. 浮點數
浮點數:是屬於有理數中某特定子集的數的數字表示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這種表示方法類似於基數爲10的科學計數法。
科學計數法:
- 1.2345:稱爲尾數。
- 10:稱爲基數。
- 8:稱爲指數。表示的是小數點的真實位置。
4.1 二進制科學計數表示法
而對於浮點數的表示格式跟科學計數法一樣,前兩個數的稱呼也跟上面一樣。而指數在這裏稱爲階碼。
- 計算機中保存的格式如上的藍條,其中對於基數不用存儲,因爲在計算機中默認就是:2。
- 在浮點數中,尾數的位數越多,有效精度越大。在計算機中尾數用補碼錶示。對於尾數是有要求的,就是尾數必須是純小數,如果尾數最高位爲1的浮點數稱爲規格化數。規格化主要是爲了提高精度。
- 階碼的位數越多,表示的範圍越大。這裏的階碼在計算機中用補碼錶示。
浮點數的例子,都是以二進制表示的:
- 上面的寫法都是合法的,就拿第一個來看。一般運算都是需要規格化浮點數。
- 0.0110101:以二進制表示的尾數,必須轉成純小數。小數點之前是符號位。
- 2:基數,在計算機中默認爲2。
- 3:以十進制數表示,稱指數或階碼。
- 那麼在計算機中就是這樣存儲的(根據上面的藍條):0 11 0 01101010(假設8位尾數,不足8位在後面補0,不是前面補0,因爲是這有小數點的,這裏採用原碼錶示)
- 小數點右移一位階碼-1,小數點左移一位階碼+1。
4.2 IEEE754的表示方法(重要)
對於原先的二進制科學計數法,因爲早期不同機器上的默認的階碼位數和尾數長度可能不一樣,所以對浮點數的表示有點差異。所以就要統一浮點數的表示,提出IEEE754標準。
- 在IEEE754中,少了階碼的符號位,階碼用移碼錶示。移碼是階碼加上一個偏移值,因爲階碼有正負,而IEEE754不保存階碼的符號位,所以使用一個偏移值,來取消階碼的正負之分(其實就是把一個有符號數變成無符號數),因此少了階碼的符號位。
- IEEE754規定偏移值爲:(e爲階碼位數),比如8位階碼的偏移值爲: = 128 - 1 = 127。
- 移碼的好處:採用指數的實際值加上固定的偏移值的辦法表示浮點數的指數,好處是可以用長度爲e個比特的無符號整數來表示所有的指數取值(就不用浪費一位存儲階碼符號位),這使得兩個浮點數的指數大小的比較更爲容易,實際上可以按照字典序比較兩個浮點表示的大小。像以前使用補碼來表示階碼,要比較大小很難比較。
- 尾數必須是純小數,如果尾數最高位爲1的浮點數稱爲規格化數。這麼說當然還有非規格化的數和一些特殊值。
- double(雙精度,64位,符號位1位,階碼11位,尾數52位。),float(單精度,32位,1位符號位,8位階碼位,23位尾數位)使用IEEE754表示的。待講。
IEEE754根據階碼和尾數還可以再分類。
4.3 IEEE754規格化浮點數
所謂規格化,就是要求尾數最高位爲1,這可以通過階碼調整。所以在IEEE754中可以把最高位的1省略掉,因爲規格化浮點數默認最高位肯定是1啊。所以有了以下的公式:
- (1+M):因爲能控制階碼,所以可以對原先的尾數進行操作,把尾數最左邊第一個爲1提到小數點左邊,而小數點右邊就當成是M,比如:0.00101000 ,按照說法,變成:1.01000000 * ,而存入計算中只需要存入:01000000就行。而拿出來時就需要M+1。由此可得規格化浮點數的範圍爲:1<=1+M<2。(1<=M<2,也有這樣的,這裏的M指的是原先沒有去最高位1的尾數)
- 根據上面,對於規格化的浮點數,單精度的尾數爲23位,但實際上爲24位,雙精度也是隱藏了一位。所以從規格化浮點數轉換到十進制時需要注意尾數。
- 上面的尾數的疑問,說要是找不到1怎麼辦,就是全爲0,這是一個特殊值,有特殊處理。
- 規格化後的尾數的最高位:正數的原碼和補碼最高位都是1,負數的原碼最高位爲1,負數的補碼最高位爲0。網上的規格化題目很多都是用原碼錶示的,那麼下面的題就都用原碼錶示,容易理解。而按網上說的現階段計算機中的數據都是按補碼錶示的,那我認爲在計算機中的浮點數尾數是補碼形式。有錯指出。
- 階碼用移碼錶示。移碼=階碼+偏移值。偏移值=(e爲階碼位數)。
- 對於階碼進一步解釋,按照單精度有8位階碼,因爲沒有符號位,那麼階碼可表示的範圍爲0~255(十進制表示)。但0(二進制全爲0)和255(二進制全爲1)是特殊值,那麼階碼可表示的範圍爲1~254。那負數怎麼表示?藉助偏移值,單精度的偏移值爲127,所以階碼實際上表示的範圍爲:-126~127。
拿個例子:假設把-5.125轉成規格化的單精度浮點數。(單精度浮點數爲4個字節,IEEE754規定1位爲尾數符號位,8位爲階碼,23位爲尾數,這裏用原碼錶示)
- -5.125轉成二進制:-101.001,把符號位和尾數分離,此時S = 1。
- 10.111轉成最高位爲1的浮點數:1.0001*,此時M=0001 0000 0000 0000 0000 000。
- 單精度的偏移值: = 128 - 1 = 127。
- 階碼用移碼錶示:E = 2 + 127 = 129。轉成二進制:129 = 10000001。
- 所以按照規格化的格式:尾數符號位 階碼 尾數:1 1000 0001 0001 0000 0000 0000 0000 000。
4.4 IEEE754非規格化浮點數
(我看百度百科寫得很好,就直接拿過來這兩段)
如果浮點數的指數部分是0,尾數部分非規格化,那麼這個浮點數將被稱爲非規格化的浮點數。在這樣情況下,階碼=1-偏移值,尾數就是原先的尾數,即不隱含尾數開頭的1。
一般是某個數字相當接近零時纔會使用非規格化浮點數來表示,其實可以把表示0歸類在非規格化浮點數。(重要)
IEEE 754標準規定:非規約的浮點數的指數偏移值比規格化的浮點數的指數偏移值小1。例如,最小的規格化的單精度浮點數的指數部分編碼值爲1,指數的實際值爲-126;而非規格化的單精度浮點數的指數域編碼值爲0,對應的指數實際值也是-126而不是-127。實際上非規格化的浮點數仍然是有效可以使用的,只是它們的絕對值已經小於所有的規格化浮點數的絕對值;即所有的非規格化浮點數比規約浮點數更接近0。
爲什麼要讓非規格化浮點數的指數偏移值比規格化浮點數的指數偏移值小1?爲了補償非規格化數的尾數沒有隱含開頭1,其實非規格化浮點數經常性的與規格化浮點數互相轉換,所以這是爲了讓非規格化浮點數能夠平滑轉換到規格化浮點數,減少誤差。雖然可能還是有誤差。
來看看《深入瞭解計算機系統》的浮點數中講的:解釋爲什麼階碼要用1-偏移值表示。
4.5 IEEE754特殊值浮點數
-
如果指數是0並且尾數的小數部分是0,這個數±0(和符號位相關)。假設爲單精度:
- 0 00000000 00000000000000000000000:+0
- 1 00000000 00000000000000000000000:-0
- 根據IEEE754,這兩個值是不同的。
-
如果指數=(轉爲二進制就是階碼全爲1)並且尾數的小數部分是0,這個數是±∞(同樣和符號位相關)。假設爲單精度:
- 0 11111111 00000000000000000000000:+∞
- 0 11111111 00000000000000000000000:-∞
-
如果指數= (轉爲二進制就是階碼全爲1)並且尾數的小數部分非0,這個數表示爲不是一個數(NaN)。比如:Math.sqrt(-1)。假設位單精度:
-
0 11111111 00000000000000000000001:NaN
-
…
-
0 11111111 11111111111111111111111:NaN
-
一共有:個表示NaN。
-
雙精度的也可以求,根據尾數位數,所以一共有個表示NaN。
-
一些運算結果不能是實數或者無窮,就會返回NaN,比如:Math.sqrt(-1)。
-
4.6 浮點數的表示範圍
假設階碼位數爲m位,尾數位數爲n位,先按非零浮點數來說,它可以表示上面三類數。跟高級語言聯繫,按照java中的浮點數最小值是非規格化數,最大值是規格化數。
對於正數的階碼:用移碼錶示。
正數階碼最大值(規格化的階碼的最大值,減去偏移值):
正數階碼最小值(非規格化數的階碼的最小值) :
對於正數的尾數:
尾數最大值(規格化數,比如:1.1111… 後都是1,規格化尾數小於2,最大值離2有2^{-n}的距離):
尾數最小值(非規格化數,距離0的距離就是2^{-n}):
根據上面尾數的範圍和階碼的範圍,可得出浮點數的表示範圍:其中有三塊是不能表示的(上溢,下溢),按照Java,對於浮點數,如果產生下溢,一律按0.0處理,如果右上溢則按正無窮處理,如果左上溢則按負無窮處理。
- 關於溢出:假設八位定點數,不考慮階碼,那麼它能夠表示的最小的數是0.00000001,更高的精度就無法表示了,比如:0.00000000000000000000001,這個就是下溢,那麼浮點數同樣存在這個問題。
- 那麼假設浮點數能夠表示的絕對值最小的數是N,那麼N/2則沒辦法表示了,因而爲了表示N/2,就會產生精度丟失,這就是浮點數下溢的情況。
上面浮點數的表示範圍可能有時會不同,不要照搬,看階碼用什麼表示,還有表示的範圍是否有包含特殊值,是否還分規格化和非規格化等等。
4.7 浮點數的舍入
任何有效數上的運算結果,通常都存放在較長的寄存器中,當結果被放回浮點格式時,必須將多出來的比特丟棄。IEEE754定義了浮點數四種舍入格式:
- 舍入到最接近(默認使用這個):如果尾數超過規定的位數,則按0舍1入的方式截取到規定的位數。比如8位尾數,此時尾數超過8位,那麼在第9位判斷是0還是1,如果是0,則截取前8位,如果是1,則截取前8位並對其加1。這就是我們十進制說的四捨五入。
- 朝+∞方向舍入:會將結果朝正無限大的方向舍入。
- 朝-∞方向舍入:會將結果朝負無限大的方向舍入。
- 朝0方向舍入:會將結果朝0的方向舍入。
4.7 浮點數的運算
這裏是有符號位的。如果從計算機取出數據時,
需要5步:對階-》尾數求和-》尾數規格化-》舍入-》溢出判斷
- 對階:小階看齊大階,一般是小數點左移。就是比較小的階碼轉成跟另一個階碼一樣大。這樣就可以運算。
- 尾數求和:利用補碼來運算。而因爲有補碼,所以可以把減法變加法,所以都是求和。
- 尾數規格化:如果不是下面的格式則需要變成下面的格式,因爲這是有符號位的,小數點的位置不能動,所以後面的移動說成尾數整體。一般尾數整體左移。如果雙符號位不一致下需要對符號位和尾數一起右移變成相同的符號位(01.xx->00.1xx,10.xx->11.0xx),如果是右移的話需要有舍入操作。所以會有點誤差。但是只要是移動就需要改變階碼,整體左移一位階碼-1,整體右移一位階碼+1。不理解就用十進制理解。
- 如果使用補碼,對於正數,那麼它的補碼的尾數規格化的形式爲:0.1xxxxxx
- 如果使用補碼,對於負數,那麼它的補碼的尾數規格化的形式爲:1.0xxxxxx
- 如果使用變形補碼(雙符號位),對於正數,尾數規格化的形式爲:00.1xxxxxx
- 如果使用變形補碼(雙符號位),對於負數,尾數規格化的形式爲:11.0xxxxxx
- 舍入:0舍1入。
- 溢出判斷:可以使用多種方式判斷,這裏使用變形補碼。跟定點數不同。但浮點數的尾數雙符號位不一致時不算溢出,因爲可以通過右移符號位使得相同。而真正判斷溢出是用階碼的雙符號位來判斷的,但階碼的雙符號位不一致時,纔算浮點數溢出。
例1:x = 0.1101 * , y = (-0.1010) * ,求x+y。(都是用二進制表示的,假設階碼和尾數都是4位)
- 對階:x的階碼比y的階碼小,所以x向y看齊,尾數整體右移動,得:x = 0.001101 * ,截取前4位:0.0011 *
- 尾數求和:使用雙補碼,爲了判斷溢出:
- x[補] = 00.0011,y[補] = 11.0110
- x[補] + y[補] = 11.1001
- 尾數規格化:發現滿足負數的變形補碼的規格化,則變:
- 11.1001 -》11.0010(尾數整體左移一位,符號位不動,這裏左移直接把尾數最高位舍掉了,後面補0)
- 那麼階碼此時需要改變,尾數整體左移一位,則階碼-1,變成:10
- 溢出判斷:階碼雙符號位是00,沒有溢出。
例2:x = 0.11010011 * , y =0.11101110 * ,求x+y。(都是用二進制表示的,假設階碼是4位,尾數是8位)
- 對階:需要改變y:0.011101110 * ,截取8位:0.01110111 *
- 尾數求和:
- 這裏x和y都是正的,所以補碼跟原碼一樣。
- x[補] + y[補] = 01.01001010
- 規格化:01.01001010 -》00.101001010
- 舍入:最後一位爲0,所以不需要進位,得:00.10100101,階碼+1:1110
- 溢出判斷:階碼雙符號位是00,沒有溢出。
4.8 常見的浮點數類型
都是以IEEE754爲標準的。
有效數字是指在一個數中,從該數的第一個非零數字起,直到末尾數字止的數字稱爲有效數字。
- 單精度浮點數:使用4個字節(32位)來表達的浮點數(float),包括符號位1位,階碼8位,尾數23位。
- 精度主要取決於尾數,十進制的雙精度浮點數最多有8位有效數字。
- 雙精度浮點數:使用8個字節(64位)來表達的浮點數(double),包括符號位1位,階碼11位,尾數52位。
- 十進制的雙精度浮點數最多能表示18位有效數字。
我們以單精度浮點數爲例,跟高級語言聯繫起來,我用Java的float聯繫。Java的float的組成跟上面一樣。而我們知道float中有規格化浮點數、非規格化浮點數和特殊值三種。
public final class Float extends Number implements Comparable<Float> {
/**
* 表示正無窮,但輸出:Infinity
*/
public static final float POSITIVE_INFINITY = 1.0f / 0.0f;
/**
* 表示負無窮,但輸出:-Infinity
*/
public static final float NEGATIVE_INFINITY = -1.0f / 0.0f;
/**
* 表示爲不是一個數
* NaN規定用0x7fc00000來表示
*/
public static final float NaN = 0.0f / 0.0f;
/**
* 正數浮點數的最大規格化數
* (1.11111111111111111111111)×2^127 ≈ (2 - 2^(-23))×2^127 ≈ 3.402823e+38f
*/
public static final float MAX_VALUE = 0x1.fffffeP+127f; // 3.4028235e+38f
/**
* 正數浮點數的最小規格化數
* (1.0)×2^(-126) ≈ 1.17549435E-38f
*/
public static final float MIN_NORMAL = 0x1.0p-126f; // 1.17549435E-38f
/**
* 正數浮點數的最小非規格化數,非規格化浮點數的指數爲0,按照公式:階碼=1-偏移值,得:階碼=1 - *(2^(8-1)-1) = -126,而非規格化浮點數最小值是距離0最近的,那麼尾數爲00...01(22個0,一共23 * 位)= 2^{-23},所以正數的非規格化浮點數最小值 = 2^{-23} * 2^{-126} = 2^{-149} ≈
* 1.4e-45f
*/
public static final float MIN_VALUE = 0x0.000002P-126f; // 1.4e-45f
/**
* 最大實際指數
*/
public static final int MAX_EXPONENT = 127;
/**
* 最小實際指數
*/
public static final int MIN_EXPONENT = -126;
/**
* 位
*/
public static final int SIZE = 32;
/**
* 字節
*/
public static final int BYTES = SIZE / Byte.SIZE;
/*
* 可以接收 float,double,String
*/
public Float(float value) {
this.value = value;
}
public Float(double value) {
this.value = (float)value;
}
public Float(String s) throws NumberFormatException {
valueparseFloat(s);
}
/*
*native關鍵字:方法對應的實習並不在當前文件
* 方法作用:將一個浮點數轉成二進制,然後轉成成在內存中的表示形式(這裏是float,那麼1位符號位,8位階碼,23位尾數),再輸出時轉成十進制輸出。
*
*/
public static native int floatToRawIntBits(float value);
/*
* 功能跟上面方法一樣,但是這裏做了NaN檢查
* 在浮點數比較時(equals)會使用到這個方法,如果浮點數相等那麼轉換的二進制一定相等。
* 我們也可以用它來把一個浮點數轉成二進制數,因爲它是先轉成十進制數,那麼需要藉助Integer.toBinaryString()方法,把十進制轉成二進制,如果輸出的二進制不滿32位則前面補0,那麼對於float,第1位是符號位,8位階碼,23位尾數。
* 如果是double型也一樣,但使用Long.toBinaryString()。
*/
public static int floatToIntBits(float value) {
int result = floatToRawIntBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if ( ((result & FloatConsts.EXP_BIT_MASK) ==
FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0)
result = 0x7fc00000;
return result;
}
/*其他的源碼,比如parseFloat方法的源碼前部分看得懂,但後部分看不懂,以後再補*/
}
最後,學習了浮點數,如果這樣完事了就學了沒用啊,那麼在Java中試試:0.1+0.2的結果是什麼?我的結果是:0.30000000000000004。意不意外!!按照上面學的,我們自己來模仿計算機運算一下:注意,計算機如果沒有在浮點數後加上f,那默認就得double類型的,這應該不用我廢話了。
- 計算機會轉爲二進制,我使用重複相乘法來轉,會發現乘不盡。過程我就不寫了,直接得出結果:
0.1 轉: 0.00011001100110011...(後面無限循環0011)
0.2 轉: 0.0011001100110011...(後面無限循環0011)
發現0.1和0.2轉成二進制是一個無限循環的二進制小數,這也說明double並沒有能夠準確表達0.1和0.2的浮點數,只能找到一個無限逼近0.1和0.2的浮點數。這需要跟對階和0舍1入聯繫。
- 那0.1和0.2是不是規格化浮點數?因爲要截取尾數位,使用語句來判斷:
System.out.println(Double.MIN_NORMAL > 0.1);
output:false
說明0.1和0.2是規格化數,是規格化浮點數,所以尾數有隱藏位
- 規範化表示:(小數點右移一位階碼-1,小數點左移一位階碼+1,這跟上面的浮點數運算說的要區別好,上面是尾數整體,而且有雙符號,小數點的位置不能變。)
0.1:0.00011001100110011...(後面無限循環0011)
二進制科學計數法:(-1)^0 * 1.1001100110011001100110011001100110011001100110011010 * 2^(-4)
解釋:尾數截取了52個,第53個是1,按照0舍1入,需要對尾數加1
0.2:0.0011001100110011...(後面無限循環0011)
二進制科學計數法:(-1)^0 * 1.1001100110011001100110011001100110011001100110011010 * 2^(-3)
解釋:尾數截取了52個,第53個是1,按照0舍1入,需要對尾數加1
- 對階:小階看齊大階,所以需要改變0.1的階碼
0.1100110011001100110011001100110011001100110011001101 * 2^(-3)
後面捨去一位0
- 相加
0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
+ 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
----------------------------------------------------------------------
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
- 對結果規格化:
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111:
(-1)^0 * 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)
解釋:尾數截取了52個,第53個是1,按照0舍1入,需要對尾數加1
- 轉爲十進制:
0.010011001100110011001100110011001100110011001100110100:0.30000000000000004
這裏就不使用雙符號位了,可以知道不會溢出。所以答案就是:以前學過有限小數和無限小數,浮點數在運算時轉成二進制時,也是有這樣的概念,所以某些數並不能精確的表示,而且可能經過了0舍1入和對階,所以某些浮點數只能找到無限接近於它的數。
那0.1f+0.2f怎麼不會?再試試:
- 是否是規格化數?
System.out.println(Float.MIN_NORMAL > 0.1);
output:false
說明0.1和0.2是規格化數,是規格化浮點數,所以尾數有隱藏位
- 二進制科學計數法
0.1:0.00011001100110011...(後面無限循環0011)
二進制科學計數法:(-1)^0 * 1.10011001100110011001101 * 2^(-4)
解釋:尾數截取了23個,第24個是1,按照0舍1入,需要對尾數加1
0.2:0.0011001100110011...(後面無限循環0011)
二進制科學計數法:(-1)^0 * 1.10011001100110011001101 * 2^(-3)
解釋:尾數截取了52個,第53個是1,按照0舍1入,需要對尾數加1
- 對階:修改0.1
(-1)^0 * 0.11001100110011001100111 * 2^(-3)
捨去最後一位,最後一位是1,需要對尾數加1
- 相加:
0.11001100110011001100111
+ 1.10011001100110011001101
-----------------------------
10.01100110011001100110100
- 對結果規格化:
(-1)^0 * 1.00110011001100110011010 * 2^(-2)
- 轉成十進制:
0.0100110011001100110011010:0.30000001192092896
這裏的結果就疑問了?爲什麼結果跟java輸出的結果不同,你可以試試下面這條語句:
System.out.printf("%.30f\n", (0.1f+0.2f));
它輸出的結果:0.300000011920928960000000000000000000000000000000000000000
你看這不是跟我們計算的一樣嗎,只不過對於float來說,它最多能表示8位有效數字,即0.3000000,而3後面的0是不顯示的,所以就變成了輸出0.3
浮點數的運算造成的誤差其中的原理都是一樣的,而且還要看是float和double類型。
自從瞭解了上面,那我自己就突然跳出一個想法,那爲什麼打印0.1結果還是輸出0.1呢?我也很納悶,卡了2天,找了資料,看了源碼,提到跟Double/Float的toString有關,主要是toString調用的FloatingDecimal.toJavaFormatString(f),對於現階段的我難以理解,這個問題十分底層。我給個鏈接:點我跳轉,如果看得懂可以在評論區給我留言,謝謝。
目前結合我自己的理解來解釋爲什麼在java打印0.1結果還是輸出0.1?
- 結合float和double來說,來看看IDEA,我誤打誤撞,讓float和double的0.1格式化,多輸出幾十個小數位:
System.out.printf("float: %.50f\n", (0.1f));
System.out.printf("double: %.53f\n", (0.1));
- 輸出的結果有點意外:
- float後面還有別的數,那麼拿上面我們計算出的兩種類型的0.1的二進制數:
對於float的0.1:0.000110011001100110011001101
對於double的0.1:0.00011001100110011001100110011001100110011001100110011010
- 我直接拿去轉了,如圖:
- 好像有點眉目了,double的精度本來就比float高,所以double表示的數比float更加精確,但不代表double能完全表示0.1(這一點我也還是疑問,不可能就我上面的轉換那樣,應該還涉及到java的設計機制,現階段只是利用上面學的東西);而對於float爲什麼還是輸出0.1,不應該輸出0.10000000149011612嗎?因爲float最多能表示8位有效位,所以輸出0.1000000,而1後面的0是不顯示的,所以輸出0.1。
你有沒有疑問是不是我算錯了?那我們來使用java逆推一下:
// 把0.1在內存中的表示形式(1位符號位,11位階碼(移碼錶示),52位尾數)轉成十進制
System.out.println(Double.doubleToRawLongBits(0.1));
// output:4591870180066957722l
// 把輸出結果轉成二進制:
System.out.println(Long.toBinaryString(4591870180066957722l));
// output:11111110111001100110011001100110011001100110011001100110011010
// 一共才62位,我們需要64位,一定是在前面補0,因爲java輸出的數會去除前導0(比如00002,輸出2),
// 所以:0011111110111001100110011001100110011001100110011001100110011010
// 按照double:1位符號位,11位階碼(移碼錶示),52位尾數
// 得 0 01111111011 1001100110011001100110011001100110011001100110011010
// 階碼轉成十進制:01111111011-》1019,這是移碼,而double的偏移值是1023,所以實際的指數爲-4
// 尾數需要+1,得:1.1001100110011001100110011001100110011001100110011010
// 所以得:0.00011001100110011001100110011001100110011001100110011010
// 這個二進制數不就是我們上面求的二進制數嗎,我拿去轉成十進制結果就是0.1
- 其實對於其他語言的浮點數都有這樣的坑,因爲都是按照IEEE754標準,目前一直存在。
那既然這樣,發展這麼久,那肯定有解決辦法,這些誤差主要產生在運算中,所以在java中使用java.math.BigDecimal來做浮點數的運算。
java.math.BigDecimal中的構造函數可以接收double,float,String類型的參數,也可以接收int、long類型,因爲BigDecimal的實習是利用BigInteger,但是在BigDecimal加入了浮點數的表示。
你試試看0.1:
BigDecimal bigDecimal1 = new BigDecimal(0.1);
BigDecimal bigDecimal2 = new BigDecimal("0.1");
BigDecimal bigDecimal3 = new BigDecimal(0.1f);
System.out.println(bigDecimal1);
System.out.println(bigDecimal2);
System.out.println(bigDecimal3);
結果居然是這樣的:
很疑惑,點開BigDecimal的構造函數看看源碼,看到註釋有這一句:我拿去翻譯了
-
參數類型爲double的構造方法的結果有一定的不可預知性。有人可能認爲在Java中寫入newBigDecimal(0.1)所創建的BigDecimal正好等於 0.1(非標度值 1,其標度爲 1),但是它實際上等於0.1000000000000000055511151231257827021181583404541015625。這是因爲0.1無法準確地表示爲 double(或者說對於該情況,不能表示爲任何有限長度的二進制小數)。這樣,傳入到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。所以最好使用String傳入。
-
BigDecimal中帶有加減乘除,得出的結果需要存儲或者覆蓋給某個值。
-
BigDecimal中帶有方法可以把BigDecimal轉成double:doubleValue()。
拿上面的0.1+0.2來試試:
BigDecimal bigDecimal1 = new BigDecimal("0.1");
BigDecimal bigDecimal2 = new BigDecimal("0.2");
System.out.println(bigDecimal1.add(bigDecimal2));
System.out.println(bigDecimal1.add(bigDecimal2).doubleValue()); // 可以轉成double,結果都是一樣的,但是使用doubleValue()可以把結果賦給double類型的變量
對於BigDecimal的其他方法就不一一說了。
4.6 浮點數與定點數對比
- 當定點數與浮點數位數相同時,浮點數表示的範圍更大。
- 當浮點數尾數爲規格化數時,浮點數的精度更高。
- 浮點數運算包含階碼和尾數,浮點數運算更加複雜。
- 浮點數在數的表示範圍、精度、溢出處理、編程等方面均優於定點數。
- 浮點數在數的運算規則、運算速度、硬件成本方面不如定點數。
5. 參考
《深入瞭解計算機系統》第三版
慕課網咚咚嗆老師(很不錯)