C++ 浮點數的儲存與精度
C++浮點數的儲存與精度
先看個例子(如下),我們看下int、float、double在記憶體的二進位制表示
#include<stdio.h> #include <stdlib.h> #include <string.h> #include<cstdlib> bool isLittleEndian() { int x = 1; return *((char*) (&x)) == 1; } template<class T> void printBinary(T d) { char* p = (char*)&d; int sz = sizeof(T); // bytes char* buff = new char[sz * 8 + 1]; buff[sz * 8] = '\0'; int used = 0; for (int n = 0; n < sz; n++) { for (int m = 0; m < 8; m++) { if ((p[n] >> m) & 1) used += sprintf(buff + used, "1");else used += sprintf(buff + used, "0"); } } if (isLittleEndian()) { int a = 0; int b = sz * 8 - 1; while (a < b) { buff[a] ^= buff[b]; buff[b] ^= buff[a]; buff[a] ^= buff[b]; a++; b--; } } printf("%s\n", buff); delete [] buff; } int main() { int i = 121; int i2 = -4; float f = 98.1; double d = 98.1; printBinary(i); // 00000000000000000000000001111001 printBinary(i2); // 11111111111111111111111111111100 printBinary(f); // 01000010110001000011001100110011 printBinary(d); // 0100000001011000100001100110011001100110011001100110011001100110 }
對int型別,其記憶體儲存的是二進位制補碼,比較好理解,對float和double型別而言,其二進位制表示怎麼理解呢?
C/C++採用的是IEEE浮點標準,它以“二進位制的科學表示法”表示一個小數:
其中:
- (-1)s表示符號位,當s=0,V為正數;當s=1,V為負數;
- M 表示有效數字,1 <= M < 2;
- 2E表示指數位。
舉例來說,十進位制的5.0,寫成二進位制是101.0,相當於1.01×2^2。那麼,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十進位制的-5.0,寫成二進位制是-101.0,相當於-1.01×2^2。那麼,s=1,M=1.01,E=2。
關於 M
注意,由於1≤M<2,也就是說,M可以寫成1.xxxxxx的形式,其中xxxxxx表示小數部分。IEEE 754規定,在儲存M時,預設這個數的第一位總是1,因此可以被捨去,只儲存後面的xxxxxx部分。比如儲存1.01的時候,只儲存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節省1位有效數字。以32位浮點數為例,留給M只有23位,將第一位的1捨去以後,等於可以儲存24位有效數字。
關於 E
首先,E為一個無符號整數(unsigned int),如果E為8位,它的取值範圍為0~255;如果E為11位,它的取值範圍為0~2047。
其次,科學計數法中的E是可以出現負數的,所以IEEE 754規定,E的真實值必須再減去一箇中間數,對於8位的E,這個中間數是127;對於11位的E,這個中間數是1023。
比如,2^10的E是10,所以儲存成32位浮點數時,必須儲存成10+127=137,即10001001。
最後,指數E可以再分成三種情況:
- E不全為0或不全為1。這時,浮點數就採用上面的規則表示,即指數E的計算值減去127(或1023),得到真實值,再將有效數字M前加上第一位的1。
- E全為0。這時,浮點數的指數E等於1-127(或者1-1023),有效數字M不再加上第一位的1,而是還原為0.xxxxxx的小數。這樣做是為了表示±0,以及接近於0的很小的數字。
- E全為1。這時,如果有效數字M全為0,表示±無窮大(正負取決於符號位s);如果有效數字M不全為0,表示這個數不是一個數(NaN)。
以float為例,最高的1位是符號位s,接著的8位是指數E,剩下的23位為有效數字M。
如下圖,E=01111100,對應的十進位制為124,124再減去中間數127,結果為-3;
M=01000...,對應的十進位制為2-2=0.25,還需要加上1,結果為1.25;
該浮點數結果 (-1)0* 1.25 * 2-3 = 0.15625。
以double為例,最高的1位是符號位S,接著的11位是指數E,剩下的52位為有效數字M。
總結如下:
位元組數 | 符號位 | 指數位 | 尾數位 | |
float | 4 bytes | 1 bit | 8 bit | 23 bit |
double | 8 bytes | 1 bit | 11 bit | 52 bit |
範圍:
float的指數範圍為-127 ~ 128,double的範圍是-1023 ~ 1024。
負指數決定了絕對值最小的非零數,正指數決定了絕對值最大的數。也即決定了範圍。
也即float的範圍為 -2128 ~2128,double的範圍是 -21024 ~21024。
精度:
float和double的精度是由尾數位決定的。浮點數在記憶體中是按照科學計數法來儲存的,其整數部分始終是一個隱藏著的1。由於他是不變的,因此對精度不會造成影響的。
float精度範圍是:能達到23二進位制位,約為 23 * log102 = 6.92 個十進位制位;
double的精度範圍是:能達到23二進位制位,約為 52 * log102 = 15.65 個十進位制位;
OK,最後我們再回到開頭的例子,
float f = 98.1; // 01000010110001000011001100110011
看下其二進位制,最高位符號位0,中間指數位10000101 的十進位制位133,E=133-127=6;
尾數位10001000011001100110011,對應的十進位制=0.532812,M=1.532812;
最後計算結果 1.532812 * 26 = 98.099998,精度為6位!
這裡我寫了個簡單函式用來解析float的二進位制:
float parseFloat(char* s) { int sign = s[0] - '0'; float M = 0; int E = 0; for (int n = 1; n <= 8; n++) { E = E * 2 + (s[n] - '0'); } for (int n = 9; n <= 31; n++) { M += pow(2, 8 - n) * (s[n] - '0'); } printf("sign=%d, E=%d, M=%f\n", sign, E, M); return pow((-1), sign) * (M + 1) * pow(2, (E - 127)); } int main() { float f = 98.1; printBinary(f); // 01000010110001000011001100110011 printf("float = %f\n", parseFloat("01000010110001000011001100110011")); }