浮点数的二进位表达方法
浮点运算知识点
小数二进制表达
与整数的二进制表达相同我们可以假设任意小数的二进制为 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的误差实际上并不影响温度计运作
案例练习
将浮点数转换成16进位形式将16进位转换成浮点数将浮点数转换成字串// 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;}