1. 程式人生 > 實用技巧 >C++ 浮點數的儲存與精度

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可以再分成三種情況:

  1. E不全為0或不全為1。這時,浮點數就採用上面的規則表示,即指數E的計算值減去127(或1023),得到真實值,再將有效數字M前加上第一位的1。
  2. E全為0。這時,浮點數的指數E等於1-127(或者1-1023),有效數字M不再加上第一位的1,而是還原為0.xxxxxx的小數。這樣做是為了表示±0,以及接近於0的很小的數字。
  3. 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"));
  
}