有同事在工作中遇到了一個奇怪的問題,這個問題與浮點數計算相關,代碼如下:
1 #include
2 using namespace std;
3
4 int main()
5 {
6 double s = 6.0;
7 double e = 0.2;
8
9 cout << static_cast
10 return 0;
11 }
這段代碼看起來很簡單,心算一下,應該輸出30才對。
但結果卻是我們在32 和 64位 linux平台下得到了不同的結果,分別是29和30,意想不到對吧?
然後,如果把代碼改成如下:
1 #include
2 using namespace std;
3
4 int main()
5 {
6 double s = 6.0;
7 double e = 0.2;
8
9 double d = s/e;
10
11 cout << static_cast
12 return 0;
13 }
你會發現在兩個平台上都得到了相同的“正確”結果!為什麼呢?
稀疏的浮點數
眾所周知,計算機是無法精確地表示所有浮點數的,無理數的稠密性使得無論我們用多高精度的數據類型來表示浮點數,所能表示的范圍相對整個無理數來說都是相當相當地稀疏疏。
因此在計算機的世界裡,我們只能盡可能用有限精度地來表示一定范圍的數據,至於那些沒法精確表示的數字,就只能在計算機所能表示的范圍裡找一個和它最接近的數來湊和湊和。
這個好像比較好理解,比如說根號2什麼的,我們都知道這些無理數不能在計算機裡完全精確的表示,但還有那麼一些有理數,在10進制裡雖然可以精確地表示,在2進制裡卻也是無法精確表示的,比如說上面例子中的0.2,你如果對此有懷疑,可以好好回顧一下怎麼把小數轉成二進制,然後慢慢用筆在紙上演算一下。
講這些,無非還是想說明,計算機世界裡的浮點數是相當疏松地,借用《深入理解操作系統》一書裡的一張圖,讓大家
浮點數的折斷與轉換
因為很多小數是無法精確表示的,因此我們只能盡可能在有限精度的小數裡找到最近接近的數來近似那些無限的小數。
那麼計算機是怎麼樣來做這些逼近的呢?常用的有如下4種方式:
其中第一種是默認使用方式,需要注意的是這些折斷方式並不僅限於由浮點數轉為整數,浮點數之間也是適用的。
在C語言中,浮點數與整數的轉換有以下幾條原則:
1) int型轉為float,不會overfloat,但有些數用float無法表示,因此可能需要rounding,記住float很稀疏。
2) 由int或float轉為double時,精度不會丟失,畢竟double精度高太多了。
3) double轉為float時,很可能會overfloat, 轉換則用round-to-even的方式(默認)進行。
4) 由float, double轉換為int時用round-to-zero的方式轉換,當然也很可能會被截斷。
請注意第3條,第4條原則,它們轉換時使用的不同原則有時會導致一些很微妙的結果。
Intel IA32 浮點運算
IA32處理器和很多其它一些處理器一樣,有專門用於保存浮點數的寄存器,當在cpu中進行浮點數運算時,這些寄存器就用來保存輸入輸出及相關的中間結果。
但IA32有一個比較特別的地方,它的浮點數寄存器是80位的,而我們在程序中只用到32和64位兩種類型,因此當把float,double放入到cpu中時,它們都會被轉換成了80位,然後以80位的方式進行運算,最後得到的結果再轉換回來。這樣特性使得浮點數的計算可以相對更精確些,但同時,一不小心很可能也會引出一些意想不到的問題。
你可能突然恍然大悟了,對的,我們最開始提到那個奇怪的問題就與此相關。
s/e得到結果是個80位的浮點數,由這個浮點數先轉換成double再轉成int,與直接就轉換成int,結果很可能是不同的。
比如在我們的例子中,s/e ~ 29.999999....時,s/e轉換成double使用round-to-even的方式,會得到也許是30.0000001,再轉成整形時,得到30.
但如果直接由29.99999...轉換成整型,得到卻是29。