1. 程式人生 > >【解碼】浮點數精度問題 | 為什麽(int)(32.3 x 100) = 3229?

【解碼】浮點數精度問題 | 為什麽(int)(32.3 x 100) = 3229?

... amp pre 16進制 匯報 eee 但我 oid 指導

零 | 序

前幾天在找一個代碼問題時,苦思不得其解,簡直要懷疑人生。查看各種參數,輸入輸出,都符合條件,最後各種排除法之後,定位到一段簡單的代碼,簡化後大致如下:

#include<stdio.h>

int main()
{
    double a = 32.3;
    int    b = 100;
    int    c = (int)(a*b);
    printf("c = %d",c); //c = 3229
    return 0;
}

原代碼中本來預想c應該會等於3230,可是最後的結果卻是3229!!!

第一反應就覺得應該是浮點數精度問題,但是怎麽條理清晰地向別人解釋呢?好像有點難度,於是回家認真翻閱了下書籍,整理了一下思路。

一個簡單的解釋是:

我們都知道計算機中只有0和1,也沒有小數點,因此要表示浮點數時有自己的一套表示方法,這套表示方法在有限位數情況下有時並不能精確的表示某個浮點數,只能盡量逼近它,例如這個例子中,我們定義了一個double型的32.3,我們以為它表示32.3,但是計算機用有限長的0和1只能表示32.2999999......,這樣當這個數乘上100時,就變成了3229.99999,當它從double轉型成int時,小數點被舍掉了,就變成了3229。

這個解釋......好像似懂非懂的樣子,那麽問題來了:

1. 浮點數在計算機中到底是怎麽存儲的?為什麽有的小數無法精確表示?

2. 浮點數乘法是怎麽實現的?

3. double轉型成int時,為什麽會把小數舍掉?

一 | 浮點數表示

要理解上面的問題,我們先從更簡單的2進制小數開始,我們知道在10進制中:123.45 = 1 x 102 + 2 x 101 + 3 x 100 + 4 x 10-1 + 5 x 10-2

類似的2進制小數也可以這樣表示:101.11 = 1 x 22 + 0 x 21 + 1 x 20 + 1 x 2-1 + 1 x 2-2 = 5.75,

如果考慮有限長度,我們知道1/3在10進制中沒辦法準確表示,同樣的,二進制中也有不能精確表示的數,如1/5,二進制只能表示那些能被寫成a x 2b的數,就像上面的5.75。

所以本文開始的例子中32.3 = 0010 0000.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...... = 32.29999999999......

這就解釋了為什麽32.3在計算機中是32.999999999999......

等等!不是說計算機中沒有小數點的嗎?

確實如此,因此在實際計算機中,采用的是IEEE浮點表示法(IEEE-754標準),即V=(-1)s x (1.M) x 2E-f表示一個浮點數,其中s是符號,M是尾數,E是階碼,其存儲規則如下:

單精度格式(32位):符號位(s)1位;階碼(E)8位,階碼的偏移量(f)為127(7FH);尾數(M)23位,用小數表示,小數點放在尾數域的最前面;
雙精度格式(64位):符號位(s)1位;階碼(E)11位,階碼的偏移量(f)為1023(3FFH);尾數(M)52位,用小數表示,小數點放在尾數域的最前面。

舉個簡單的例子:(1.75)10 = (1.11)2 = 1.11 x 20,所以在單精度格式中s = 0,M = 11,E = 127 = (01111111)2

因此在計算機中,float型的1.75存儲為 0 01111111 11000000000000000000000 = (3FE00000)16

而double型的1.75存儲為(3FFC000000000000)16,這個就留給您自己去推算一遍了。

下面我們通過一段代碼來驗證一下上面的原理,證實1.75在計算機中確實是這樣存儲的。

首先我們定義一個指向類型為unsigned char的對象指針,然後定義一個show_bytes方法,打印出每個以16進制表示的字節,%.2x表示整數必須用至少兩個數字的十六進制格式輸出。接著定義show_int,show_float,show_double分別調用show_bytes,根據不同的類型和長度,打印出對應的字節表示。

#include<stdio.h>

typedef unsigned char *byte_pointer; //定義一個指向類型為unsigned char的對象指針

//以16進制打印指針指向地址中的字節序列
void show_bytes(byte_pointer start, int len){ int i; for (i = 0; i < len; i++) printf("%.2x",start[i]); printf("\n"); } //打印整數型變量 void show_int(int x){ show_bytes((byte_pointer)&x, sizeof(int)); } //打印單精度浮點變量 void show_float(float x){ show_bytes((byte_pointer)&x, sizeof(float)); } //打印雙精度浮點變量 void show_double(double x){ show_bytes((byte_pointer)&x, sizeof(double)); }

//主程序
int main() { double a = 1.75; show_float(a); show_double(a);return 0; }

運行結果為:

0000e03f
000000000000fc3f

註意到這裏結果似乎跟我們推算的值不太一樣,這是因為我的計算機采用小端法存儲(這個概念如不清楚請Google之),即把低序的存在低地址,所以00 00 e0 3f從高地址開始讀就是3f e0 00 00。那麽本文一開始提到的32.3在雙精度中是怎麽表示的呢?修改程序後運行可得:

cdcc0042
9a99999999194040

二 | 浮點數乘法

搞清楚了浮點數的存儲方式,我們來看看浮點數的乘法是怎麽實現的。假設有兩個浮點數:

x = Mx x 2Ex  y = My x 2Ey

那麽x*y =( Mx x 2Ex ) ( My x 2Ey ) = 2Ex+Ey·(Mx * My),

也就是說兩個浮點數相乘的結果就是它們的階碼相加,尾數相乘

所以在雙精度中32.3 x 100 = (1.0000 0010 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 x 25) x (1.1001 x 26)

            = 1.1001 0011 1011 1111 1111 1111 1111... x 211

            = (0100 0000 1010 1001 0011 1011 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111)2

            = 3299.9999999999......

這裏是以二進制小數的方式簡單說明了下浮點數的乘法,雖然浮點數的乘法可以轉換成定點數的加法和乘法,但我們知道在計算機中的0和1,也並沒有真正的“加法”和“乘法”,所有的操作是通過寄存器和邏輯門操作完成的,想要真正“理解”浮點數乘法操作是怎麽實現的,不妨研讀下“匯編語言”相關內容。

PS. 順便說一下,浮點數32.3乘整數100,按C語言的規則是100轉成浮點數再運算,而不是32.3先轉成整數再運算。

三 | 浮點數轉型成整數

浮點數轉型成整數時,會把小數點舍掉,有人說,這是C語言規定的,沒什麽好解釋的。但是計算機總有自己的一套規則吧,究竟是怎麽轉換的呢?這方面容我再好好深入學習下《匯編原理》和《深入理解計算機系統》,再來向各位匯報,也歡迎各位大神指導。

另外,如果把文章開頭的double a = 32.3變成float a = 32.3,結果c會變成3230,各位讀者如果有興趣可以思考下為什麽。

總結一下,浮點有風險,使用需謹慎!

參考文獻:

1. 《深入理解計算機系統(第2版)》 機械工業出版社

2. https://en.wikipedia.org/wiki/IEEE_754_revision

3. http://share.onlinesjtu.com/mod/tab/view.php?id=176

【解碼】浮點數精度問題 | 為什麽(int)(32.3 x 100) = 3229?