iT邦幫忙

1

浮點數的二進位表達方法

浮點數的二進位表達方法

浮點運算知識點

小數二進制表達

與整數的二進制表達相同我們可以假設任意小數的二進制為 1011.0011也就是說,按照跟整數轉換相同的思路我們可以換算出

 1*2^(3) + 0*2^(2) + 1*2^(1) +1*2^(0) + 0*2^(-1) + 0*2^(-2) + 1*2^(-3) + 1*2^(4) = 11.1875

不過像1011.0011這種表達式是給人看得,真正在計算機內傳輸,是運用更高效率的科學記號方式,更進一步說是運用了正則表達式與ECXESS系統,畢竟使用上述二進制表示方法,不僅沒有告訴計算機小數點前後各有幾位,在傳統32bits的浮點數下能表示的範圍也有限,所以該方法僅限於讓人們清楚意識到,小數位跟整數位一樣,是使用2的次方數作為進位依據,差別僅在於所使用為負數

小數轉換誤差

我們從上一張圖可以看出一個問題,那就是小數位的表達是存在一個特定誤差範圍的,例如若要表示0.1(10進制),但是1* 2^(-1) = 0.5、1* 2^(-2) = 0.25、1* 2^(-3) = 0.125、1* 2^(-4) = 0.0625...

不管我們怎麼分配始終無法完美取得0.1,這也是計算機在小數位上存在的既定誤差,另一方面0.1轉換成二進制會等於0.0001100110011...無限循環,永遠無法求得準確值,計算機最後只能想出折衷方案取四捨五入或直接取到固定位數

浮點數表達式

浮點數表達式是計算機內用來表示小數的方法,由IEEE規定組成格式,並用有32 bits單精度(float)與64 bits雙精度(double)的資料型態

浮點數的表達方法主要由4個參數組成,分別是正負符號、尾數(m)、基數(n)、指數(e) :

那麼問題來了,二進制的浮點數該怎麼用上述科學記號方式表達成二進位的0和1進而讓計算機能夠讀懂? 換個角度想,我們把數字改成10進制,我們先就整數而言,比如說12500 :

10進位整數位中符號為正,尾數為1.25、基數為10、指數為4,這種方式是科學記號約定俗成的表達方式,其中重點在尾數應該表示為一個大於等於1,小於10的一組小數,因為是10進位,每增加一個位就必須乘以10,所以基數應當表達為10,指數表達進位數

要將12500轉換成浮點表達事前必須先概括介紹使用正則表達式與Excess系統的目的

正則表達式

計算機系統中用來表示浮點數尾數的方法

Excess系統

計算機系統中用來表示指數的的方法

使用這種類似科學記號的表達方式有效增加能容納位數,以及更便於計算機進行運算,因為最終目的是要使計算機能夠讀懂12500,更精確地說就是0和1的組合

可能很多人會直接將12500轉換成二進制 = 0011 0000 1101 0100,作為運算結果,不過浮點數的表達方式與整數是完全不同的,所以這種轉換是錯誤的,我們來看一下IEEE是怎麼規定浮點數的二進位表達方式 :

先釐清最簡單的部分,舉float為例,12500為正數,所以符號位是0,計算機是使用二進制單位表示,所以基數為2

指數我們需要使用Excess系統,該系統會將例如8 bits的指數位(0~255)取中間值作為0,也就是說127

0111 1111代表指數為0,二進制的127~255依序分別是0、1、2 ... 128,相反的二進制的126~0分別對應-1、-2 ... -127

12500存在於2^(13) = 81922到2^(14)=16384之間,所以我們求得12500的指數為13,因為使用Excess系統故加上127=140,再將140轉換成二進制可以得到1000 1100,這就是8位指數位

而正則表達式就像上面的小數轉換表一樣將賦值成1的位數呈上2^(-n)次方然後依序相加,需要注意最後需要加上1 * 2^(0),這是IEEE規定的浮點數格式,你可以想像科學記號也對於尾數的規定,在浮點數中我們使用的是小數點前固定為1的正則表達式,目的是湊出一個大於等於1小於2的一個小數位與指數位相乘進而得出近似值

因為+1是一個約定俗成,人盡皆知的概念,所以在真正填充二進位值的時候可以省略掉1這個欄位,空出來的空位剛好可以填充多一個小數位增加精度(雖然也不會增加多少,畢竟都乘到2^(-23))

回到12500的例子,轉成二進制等於 011 0000 1101 0100,經過右移將小數點前一位製作成1,最後在將小數點後捕到23位,該值就是12500的浮點數尾數,但是因為浮點數的正則表達規範,我們可以省略掉小數位前的1

不過在真實的案例中我們不太需要去編寫API去刻意轉換浮點數的二進位表達,因為當我們在宣告一個浮點變數時,計算機會自動將指標指向內存4 bytes區塊,並給定使用浮點表達式所生成的二進制碼資料初始值

程式案例

void float_to_hex(float a){
    float num = a;
    char buffer[40] = {0};
    unsigned int get_hex;
    int i,j=0; 

    get_hex = *(unsigned int*)&a; // hex必須要用unsigned int來取值
    printf("hex: %0x\n", get_hex);

    for(i = 0; i < 32; i++){
        if(i == 1 || i == 9){
            buffer[j++] = '-';
        }

        if((get_hex >> (32-(i+1)))%2 == 1)
            buffer[j++] = '1';
        else
            buffer[j++] = '0';
    }
    buffer[j] = '\0';
    printf("bin: %s\n", buffer);

    return;
}

執行結果

如何消除小數誤差

在前文我們提到計算機在做浮點運算時,會產生些微誤差,比如說0.1連續加10次,理論上應該要為10,但是永遠不可能

float a=0;
for(int i = 1 ; i <= 10 ; i++){
    a += 0.1

另外一種誤差錯誤發生在利用浮點作為判斷依據,這種寫法很危險,假如小數位發生錯誤,整個程式將會卡在無線迴圈中,所以一般我們不會使用浮點數作為判斷式的判斷子

float a=0;
while(1){
    a += 0.02;
    printf("%f\n", a);
    if(a == 4) // loop
        break;
}

要解決浮點數造成的問題,可以試著把浮點數轉成整數來計算,不是直接casting,而是想辦法藉由運算消除小數部分(例如乘以1000後運算),運算出結果後再將結果轉成浮點數,或者因為浮點造成的誤差實際上很小,如果對誤差容忍度不會太低,那大可忽略不計,例如室外測量溫度範圍在0~40度,那麼0.00001的誤差實際上並不影響溫度計運作

案例練習

  1. 將浮點數轉換成16進位形式
  2. 將16進位轉換成浮點數
  3. 將浮點數轉換成字串
// convert float to str
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>

float sqrt_float(uint16_t num, int exp){
    float number = num;

    if(exp == 0)
        return 1;
    else{
        for(int i = 1 ; i < abs(exp) ; i++)
            number *= num;
    }

    if(exp < 0)
        number = 1/number;

    return number;
}


uint32_t float_to_hex(float a){
    float num = a;
    char buffer[40] = {0};
    unsigned int get_hex;
    int i,j=0; 

    get_hex = *(unsigned int*)&a; // hex必須要用unsigned int來取值
    printf("hex: %0x\n", get_hex);
    printf("dec: %0u\n", get_hex);

    for(i = 0; i < 32; i++){
        if(i == 1 || i == 9){
            buffer[j++] = '-';
        }

        if((get_hex >> (32-(i+1)))%2 == 1)
            buffer[j++] = '1';
        else
            buffer[j++] = '0';
    }
    buffer[j] = '\0';
    printf("bin: %s\n", buffer);

    return get_hex;
}

char *hex_to_float_to_str(uint32_t num, int8_t precision){ // 16進制轉浮點再轉字串
    int j=0, exp, m; 
    float real_part, remain_part, n, value;
    bool negative=false;
    char *buffer = (char*)malloc(128);
    
    if((num & 0x80000000) != 0){
        negative = true; // 正負號
        buffer[j++] = '-';
    }

    exp = ((num & 0x7f800000) >> 23) - 127; // 取指數
    m = ((num & 0x007fffff));

    for(uint16_t i = 1 ; i <= 23 ; i++) // 取尾數
        n += ((m >> (23 - i))%2) * (1/sqrt_float(2,i)); 

    n++;
    value = n*(sqrt_float(2,exp));

    // Float to string, since no function in C.
    real_part = floor(value);
    remain_part = value - real_part;  
    for(uint16_t i = 1 ; i <= precision ; i++)
        remain_part *= 10;

    remain_part = floor(remain_part);           

    if(remain_part < 0) // block '-'
        remain_part *= -1;

    sprintf(buffer+j, "%.0f.%.0f", real_part, remain_part);
    

    return buffer;
}

int main(){
    float a;
    uint32_t test; 
    char *buffer_p;
    printf("Input a float type number: ");
    scanf("%f", &a);

    test = float_to_hex(a); // float to bin & hex

    buffer_p = hex_to_float_to_str(test, 5); // turn hex to float
    printf("float to string: %s\n", buffer_p);

    return 0;
}

尚未有邦友留言

立即登入留言