浮点数的二进位表达方法

浮点数的二进位表达方法

浮点运算知识点

小数二进制表达

与整数的二进制表达相同我们可以假设任意小数的二进制为 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;
}

<<:  [Day 2] 差异性安装

>>:  网站不想你爬

ESP32_DAY4 用VS Code开范例程序

经过前两天的环境准备,我们就来试着让ESP32开发板上内建的LED灯闪烁,熟悉一下如何在VS Cod...

[Day 04] 眼前的黑不是黑,你说的白是什麽白?(直方图均衡化)

前言 「眼前的黑不是黑,你说的白是什麽白」-- 你是我的眼(萧煌奇) 没错,并非所有图片都是在理想的...

Day25 NodeJS中的前端框架 I

在开发网站系统时,使用前端框架可以让资料更容易在介面中被使用、也可以建立模组化的介面,可以更有效的提...

Progressive Web App 针对应用操作介面优化操作体验 (27)

网页的外观和操作本质上还是和原生的有差异但可以透过配置来让体验更接近。 全萤幕模式 视觉设计 事件操...

DAY11 - DFS应用

昨天写了DFS模板,今天就搭配模板放几题DFS的例题!! void dfs(){ if(越界或不合理...