摘要:說起浮點數,大家都是又恨又愛的。當小數不為時,浮點數的值為,即不是一個數。所以,整個浮點數的二進制表示就是。最后其實,浮點數有很多坑。因此,我們在使用浮點數的時候,一定要小心。還有,涉及到金額計算的時候,一定不能使用浮點數。
本文為作者自己的總結的,由于作者的水平限制,難免會有錯誤,歡迎大家指正,感激不盡。
說起浮點數,大家都是又恨又愛的。愛呢,是因為,只有它可以方便地使用小數;恨呢,是因為它并不能精確地表示小數。
以 PHP 為例:floor((0.1 + 0.7) * 10) 這樣一個函數調用,根據數學老師死得晚原理,大家都能得出 8 這個結果??墒鞘聦嵣夏??它會返回 7。數學老師的棺材板。。。(╯‵□′)╯︵┻━┻
可是為什么會出現這種情況呢?這就要從浮點數的特性說起了。
萬物皆二進制我們都知道,在計算機中,一切的一切都是二進制表示的。假設一個 4 字節整型的十進制數 8,在大端表示的機器中,表示成 00000000 00000000 00000000 00001000(0x0000008)。將十進制整數轉換成二進制數,是非常容易的。可是,小數呢?比如,我們要表示 1.75,該怎么存儲在計算機中呢?顯然,不能像整數一樣存儲了。
小數的二進制讓我們回憶一下,在十進制中,小數是怎么計算的。上面的 1.75 我們是這么算的:1 × 10^0 + 7 × 10^-1 + 5 × 10^-2 。那么我們按照相同的規則,來用二進制計算一下小數部分:0.75 = 1/2 + 1/4,也就是 1 × 2^-1 + 1 × 2^-2 ,再加上前面的整數部分,那么整個式子就變成了 1 × 2^0 + 1 × 2^-1 + 1 × 2^-2 ,寫成二進制形式就是 1.11。所以,1.75 的二進制表示是 1.11。
對于將小數轉換為二進制,和整數部分除二取余相反的,是乘二取整。
0.75 * 2 = 1.5 -> 1
0.5 * 2 = 1 -> 1
所以我們同樣可以得出 1.11。
科學計數法好了,我們已經知道如何表示一個小數的二進制了。辣么,問題來了。學過 C 語言的同學都知道,一個 float 只有 4 字節,一個 double 也只有 8 字節。那么,這么表示一個小數,好像范圍很有限。
在數學老師哭暈在廁所之前,我們應該還記得十進制數中有這么一個東西——科學計數法,我們可以很方便地用它來表示很大的十進制數。那么,同理,我們也可以用在浮點數的表示上。
讓我們先來回憶一下,科學計數法的表示。假設我們有一個數 17500,我們可以用科學計數法表示成 1.75 × 10^4 。我們照葫蘆畫瓢,在二進制數中,假設有一個數是 11010。我們來和十進制對應一下。十進制是乘 10,那么二進制就是乘 2,我們對應的就可以寫成 1.101 × 2^100 。對,其實就是這么簡單。那也許有的人會問了,為什么不寫成 0.1101 × 2^101 呢?我們再來回憶一下,在十進制科學計數法中,是不是有一個規定,整數部分的范圍是 [1,10)。那對應到我們的二進制數上,這個規定就可以變成 [1,2) 了,沒錯,對應關系就是這么簡單。
浮點數好了,我們現在也知道怎么使用二進制來表示小數,以及使用科學計數法來表示二進制小數了。那么,我們距離把數字存入計算機內存僅剩一步之遙了,我們要把所有的東西存到內存里去,那么我們就需要合理地分配內存空間。浮點數有兩種,一種是單精度浮點數(float),占用 4 字節的內存。其中,1 位是符號位,8 位是階碼(冪),23 位是尾數(小數部分)。
細心的各位可能會發現,好像沒有整數部分?別急,這就是上面那個規定的有用之處。當整數部分在 [1,2) 之間時,也就只可能取到一個值 1,那么,對于這個值,我們是不是就可以當做默認值而不記錄在浮點數的表示中了?而這樣,我們的浮點數的精度又多了一位(小數部分的位數決定了精度)。這種表示叫做隱含 1 開頭的表示。
規格化與非規格化 偏置值到了這里,我們發現,第一位是浮點數的正負符號,那么,對于一個科學計數法來說,階碼同樣需要有正負。而在單精度中,階碼只有 8 位;雙精度中,階碼只有 11 位。如果我們給階碼表示成補碼,那么,我們能夠表示的數的范圍就會縮小,這樣顯然是不劃算的。于是,偏置值就由此誕生了。
規格化的值(階碼不全為 0 或 1)在內存中的規格化的浮點數表示中,階碼并非是 2 的冪,而是經過計算的結果,這個計算公式就是 e - Bias,這里的 Bias 就是偏置值,而 e 就是階碼在浮點數中的二進制表示。Bias 的值是 2^k-1 - 1(單精度是 127,雙精度是 1023),所以,e - Bias 的取值范圍就是 [-126, 127](單精度)和 [-1022, 1023](雙精度)。其實如果對補碼了解的比較好的同學,應該就能看出來,這其實就是省略了符號位的補碼表示)。
通過上面的隱含 1 開頭的表示的尾數,我們可以計算出基數 M = 1 + f。那么我們整個的浮點數可以寫成這樣一個表達式:M × (e - Bias)。
非規格化的值(階碼全為 0)對于規格化和非規格化的值來說,我們都可以用同一個式子來表示。不過,為了某些更加方便的原因(這里就不展開講了),對它們做了區分。如果按照規格化的計算來看,階碼的值是 0 - Bias,不過在這里,我們讓階碼的值等于 1 - Bias。同樣的,由于我們給階碼加了 1,那么整個浮點數就會向左移動一位,那么,我們需要讓浮點數的值不變,M 就不在需要上面整數部分的 1 了,所以 M = f。
同時,我們會發現一個問題,那就是 +0.0 和 -0.0 在浮點數的二進制表示上是不同的。
特殊值(階碼全為 1)最后,還剩下這樣一種數字,那就是階碼全為 1 的情況。當小數為 0 的時候,浮點數的值為 ∞。當小數不為 0 時,浮點數的值為 NaN,即不是一個數(Not a Number)。
計算浮點數好了,扯了這么多,我們現在回到最開始的問題上,floor((0.1 + 0.7) * 10) = 7。我們先看 0.1 的二進制表示。
首先,我們將十進制小數轉換成二進制小數,可以得到 0.000[1100]···。讓我們轉換成浮點數的二進制表示。按照上面的規則,它可以被表示成科學計數法 1.10011001100110011001100 × 2^-4 ,這樣,階碼就是 -4 + 127 = 123,二進制表示為 01111011。所以,整個浮點數的二進制表示就是 00111101110011001100110011001100(0x3dcccccc)。同樣的,0.7 會表示為00111101001100110011001100110011(0x3d333333)。
首先我們要對階碼小的數進行對階,然后再進行尾數的加法,這樣,我們得到的值就是 00111101111001100110011001100101。我們將其轉換成十進制,發現,它是小于 0.8 的。因此,當我們再進行乘法運算向下取整時,會等于 7。
最后其實,浮點數有很多坑。因此,我們在使用浮點數的時候,一定要小心。還有,涉及到金額計算的時候,一定不能使用浮點數。
參考文獻本文為作者自己讀書總結的文章,由于作者的水平限制,難免會有錯誤,歡迎大家指正,感激不盡。
《深入理解計算機系統(第 3 版)》第 2.4.2 節
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88639.html
摘要:而的浮點數設置的偏移值是,因為指數域表現為一個非負數,位,所以,實際的,所以。這是因為它們在轉為二進制時要舍入部分的不同可能造成的不同舍 IEEE 754 表示:你盡管抓狂、罵娘,但你能完全避開我,算我輸。 一、IEEE-754浮點數捅出的那些婁子 首先我們還是來看幾個簡單的問題,能說出每一個問題的細節的話就可以跳過了,而如果只能泛泛說一句因為IEEE754浮點數精度問題,那么下文還是...
閱讀 2305·2021-09-28 09:45
閱讀 3596·2021-09-24 09:48
閱讀 2256·2021-09-22 15:49
閱讀 3093·2021-09-08 16:10
閱讀 1586·2019-08-30 15:54
閱讀 2317·2019-08-30 15:53
閱讀 3012·2019-08-29 18:42
閱讀 2865·2019-08-29 16:19