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