爲什麼“0.1+0.2=0.30000000000000004”?
首先聲明這不是bug,原因在與十進制到二進制的轉換導致的精度問題!
其次這幾乎出現在很多的編程語言中:C/C++,Java,Javascript中,準確的說:“使用了IEEE 754浮點數格式”來存儲浮點類型(float 32,double 64)的任何編程語言都有這個問題!
簡要介紹下IEEE 754浮點格式:它用科學記數法以底數爲2的小數來表示浮點數。IEEE浮點數(共32位)用1位表示數字符號,用8爲表示指數,用23爲來表示尾數(即小數部分)。此處指數用移碼存儲,尾數則是原碼(沒有符號位)。之所以用移碼是因爲移碼的負數的符號位爲0,這可以保證浮點數0的所有位都是0。
雙精度浮點數(64位),使用1爲符號位、11位指數位、52位尾數位來表示。
因爲科學記數法有很多種方式來表示給定的數字,所以要規範化浮點數,以便用底數爲2並且小數點左邊爲1的小數來表示(注意是二進制的,所以只要不爲0則一定有一位爲1),按照需要調節指數就可以得到所需的數字。
例如:十進制的1.25 = 二進制的1.01 = 則存儲時指數爲0、尾數爲1.01、符號位爲0.
(十進制轉二進制)
回到開頭,爲什麼“0.1+0.2=0.30000000000000004”?首先聲明這是javascript語言計算的結果(注意Javascript的數字類型是以64位的IEEE 754格式存儲的)。
正如同十進制無法精確表示1/3(0.33333…)一樣,二進制也有無法精確表示的值。例如1/10。
64位浮點數情況下:
十進制0.1
= 二進制0.00011001100110011…(循環0011)
= 尾數爲1.1001100110011001100…1100(共52位,除了小數點左邊的1),指數爲-4(二進制移碼爲00000000010),符號位爲0
= 存儲爲:0 00000000100 10011001100110011…11001
= 因爲尾數最多52位,所以實際存儲的值爲0.00011001100110011001100110011001100110011001100110011001
十進制0.2
= 二進制0.0011001100110011…(循環0011)
= 尾數爲1.1001100110011001100…1100(共52位,除了小數點左邊的1),指數爲-3(二進制移碼爲00000000011),符號位爲0
= 存儲爲:0 00000000011 10011001100110011…11001
因爲尾數最多52位,所以實際存儲的值爲0.00110011001100110011001100110011001100110011001100110011
0.00011001100110011001100110011001100110011001100110011001
+ 0.00110011001100110011001100110011001100110011001100110011
= 0.01001100110011001100110011001100110011001100110011001100
轉換成10進制之後得到:0.30000000000000004!
浮點數中的特殊數字
除了一般範圍內的數字之外,還有一些特殊數字:無窮大、負無窮大、-0和NaN(“代表不是數字”)。造成了如下一些特殊情況:
public class FloatTest {
public static void main(String[] args) {
System.out.println(0.1f+0.2f); //0.3
System.out.println(0.1d+0.2d); //0.30000000000000004
System.out.println(Math.sqrt(-1.0)); //NaN
System.out.println(0.0 / 0.0);//NaN
System.out.println(1.0 / 0.0);//Infinity
System.out.println(-1.0 / 0.0);//-Infinity
System.out.println(0.0 / 0.0 + 1.0);//NaN + 1.0 = NaN
System.out.println(1.0 / 0.0 + 1.0);//無窮大 + 1.0 = Infinity
System.out.println(1.0 / 0.0 + 1.0 / 0.0);//無窮大 + 無窮大 = Infinity
System.out.println(0.0 / 0.0 1.0);//NaN 1.0 = false
System.out.println(0.0 / 0.0 == 1.0);//NaN == 1.0 = false
System.out.println(0.0 / 0.0 1.0);//NaN 1.0 = false
System.out.println(0.0 / 0.0 == 0.0 / 0.0);//NaN == NaN = false
System.out.println(0.0 == -0.01); //false
}
}
更精確的計算
既然一般的浮點數計算有這麼多問題,那麼如何實現更精確的計算呢?
Java中提供了BigDecimal類實現基於十進制的浮點數計算。
在Javascript 2(目前瀏覽器不支持)中提供一種use decimal;實現十進制浮點數計算:
{
use decimal;
var a = 0.1; // a is a decimal
var b = 0.2; // b is a decimal
var c = a + b; // c is a decimal (0.3)
}
var d = 0.1 + 0.2; // d is a double (0.30000000000000004)
var a = 0.1m; // a is a decimal
var b = 0.2m; // b is a decimal
var c = a + b; // c == 0.3m更詳細的JS的十進制計算方法。
C#也支持如上的m操作符實現十進制浮點數計算。