本章討論一些零碎的話題.
一. 其他導(dǎo)致浮點數(shù)存儲不精確的原因
在第三章中我們提到過, 數(shù)學中的小數(shù)是連續(xù)的, 而計算機中的小數(shù)(準確來說是ieee754標準中的小數(shù))是離散的.
這就會引發(fā)精度問題: 圖中的綠色指針只能指向藍點, 不能指向藍點之間的數(shù). 比如上面最右邊的圖, 綠色指針其實無法指向0.3, 當你想要指向0.3時, 實際上會被舍入為0.234, 即舍入到離它最近的藍點對應(yīng)的值.
而除此之外, 進制問題也會導(dǎo)致IEEE754浮點數(shù)存儲不精確
簡單來說就是: 有限長度編碼下, 每種進制都有他們不能精確表示的值
比如: 10進制不能精確的表示1/3 (0.3333333.....)
十進制可以精確表示1/5 (0.2), 但二進制無法精確表示1/5
不能精確表示時,只能進行近似. 編碼長度越長止后,近似程度越高
舉例: 下圖嘗試用二進制表示0.2, 可以發(fā)現(xiàn), 只能近似表示...
當你把一個十進制數(shù)存儲到計算機中時, 實際上存儲的是該數(shù)的二進制表示.
所以, 當你寫入如下代碼時:
float f = 0.2;
雖然0.2(十進制)遠遠沒有到達32位浮點數(shù)的精度上限(7位精度), 但計算機其實無法精確地存儲該數(shù)值, 因為0.2(十進制)無法使用二進制格式精確表示.
此時變量f對應(yīng)的內(nèi)存狀態(tài)是這樣的:
↑ 你鍵入的是0.2
↑ 內(nèi)存中實際存儲的是0.20000000298023223876953125
可以在c語言中驗證一下:
可見十進制的0.2無法用二進制精確表示, 但十進制的0.5卻可以用二進制精確表示.
二. 二進制的小數(shù)形式
有些同學可能會納悶, 二進制為什么會有小數(shù)形式? 我常見的二進制都是整數(shù)形式啊, 比如十進制的9, 表示為二進制是1001, 怎么會有 1001.101 這種二進制的小數(shù)格式呢.
其實對于程序員來說, 這里確實比如容易讓人困惑, 比如win10自帶的計算機, 就不支持二進制小數(shù):
許多編程語言, 比如js, 也不支持直接使用二進制小數(shù):
但和十進制一樣, 二進制其實也有小數(shù)形式, 而且很容易理解:
比如對于十進制數(shù) 78.23
十位: 7, 表示
個位: 8, 表示
十分位: 2, 表示2/10, 或說表示
百分位: 3, 表示3/100, 或說表示
這個十進制所表示的值是: 70 + 8 + 2/10 + 3/100
二進制數(shù)也是同理的:
比如對于二進制數(shù) 10.11
第一位: 1, 表示1 * 2^1 = 2
第二位: 0, 表示0 * 2^0 = 0
第三位: 1, 表示1 * 2^-1 = 0.5
第四位: 1, 表示1 * 2^-2 = 0.25
所以這個二進制表示的值, 其實就是十進制的2 + 0 + 0.5 + 0.25 = 2.75
這里比較有意思的一點是:
十進制小數(shù)點后面的那一位(也就是十分位), 對應(yīng)的權(quán)是1/10, 也就是0.1
即, 對于十進制數(shù)3.4, 這個4對應(yīng)的值是: 4 * 權(quán)? =? 4 * 0.1 = 0.4
而二進制小數(shù)點后面的一位, 對應(yīng)的權(quán)是1/2, 也就是十進制的0.5
所以對于二進制數(shù)0.1, 這個1對應(yīng)的值是: 1 * 0.5 = 0.5, 所以二進制的0.1, 其實等于十進制的0.5
這讓我想起來一個腦筋急轉(zhuǎn)彎, 問: 什么時候 1.1 比 1.3 要大?
答: 當1.1是個二進制數(shù), 而1.3是個十進制數(shù)的時候...
事實上: 對于小數(shù)點之后的位, 二進制的位權(quán)始終比十進制的位權(quán)要大, 舉例:
十進制數(shù): 小數(shù)點之后的位權(quán)依次是: 1/10,? 1/100,? 1/1000...
二進制數(shù): 小數(shù)點之后的位權(quán)依次是: 1/2,? 1/4,? 1/8...? 相應(yīng)位的權(quán)始終比↑十進制的要大
所以會出現(xiàn)這種現(xiàn)象
二進制:? ? ? ? ? ? ? ? 1.000001, 小數(shù)點后面的數(shù)看起來已經(jīng)很小很小了
對應(yīng)的十進制是:? 1.015625, 小數(shù)點后面的數(shù)其實還挺大...
在IEEE765標準中, 我們會經(jīng)常和二進制小數(shù)打交道, 所以這里補充一下相關(guān)知識.
三. 關(guān)于32位浮點數(shù), 一些不太正確的認知
1. 32位浮點數(shù)能存儲很大的整數(shù)
這是32位浮點數(shù)的取值范圍:
當我第一次看到這個取值范圍時, 我是很驚訝的, 怎么這么大?
一個浮點數(shù), 占用32字節(jié), 竟然能存儲下約±340000000000000000000000000000000000000這么大的數(shù)
相比之下, 一個同樣32字節(jié)的long類型, 存儲范圍只有約±2147483647
那我為啥還要用long類型...
...
一路學習到現(xiàn)在, 倒是可以繞過這個彎兒了, 那就是:
32位浮點型確實最大可以存儲到這么大的數(shù), 但精度很低
第三章中我們說過, 32位浮點數(shù)表盤中的藍點會越來越稀疏:
等到了這么大的數(shù)時, 其實藍點已經(jīng)稀疏的不成樣子了, 基本是不可用狀態(tài)
根據(jù)wiki中給出的間隔, 對于1.70141e38 到 3.40282e38范圍中的數(shù), 間隔是2.02824e31
也就是說, 大體上: 32位浮點數(shù)中, 能精確存儲1.70141e38
但無法精確存儲1.70141e38 + 1
也無法精確存儲1.70141e38 + 2,
也無法精確存儲1.70141e38 + 100000000000
...
下一個能精確存儲的數(shù)是: 1.70141e38 + 20282400000000000000000000000000 (即加上間隔)
這個精度基本上是不可用的.
事實上, 如果你要用float存儲整數(shù)的話, 最多只能精確存儲到 16777216
再大的話, 間隔就會變?yōu)?, 就不適合用來存儲整數(shù)了:
此時再回過頭來看看同為 32位 的long類型, 能精確存儲的整數(shù)范圍
約是: ±2147483647
比:? ? ±16777216? ? 大多了
所以存儲大整數(shù)還是用long類型吧
總結(jié): 32位浮點數(shù)只是有能力存儲到, 實際上存儲的數(shù)過大會導(dǎo)致精度過低, 基本上不可用. 用32位浮點數(shù)存儲整數(shù)時, 只適用存儲±16777216之間的整數(shù).
2. 32位浮點數(shù)能存儲很精確的小數(shù)
這是32位浮點數(shù)的取值范圍:
看起來好像能存儲這么精確的小數(shù)...
但其實和存儲整數(shù)一樣, 32位浮點數(shù)只是有能力存儲到這么小的小數(shù)而已...
事實上在第三章中我們詳細講解過: 32位浮點數(shù)的精確度是7位有效數(shù).
即如果你要存儲的數(shù) 整數(shù)部分 + 小數(shù)部分 放在一起超過了 7 位, 32位浮點數(shù)就不能精確存儲了
比如, 32位浮點數(shù)就不能精確存儲我們常背的部分圓周率
32位浮點數(shù)倒是可以存儲常見的月工資, 比如 5078.65, 或 12665.73. 但如果要存儲年工資, 或把工資存儲到3位小數(shù), 32位浮點數(shù)就不一定夠用了...
所以, 雖然32位浮點數(shù)的取值范圍看起來很大, 足足有:
但其實32位浮點數(shù)只適合存儲常見數(shù)據(jù)...
感性地去認知的話, float(也就是32位浮點數(shù))類型其實和int類型有些相似: int用于存儲最常用, 最自然的整數(shù). float則用于存儲最常用, 最自然的浮點數(shù)...編程時, 如果要存儲的數(shù)很大或精度很高(相對來說现恼,這些數(shù)往往不怎么常用或不怎么自然), 就要考慮改用long或double.
精確來說的話, 就是不要被32位浮點數(shù)駭人的取值范圍嚇到. 而是記住事實上它只能存儲7位有效數(shù)就行了.