在上一篇博客中我们提到了整型在内存中的存储,而今天我们就来谈谈浮点型在内存中的存储。在这之前首先让我们来看一段代码,并猜测一下各自的输出:
#include <stdio.h>
int main()
{
int a = 5;
float* pf = (float*)&a;
printf("%d\n", a);//1
printf("%f\n", *pf);//2
*pf = 5.0;
printf("%d\n", a);//3
printf("%f\n", *pf);//4
return 0;
}
相信大部分人可能认为代码中编号1、2、3、4对应位置为5、5.000000、5、5.000000。但实际上这段代码的结果并非如此,以下是使用VS2022的运行结果:
可见,2与3的结果与我们的预期完全不同,这是为什么呢?通过观察原代码,我们可以发现main函数第二行将整型a的地址进行强制类型转换为了浮点型地址,并且编号3处将浮点数5.0以整型的格式打印,从而导致了2、3处的结果。以此,我们可以猜想浮点型在内存中的存储方式是否与整型不同?而事实正是如此,接下来我们便来看看浮点型在内存的存储方式。
浮点型的存储
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰为以下的形式:
其中(-1)^S是符号位,S=0时V表示正数,S=1时V表示负数;M是有效数字,M的值是大于1而小于2的;
2^E则是指数位。
就拿先前的十进制数5.0来举例,用二进制可以表示为101.0,而按上面的公式表示为:1.01*2^2。其中S为0,M其实为01(这里后面会说明),E为2。
而在内存中就是将S、M、E这三个数以二进制形式按顺序存储在内存中,从而实现对浮点型V的存储。以下分别是float与double类型在内存中的存储格式与大小(单位:bit):
float:
float类型的大小是4个字节也就是32个比特位,其中第一个比特位存放S,S后的8个比特位存放E,而剩下的23个比特位便是存放有效位M.
double:
double类型的大小是8个字节也就是64个比特位,与float不同的是,double类型的E占11个比特位,M占52比特位。
至于有效位M,前面说过1<=M<2,也就是说M始终可以表示为1.###,因此在IEEE754规定计算机在存储M时默认将M的首位定为1,所以计算机便只需要存储小数点后面的数位到M处,等到使用时再把1加上去,这样便可以节省空间来存储更大的数字(是不是很巧妙)。这就是之前例子中1.01仅存储01的原因。
至于指数E,首先E为一个无符号整型,这也就意味着当E占8个比特位时的取值范围为0-255,占16个比特位时的取值范围为0-2047,但我们知道科学计数法的指数位是可以为负数的。因此,IEEE754又规定,存入E的真实值时必须加上一个中间值,对于8个比特位的E这个中间值为127,16个比特位时为1027,对于上面的5.0的例子,E处存放的就是127+2=129的二进制形式,也就是:1000 0001。
如何指数E还存在2种极端的情况:
E全为0
此时,浮点数的E的真实值等于1-127(或1-1023),而这时的M不在加上首位的1,变成了0.#####,这样做是为了表示±0,以及接近0的很小的数。
E全为1
这时,若M全为1,可以表示正负无穷大(正负取决于S)。
最后让我们回到一开始的那段代码的例子,为了方便主要代码显示如下:
int a = 5;
float* pf = (float*)&a;
printf("%d\n", a);
printf("%f\n", *pf);
*pf = 5.0;
printf("%d\n", a);
printf("%f\n", *pf);
代码上部分将整型a以浮点型的形式存放,其二进制序列表示如下:
符号位的S为0,指数位E也全为0,有效位M为0000 0000 0000 0000 0000 101,用公式表示相当于:
V=0.0000 0000 0000 0000 0000 101*2^(-126)。显然V会时一个非常小且接近于0的数,因此用十进制的小数表示就是0.000000。
代码下半部分则将浮点数5.0以整型格式进行输出,其二进制序列的转变如下:
作为整型二进制表示就成为了将S、E、M三部分结合。可见,将浮点数5.0的二进制表示形式以整型的进行表示会是一个相当大的数字,而这个二进制序列原码的十进制便是前面运行结果3的数字。