小數(shù)計算為什么會出錯膝晾?
簡要答案
實際上栓始,不是運算本身會出錯,而是計算機根本就不能精確的表示很多數(shù)血当,比如0.1這個數(shù)幻赚。
計算機是用一種二進制格式存儲小數(shù)的,這個二進制格式不能精確表示0.1臊旭,它只能表示一個非常接近0.1但又不等于0.1的一個數(shù)落恼。
數(shù)字都不能精確表示,在不精確數(shù)字上的運算結(jié)果不精確也就不足為奇了离熏。
0.1怎么會不能精確表示呢佳谦?在十進制的世界里是可以的,但在二進制的世界里不行滋戳。在說二進制之前钻蔑,我們先來看下熟悉的十進制。
實際上奸鸯,十進制也只能表示那些可以表述為10的多少次方和的數(shù)咪笑,比如12.345,實際上表示的:110+21+30.1+40.01+5*0.001娄涩,與整數(shù)的表示類似窗怒,小數(shù)點后面的每個位置也都有一個位權(quán),從左到右蓄拣,依次為 0.1,0.01,0.001,...即10^(-1), 10^(-2), 10^(-3)扬虚。
很多數(shù),十進制也是不能精確表示的球恤,比如1/3, 保留三位小數(shù)的話辜昵,十進制表示是0.333,但無論后面保留多少位小數(shù)咽斧,都是不精確的路鹰,用0.333進行運算贷洲,比如乘以3,期望結(jié)果是1晋柱,但實際上卻是0.999优构。
二進制是類似的,但二進制只能表示哪些可以表述為2的多少次方和的數(shù)雁竞,來看下2的次方的一些例子:
可以精確表示為2的某次方之和的數(shù)可以精確表示钦椭,其他數(shù)則不能精確表示。
為什么一定要用二進制呢碑诉?
為什么就不能用我們熟悉的十進制呢彪腔?在最最底層,計算機使用的電子元器件只能表示兩個狀態(tài)进栽,通常是低壓和高壓德挣,對應(yīng)0和1,使用二進制容易基于這些電子器件構(gòu)建硬件設(shè)備和進行運算快毛。如果非要使用十進制格嗅,則這些硬件就會復(fù)雜很多,并且效率低下唠帝。
為什么有的小數(shù)計算是準(zhǔn)確的
如果你編寫程序進行試驗屯掖,你會發(fā)現(xiàn)有的計算結(jié)果是準(zhǔn)確的。比如襟衰,我用Java寫:
System.out.println(0.1f+0.1f);
System.out.println(0.1f*0.1f);
第一行輸出0.2贴铜,第二行輸出0.010000001。按照上面的說法瀑晒,第一行的結(jié)果應(yīng)該也不對吧馨印?
其實苔悦,這只是Java語言給我們造成的假象轩褐,計算結(jié)果其實也是不精確的,但是由于結(jié)果和0.2足夠接近间坐,在輸出的時候,Java選擇了輸出0.2這個看上去非常精簡的數(shù)字邑退,而不是一個中間有很多0的小數(shù)竹宋。
在誤差足夠小的時候,結(jié)果看上去是精確的地技,但不精確其實才是常態(tài)蜈七。
怎么處理計算不精確
計算不精確,怎么辦呢莫矗?大部分情況下飒硅,我們不需要那么高的精度砂缩,可以四舍五入,或者在輸出的時候只保留固定個數(shù)的小數(shù)位三娩。
如果真的需要比較高的精度庵芭,一種方法是將小數(shù)轉(zhuǎn)化為整數(shù)進行運算,運算結(jié)束后再轉(zhuǎn)化為小數(shù)雀监,另外的方法一般是使用十進制的數(shù)據(jù)類型双吆,這個沒有統(tǒng)一的規(guī)范,在Java中是BigDecimal会前,運算更準(zhǔn)確好乐,但效率比較低,本節(jié)就不詳細說了瓦宜。
二進制表示
我們之前一直在用"小數(shù)"這個詞表示float和double類型蔚万,其實,這是不嚴謹?shù)模?小數(shù)"是在數(shù)學(xué)中用的詞临庇,在計算機中反璃,我們一般說的是"浮點數(shù)"。float和double被稱為浮點數(shù)據(jù)類型苔巨,小數(shù)運算被稱為浮點運算版扩。
為什么要叫浮點數(shù)呢?這是由于小數(shù)的二進制表示中侄泽,表示那個小數(shù)點的時候礁芦,點不是固定的,而是浮動的悼尾。
我們還是用10進制類比柿扣,10進制有科學(xué)表示法,比如123.45這個數(shù)闺魏,直接這么寫未状,就是固定表示法,如果用科學(xué)表示法析桥,在小數(shù)點前只保留一位數(shù)字司草,可以寫為1.2345E2即1.2345*(10^2),即在科學(xué)表示法中泡仗,小數(shù)點向左浮動了兩位埋虹。
二進制中為表示小數(shù),也采用類似的科學(xué)表示法娩怎,形如 m*(2^e)搔课。m稱為尾數(shù),e稱為指數(shù)截亦。指數(shù)可以為真爬泥,也可以為負柬讨,負的指數(shù)表示哪些接近0的比較小的數(shù)。在二進制中袍啡,單獨表示尾數(shù)部分和指數(shù)部分踩官,另外還有一個符號位表示正負。
幾乎所有的硬件和編程語言表示小數(shù)的二進制格式都是一樣的葬馋,這種格式是一個標(biāo)準(zhǔn)卖鲤,叫做IEEE 754標(biāo)準(zhǔn),它定義了兩種格式畴嘶,一種是32位的蛋逾,對應(yīng)于Java的float,另一種是64位的窗悯,對應(yīng)于Java的double区匣。
32位格式中,1位表示符號蒋院,23位表示尾數(shù)亏钩,8位表示指數(shù)。64位格式中欺旧,1位表示符號姑丑,52位表示尾數(shù),11位表示指數(shù)辞友。
在兩種格式中栅哀,除了表示正常的數(shù),標(biāo)準(zhǔn)還規(guī)定了一些特殊的二進制形式表示一些特殊的值称龙,比如負無窮留拾,正無窮,0鲫尊,NaN (非數(shù)值痴柔,比如0乘以無窮大)。
IEEE 754標(biāo)準(zhǔn)有一些復(fù)雜的細節(jié)疫向,初次看上去難以理解咳蔚,對于日常應(yīng)用也不常用,本文就不介紹了搔驼。
如果你想查看浮點數(shù)的具體二進制形式移斩,在Java中寝优,可以使用如下代碼:
Integer.toBinaryString(Float.floatToIntBits(value))
Long.toBinaryString(Double.doubleToLongBits(value));
寫在最后
都看到這里酒甸,保存思維導(dǎo)圖礁叔,順便給個贊唄