小数的存储方式分为定点数存储和浮点数存储,但是根据IEEE 754 标准,现在用的编译器都是浮点数存储的。
首先所有的数据都要先转化成二进制数才能进行存储,但是在转换的过程中会遇到一个问题,小数部分(十进制数)转换成二进制使用“乘二取整法”(就是不断乘以 2,直到小数部分为 0),一个有限位数的小数并不一定能转换成有限位数的二进制,只有末位是 5 的小数才有可能转换成有限位数的二进制,其它的小数都不行。这就造成了转化出来的截取的二进制小数最后一位精度是不确定的。
-
定点数存储,即小数点固定。
用4个字节(32位)来存储无符号的定点数,并且约定,前16位表示整数部分,后16位表示小数部分,如下图所示:
16bit用于存储整数部分,16bit用于存储小数部分。
19.625 = 10011.101整数部分就是0000000000010011,小数部分是1010000000000000。存储范围
全置1最大:这个数值范围是求极限来的。
最低位置1,其余为0最小:
注:为什么不能为0,我个人的猜想是因为上面说过的,最后一位的精度不确定,所以不能为0。
关于精度,在文章的最开始就说过了,最后一位数是不确定的,小数部分的最后一位可能是精确数字,也可能是近似数字(由四舍五入、向零舍入等不同方式得到);除此以外,剩余的31位都是精确数字。从二进制的角度看,这种定点格式的小数,最多有 32 位有效数字,但是能保证的是 31 位;也就是说,整体的精度为 31~32 位。
关于定点数存储的优缺点:小数部分用了16bit去存储,能保证精度的是15bit,优点就存储的小数精度很高,但是可以看到整数部分,只有16bit,基本上这个小数的最大值就取决于整数部分,16bit转换成十进制65536,可见取值范围就太小了。综合优缺点,于是就设计出了浮点数存储的方式
-
浮点数存储
浮点数是以指数的形式存储的,先看这张图
double和float类型所占的字节大小都是固定了的。
怎么存储的,先看这个数
把19.625转换成了二进制的小数指数形式,假设现在用float类型去存储。
符号位就是0。
尾数部分,因为转化成二进制后的指数形式,小数点前的那一位数字必定是1,所以可以省略不写(不同于十进制,十进制的指数,小数点前一位的取值范围是1-9),即尾数部分只需要把小数点后面的东西存储进去,那么尾数部分就是00111010000000000000000(23bits)。
指数部分,指数有正有负,不同于整数存储,还拿一位来做符号位,指数部分的存储方式是:
真正的指数加上指数存储范围一半的数值,再放入指数部分。
这句话怎么理解,就拿19.624这个数来说,转化成二进制后指数部分是4,这个就是真正的指数。指数存储范围的一半怎么找,float指数范围是8bit,存储范围就是0-255,一半的值就是127,即
所以指数部分存储的数就是127+4=131,转化成二进制1000 0011。同理double类型的指数也是真正的指数加上存储范围的中间值在放入指数部分的。
所以19.625这个数用float类型在内存空间中存储的数值就是0 10000011 00111010000000000000000。关于精度问题
其原因还是要参照文章开头第一段话。对于 float,尾数部分有 23 位,再加上一个隐含的整数 1,一共是 24 位。最后一位可能是精确数字,也可能是近似数字(由四舍五入、向零舍入等不同方式得到);除此以外,剩余的23位都是精确数字。从二进制的角度看,这种浮点格式的小数,最多有 24 位有效数字,但是能保证的是 23 位;也就是说,整体的精度为 23-24 位。如果转换成十进制,2^24 = 16 777 216,一共8位;也就是说,最多有 8 位有效数字,但是能保证的是 7 位,从而得出整体精度为 7~8 位,这也是为什么很多教材中强调float的精度为7位。
对于 double,同理可得,二进制形式的精度为 52-53 位,十进制形式的精度为15~16 位。
3. 用代码验证 float 的存储
前面我们分析的19.625这个数用float类型在内存空间中存储的数值就是0 10000011 00111010000000000000000。
下面来验证一下
#include <stdio.h>
#include <stdlib.h>
//浮点数结构体
typedef struct {
unsigned int tail : 23; //尾数部分
unsigned int exp : 8; //指数部分
unsigned int sign : 1; //符号位
} stu_float;
int main()
{
char str[25] = { 0 };
float f = 19.625;
stu_float *p = (stu_float*)&f;
itoa(p->sign, str, 2);
printf("sign: %s\n", str);
itoa(p->exp, str, 2);
printf("exp: %s\n", str);
itoa(p->tail, str, 2);
printf("tail: %s\n", str);
return 0;
}
输出的结果吻合分析。
这当中涉及到位域的知识,可以查看我的文章C/C++——位域的使用
itoa()是一个将整数数据转化成指定进制的字符串函数。