This page looks plain and unstyled because you're using a non-standard compliant browser. To see it in its best form, please visit upgrade to a browser that supports web standards. It's free and painless.

瞪眼族Blog 會員登入 會員註冊

« 上一篇 | 下一篇 »

前面一篇我們講了位運算後,基本C語言的大塊都提及了,一些細節和用法暫時不再本模塊介紹了。希望我的心願能夠在我畢業之前給我的大學生涯劃上一個圓滿的句號。加油努力。

在本模塊的第三篇就已經講了基本的數據類型,其中把浮點數刻意留在了後面來介紹。我的理解是在我們理解了內存,指針,位運算等後,再來介紹浮點這個特殊而又普通的數據類型比較好理解。浮點數和基本類型數據的存儲差別比較大,這裡不是說存儲形式的差別,而是浮點數存放的時候是要經過運算後再轉換成整數的4字節或8字節的形式,然後再存放到內存裡。因此,只通過16進制數是看不出來和整數有什麼差別。同樣,浮點數具體是怎麼存儲的,在大學的課程上一般不會細細講解,一般是我們自己有興趣再查閱資料。包括本篇的內容,如果你不是一個自學者或者充滿好奇心,你也不會看下去,也不會找到本篇的URL。因此,包括很多已經工作很多年的程序員都不知道浮點數具體是怎麼運算然後存儲的。就我來講,認為還是非常有必要了解這個常用的數據類型的換算過程,雖然我們個人來講很難去打破當前浮點數的計算規則以至於將他的精度提高,但是了解下底層工作者們的辛苦,我們應該向他們真誠的致敬。因為有他們,我們便有了大樹可以乘涼。

好了,廢話不多說。本篇的目的就是為了讓更多的人了解浮點數存儲的基本原理,還是那句話,學習的同時帶著思考。同樣這裡不討論浮點數的精度損失和數值的計算理論。直接講實質的表現。

在計算機發展過程中,我們使用的小數和實數曾經提出過很多種的表示方法。典型的比如相對於浮點數的定點數(Fixed Point Number)。在這種表達方式中,小數點固定的位於實數所有數字中間的某個位置。貨幣的表達就可以使用這種方式,比如88.22 或者22.88 可以用於表達具有四位精度(Precision),小數點後有兩位的貨幣值。由於小數點位置固定,所以可以直接用四位數值來表達相應的數值。 SQL 中的NUMBER 數據類型就是利用定點數來定義的。還有一種提議的表達方式為有理數表達方式,即用兩個整數的比值來表達實數。

很顯然,上面的定點數表示法有缺陷,不能表示很小的數或者很大的數。於是,為了解決這種問題,我們的前輩們自然想到了科學技術法的形式來表示,即用一個尾數(Mantissa ),一個基數(Base),一個指數(Exponent)以及一個表示正負的符號來表達實數。比如123.456 用十進制科學計數法可以表達為1.23456 × 102 ,其中1.23456 為尾數,10 為基數,2 為指數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大範圍的實數。

大約就在1985年,IEEE標準754的推出,它是一個仔細制定的表示浮點數及其運算的標準。這項工作是從1976年Intel發起8087的設計開始的,8087是一種為8086處理器提供浮點支持的芯片,他們僱傭了William Kahan,加州大學伯克利分校的一位教授,作為幫助設計未來處理器浮點標準的顧問。他們支持Kahan加入一個IEEE資助的製訂工業標準的委員會。這個委員會最終採納了一個非常接近於Kahan為Intel設計的標準。目前,實際上所有的計算機夠支持這個後來被稱為IEEE浮點(IEEE floating point)的標準。這大大改善了科學應用程序在不同機器上的可移植性。所謂IEEE就是電器和電子工程師協會。

介紹完了歷史,先來看看浮點數最直接的表示。在數學上:

12.341010 = 1*101 2*100 3*10-1 4*10-2 = 12(34/100) (這裡由於編輯器的原因,只能寫這麼機械了)。

在比如二進制:

101.112 = 1*22 0*21 1*20 1*2-1 1*2-2 = 4 0 1 1/2 1/4 = 5(3/4)。


上面簡單的描述了在數學意義上的浮點數表示,但是在計算機中,我們存放在內存中的直觀上看16進制數,那麼這些16進制數是怎麼表示我們浮點數的二進制形式呢?

在IEEE 標準中,浮點數是將特定長度的連續字節的所有二進制位分割為特定寬度的符號域,指數域和尾數域三個域,其中保存的值分別用於表示給定二進制浮點數中的符號,指數和尾數。這樣,通過尾數和可以調節的指數(所以稱為"浮點")就可以表達給定的數值了。具體的格式:

                     符號位 階碼 尾數 長度
float 1 8 23 32
double 1 11 52 64

我們都知道浮點數在32位機子上有兩種精度,float佔32位,double佔64位。很多朋友喜歡把double用於8字節的數據存儲。從這點我們應該不要特殊看到浮點數的內存存儲形式,他跟整數沒有什麼區別,只是在這4字節或者8字節裡有3個區域,整數有符號只有符號位及後面的數值,之所以最高位表示有符號數的符號位。原因之一在於0x7fffffff位最大整數,為整個32位所能表示的最大無符號整數0xffffffff的一半減一,也就是:比如1字節:無符號是:0xff,有符號正數為:(0, 127],負數為[-128, 0)。在8位有符號時,肯定內存值大於等於: 0x80。二進制就是1000 0000,比他大,只會在低7位上變化,最高位已經是1了,變了就變小了。所以這裡也是一個比較巧用的地方,一舉兩得。


那麼,我們先來看32位浮點數的換算:

1. 從浮點數到16進制數

float var = 5.2f;

就這個浮點數,我們一步一步將它轉換為16進制數。

首先,整數部分5,4位二進製表示為:0101。

其次,小數部分0.2,我們應該學了小數轉換為二進制的計算方法,那麼就是依次乘以2,取整數部分作為二進制數,取小數部分繼續乘以2,一直算到小數結果為0為止。那麼對0.2進行計算:

0.2*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2(0.2)*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2 ... ...

                0 0 1 1 0 0 1 1 ... ...

因此,這裡把0.2的二進制就計算出來了,結果就為:0.00110011... ... 這裡的省略號是你沒有辦法計算完。二進制序列無限循環,沒有到達結果為0的那一天。那麼此時我們該怎麼辦?這裡就得取到一定的二進制位數後停止計算,然後舍入。我們知道,float是32位,後面尾數的長度只能最大23位。因此,計算結束的時候,整數部分加上小數部分的二進制一共23位二進制。因此5.2 的二進製表示就為:

101.00110011001100110011

一共23位。

此時,使用科學計數法表示,結果為:

1.0100110011001100110011 * 22

由於我們規定,使用二進制科學計數法後,小數點左邊必須為1(肯定為1嘛,為0的話那不就是0.xxxx*sxxx 了,這樣沒有什麼意義),這裡不能為0是有一個很大的好處的,為什麼?因為規定為1,這樣這個1就不用存儲了,我們在從16進制數換算到浮點數的時候加上這個1就是了,因為我們知道這裡應該有個1,省略到這個1的目的是為了後面的小數部分能夠多表示一位,精度就更高一些了喲。那麼省略到小數點前面的1後的結果為:

.01001100110011001100110 * 22

這里後面藍色的0就是補上的,這裡不是隨便補的一個0,而是0.2的二進制在這一位上本來就應該為0,如果該為1,我們就得補上一個1.是不是這樣多了一位後,實際上我們用23位表示了24位的數據量。有一個位是隱藏了,固定為1的。我們不必記錄它。

但是,在對階或向右規格化時,尾數要向右移位,這樣被右移的尾數的低位部分會被丟掉,從而造成一定的誤差,因此要進行舍入處理。常用的捨入方法有兩種:一種是“0舍1入”法,即如果右移時被丟掉數位的最高位為0則捨去,為1則將尾數的末位加“1”,另一種是“恆置1”,即只要數位被移掉,就在尾數的末位恆置“1”。

舉個例子:

123.456的二進製表示:

123.456的二進製到23位時:111 1011.0111 0100 1011 1100 01...

後面還有依次為01...等低位,由於最高位的1會被隱藏,向後擴展一位如果不做舍入操作則結果為:

1.11 1011 0111 0100 1011 1100 0 * 26

但是經過舍入操作後,由於被捨掉的位的最高位是1,或者“恆置1”法,最後面的0都應該是1。因此最終就應該是:

1.11 1011 0111 0100 1011 1100 1 * 26

在這裡需要說明,不管是恆置1,還是0舍1入法,其根本都是為了減小誤差。

好了,尾數在這裡就計算好了,他就是01001100110011001100110 。

再來看階數,這裡我們知道是2^2次方,那麼指數就是2。同樣IEEE標準又規定了,因為中間的階碼在float中是佔8位,而這個階碼又是有符號的(意思就是說,可以有2^-2次方的形式)。

float 類型的偏置量Bias = 2k-1 -1 = 28-1 -1 = 127 ,但還要補上剛才因為左移作為小數部分的2 位(也就是科學技術法的指數),因此偏置量為127 2=129 ,就是IEEE 浮點數表示標準:

        V = (-1)s × M × 2E

        E = e - Bias

中的e ,此前計算Bias=127 ,剛好驗證了E = 129 - 127 = 2 。


這裡的階碼就是12910 ,二進制就是:1000 00012 。

因此,拼接起來後:

1000 0001 01001100110011001100110

| ← 8位→ | | ←------------- 23位-------------→ |

一共就是31位了,這裡還差一位,那就是符號位,我們定義的是5.2,正數。因此這裡最高位是0,1表示負數。

而後結果就是:

  0 1000 0001 01001100110011001100110

1位| ← 8位→ | | ←-------------- 23位------------→ |

到這裡,我們內存裡面的十六進制數產生了,分開來看:

0 100 0000 1 010 0110 0110 0110 0110 0110

    4 0 A 6 6 6 6 6

因此,我們看到的就是0x40A66666, 此就是5.2最終的整數形式。

2.從十六進制數到浮點數

我們還是可以用上面5.2的例子,再將0x40A66666換算回去,用同樣一個例子,結果更直觀,逆運算更好理解。那我們就開始吧。

首先,要還原回去,必須將這個16進制用我們的計算器換算成二進制:

0 100 0000 1 010 0110 0110 0110 0110 011 0

我是COPY上面的。這裡顏色已經很明顯了,我劃分成了3個區域。

首先確定符號,這裡是0,因此是正數。

其次看綠色的8位,換成10進制就是:12910

我們逆運算,知道這裡需要129 - 127 = 2得到指數,得到了指數,我們便知道我們小數點是向哪個方向移動了好多位。腦子裡已經有了一個科學計數法的錐形。

再次把紅色的23位提取出來,這裡不把它換成10進制,因為我們指數是表示的二進制上移動了多少位,底數是2,而不是10。

這裡因為之前我們都知道有個固定的1給省略了,因此這裡要給加上去。加上去之後:

1 010 0110 0110 0110 0110 011 0

這裡是24位,我們先不管,小數點添進去:

1 . 010 0110 0110 0110 0110 011 0 * 22

然後將科學計數法變換成普通的二進制小數:

1 01 . 0 0110 0110 0110 0110 011 0

到這裡,就真正可以把整數部分換成十進制了:

1 01 . 0 0110 0110 0110 0110 011 0

   5. xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

我們知道了,整數部分是5,後面的小數部分再進行逆運算:

這裡我們就應該想想小數到二進制數是乘法,這裡逆運算就應該除以2,因此就可以表示為:

0 . 0 0110 0110 0110 0110 011 0

0 0*2-1 0*2-2 1*2-3 1*2-4 0*2-5 0*2-6 1*2-7 ... ... 0*2-21 這樣一個式子,我們算出結果來,放在浮點數里:

5.1999998。

因此我們可以看到精度已經有損失了。

問題一:寫寫-5.2的16進制數?

再來看一個例子:

float var = 0.5, 算16進制數。

首先,0.5整數部分為0,這裡就不處理了。

其次,0.5小數部分,二進製表示為:0.1

這裡是0.1,將尾數補滿23位則是:

0.10 0000 0000 0000 0000 0002

由於小數點左邊是0,因此需要向右移動一位,因此:

1.0 0000 0000 0000 0000 00002 * 2-1

這裡1又被省略掉,所以23位全部變成了0 ,因此:

.00 0000 0000 0000 0000 00002 * 2-1

然後,因為這裡指數是-1,因此階碼就是:-1 127 = 126 = 0111 11102

這樣一來,階碼就有了,由於又是正數,那麼組合起來:

0 01111110 00000000000000000000000

這樣一來,最終的16進制數則為:0x3f000000.

是不是很簡單啊。


64位浮點數 的換算:

這裡就不再具體說明怎麼換算的了,只需要提到2個地方:

一是,中間的階碼在double中佔有11位,因此就不是127了,而是加上1023,因為11位能表示的最大無符號數是2047,因此有符號範圍[-1024, 1023]。

二是,尾數是52位,因此精度更高,能表示的數也就越大。我們在換算5.2的時候,後面的小數二進制前面的5的二進制再省略一位後的總位數要填滿52位。

好了,浮點數也沒有太多要說的,就到這裡吧,在用的時候注意精度和範圍就可以了。

最後在提一個問題:

問題二:

float var0 = 5.2;

float var1 = 500.2;

float var2 = 50000.2;

float var3 = 5000000.2;

觀察這幾個數,加深一下那三個域的計算方式,並說出這些數據有什麼規律?