引入:整形和浮点型在内存中的存储
浮点数包括以下几个类型:float、double、long double
float类型大小为四个字节,double类型大小为八个字节,相应的,整型类型也有相似大小的类型,int类型大小就为四个字节,long long为八个字节。
值也类似:
类型 | int | float | long long | double |
值 | 3 | 3.0 | 4 | 4.0 |
整形和浮点数在值的表示上是有交集的,下面我们通过一个实验来对其内存存储形式进行深入探究:
先大胆提出假设“整形和浮点型在内存中的存储形式是不同的”。
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
分别以整形和浮点数将一份内存以不同的类型打印出来(浮点数初始化,和整形初始化情况下),如果打印出的结果不同则说明对于同一份内存,整形和浮点数的读取方式不同,由此又可以推出,对于3(3.0)这个值,存在内存中的格式也必定不相同。
代码运行——>
![](https://i-blog.csdnimg.cn/blog_migrate/68530faa641badd910e48476eca84012.png)
上半段代码所有语句都是围绕n所在内存的操作。
![](https://i-blog.csdnimg.cn/blog_migrate/fbd402fe6ea00dd89fc22ad2f72f7960.png)
打印出来的结果可以说是大相径庭,以int形式打印出来是9,可以float的形式打印出来甚至输出了0.000000这么个”古怪“的结果。
代码下半段,通过指针pFloat对n所在的内存空间进行了修改,使值9.0以浮点数的格式存入该内存空间。
![](https://i-blog.csdnimg.cn/blog_migrate/aaa293071a2a731728f88d5e17036893.png)
这次*pFloat的值正常了,但n的值却又”古怪“了起来,不过通过上下段代码的运行结果可以得出结论,“整形和浮点型在内存中的存储形式是不同的”这个假设是正确的。
主题:浮点型在内存中的存储
对于整形和浮点型在内存中的存储形式究竟有何区别,可以用printf以%p(十六进制)打印出来,直观的对比一下。
![](https://i-blog.csdnimg.cn/blog_migrate/e9995e52dbb491664bc326e558ee5af6.png)
十六进制转换为二进制数:
00000000 00000000 00000000 00001001 | 01000001 00010000 00000000 00000000 |
整形的二进制存储相信大家都很清楚,以整形存储的逻辑来看浮点型确实有种一团乱麻,摸不着头脑的感觉。
其实浮点型的存储是由国际标准IEEE(电气和电子工程协会) 754规定的,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位
即:二进制浮点数由S、E、M的三部分组成,二进制表示为“S+E+M”。
双精度:
![](https://i-blog.csdnimg.cn/blog_migrate/17bfc6739179fa882c09dca8f3e10aa2.png)
单精度:
![](https://i-blog.csdnimg.cn/blog_migrate/dd0e26959c72c27e0b44892957ae2ec1.png)
对于flaot型浮点数,S占1个比特位,E占8个比特位,M占23个比特位。
那么我们就可以对n转化的二进制数进行拆分,S‘0’,E‘1000001 0’,M‘0010000 00000000 00000000’,也就是S=0,E=130,M=4096,但如果带入公式“(-1)^S * M * 2^E”,发现结果为2^130*4096,怎么看也和9.0扯不上关系。
显然,简单带入的理解是错误的,重回定义,2^E表示指数位,我们不妨以指数的视角再来看待E。
1.E部分
要完全理解首先要知道什么是指数位,对于一个十进制数10086.0,可以表示为1.0086*10^4,其中4就是指数;对于一个二进制数浮点数1001.1(等十进制数9.5),可以表示为1.0011*2^3,其中指数为3。
接着往下看:
E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^1的E是1,所以保存成32位浮点数时,必须保存成1+127=128,即10000000;E是-1时,E=-1+127=126,即01111110;E是0时,E=0+127=127,即01111111。
4 | 3 | 2 | 1 | 0 | -1 | -2 | -3 | -4 |
10000011 | 10000010 | 10000001 | 10000000 | 01111111 | 01111110 | 01111101 | 01111100 | 01111011 |
取值范围为0~255,即E取值范围+127~-127,E全0全1单独讨论。
E全为0:这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
E全为1:这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
关于E全0全1单独讨论的原因最后再说。
2.M部分
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字,对于足够多的二进制位,这样的精度提升无疑是巨大的。
3.S部分
这个部分最简单,在公式“(-1)^S * M * 2^E”中,当S为1,(-1)^1=-1,公式计算出来的结果自然而然的成了负数;当S为0,(-1)^0=1,公式计算出来的结果自然而然的成了正数。
应用:尝试对内存中二进制浮点数数进行转化
知道了SEM三部分的含义,我们来尝试着重新对引入部分的例子进行解析,这里主要讨论浮点数部分,而为了方便这里也再写一遍代码。
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
首先还是上半部分:
![](https://i-blog.csdnimg.cn/blog_migrate/dc46526f4b41db4edbcdce4f529cc833.png)
”printf("n的值为:%d\n",n);“没什么好说的,重要的是”printf("*pFloat的值为:%f\n",*pFloat);“,%f就是要以浮点数的理解方式来理解n的值。
已知,n存储的32位二进制为:00000000 00000000 00000000 00001001。
首先将32位二进制位划分成SEM三部分,S部分”0“,E部分”0000000 0“,M部分”0000000 00000000 00001001“。
再代入公式,得(-1)^(1-127)*2^0*1.0000000 00000000 00001001,即1*9*9(之所以为9,是因为结果6翻了)
最后得出一个接近无穷小的浮点数,所以打印结果就该是0.000000。
再说下半部分:
![](https://i-blog.csdnimg.cn/blog_migrate/3b6ac74459021c4a9aaf221a8abab352.png)
对于直来直去的”printf("*pFloat的值为:%f\n",*pFloat);“不感兴趣,还是讨论重要的”printf("n的值为:%d\n",n);“。
1.首先,计算出浮点数的32位二进制表示。
2.S符号位,9.0为整数,S自然为0;
3.E指数位,9.0转成二进制为1001.0,即1.001,指数位E应该要等与3,实际保存为10000010 (3+127=130);
4.M有效数字(小数部分),1.001去掉前面的1有效数字是001,余下补0,即M部分为0010000 00000000 00000000。
最后组合起来,9.0 = 01000001 00010000 00000000 00000000。
将得到的二进制数以整形的理解方式转化为:1091567616。
结果不能说差不多,只能说一模一样。
补充:E全0全1讨论及无限小数
1.E全0全1讨论
在”应用“部分的上半部分代码讨论中就出现了全0的情况,
(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
可以说是非常小,乃至于无限接近于0,这种情况下将全为零设置为(1-127)可以说和本来结果大差不差了。(我是这么理解的)
对于全1的情况,是对无穷大的讨论,这点和全0的情况类似。
2.无限小数问题
5.5 二进制可以表示为101.1
5.625 二进制可以表示为101.11
那么,5.3呢?
5很容易表示,小数位的3就麻烦了,小数位的第一位表示2^(-1),即0.5;小数位的第二位表示2^(-2),即0.125;小数位的第三位表示2^(-3),即0.0625;小数位的第四位……
经过不断的尝试我们发现,无论加到多少位,始终不能表示5.3,只能无限接近与它。
#include <stdio.h>
int main()
{
float a = 5.3;
printf("%p", a);
return 0;
}
运行结果——>
![](https://i-blog.csdnimg.cn/blog_migrate/d5645478a30f44ba81100da81a795e58.png)
转化为三十二位二进制数:01000000 00000000 00000000 00000000
调试中&a的内存显示(小端存储)——>
![](https://i-blog.csdnimg.cn/blog_migrate/4a8e7ab058f2f8e0bf35d9bf4e2bd30e.png)
转化为三十二位二进制数:01000000 10101001 10011001 10011010
以%lf打印———>
![](https://i-blog.csdnimg.cn/blog_migrate/b8fac5b4ead11199e0188c58d168ec81.png)