四、浮点型在内存中的存储【更深,更强💪】
在第二模块,我有提到过一个叫做【浮点数家族】,里面包含了[float]
和[double]
类型,对于浮点数其实我们不陌生,在上小学的时候就有接触到的3.14
圆周率,还有以科学计数法表示的1E10
- 在计算机中整型类型的取值范围限定在:
limits.h
;浮点型类型的取值范围限定在:float.h
- 我们可以在【everything】中找找有关
float.h
这个头文件
- 然后把它拖到VS中来,便可以观察到它的这个头文件中所定义的内容,如果有兴趣可以自己去看看
1、案例引入
首先要了解浮点数在内存中的存储规则,我们要通过一个案例来进行引入。请问下面四个输出语句分别会打印什么内容?
int main() { int n = 9; float* pFloat = (float*)&n; printf("n的值为:%d\n", n); //1 printf("*pFloat的值为:%f\n", *pFloat); //2 *pFloat = 9.0; printf("num的值为:%d\n", n); //3 printf("*pFloat的值为:%f\n", *pFloat); //4 return 0; }
- 来简单分析一下,整型变量n里面存放了9,然后通过强制类型转换为浮点型的指针,存放到pFloat中。
- 首先第一个去打印n的值毫无疑问就是
9
- 第二个对pFloat进行解引用访问,对于
float
类型的指针与int一样都可以访问四个字节的地址,所以解引用便访问到了n中的内容,又因为浮点数小数点后仅6位有效,因此打印出来应该是9.000000
- 接下去通过pFloat的解引用修改了n的值,不过第三个以
%d
的形式进行打印,应该也还是9
- 第四个的话也是以浮点数的形式进行打印,那应该也是
9.000000
- 可结果真的和我们想象的一样吗,这就来看一下运行结果👇
- 可以看到,我们猜测推理的4个里面对了两个,中间的两个出了问题,而且还是两个很古怪的数字,对于
n
和*pFloat
在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
这要涉及到浮点数在内存中【存】与【取】规则,接下去我们首先来了解一下这个规则
2、浮点数存储规则
① 概念理清
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
V = (-1)^S * M * 2^E
(-1)^S
表示符号位,占一位。当S = 0
,V为正数;当S = 1
,V为负数- M【尾数】表示有效数字,放在最低部分,占用23位(1 <= M < 2)
- 2^E表示指数位。
E
为指数,占用8位【阶符采用隐含方式,即采用移码方法来表示正负指数】
② 例题分析
在讲解例题之前,你首先要知道对于一个二进制数来说,其整数部分和小数部分每一位上所占的权重分别是多少👈
- 对于【整数部分】我们非常熟悉,但是对于【小数部分】而言,你是否知道呢?
- 看完上了上面这个,我们就通过三道例题来进行讲解巩固
- v = 5.5
- 整数部分的5可以写成
101
,这毋庸置疑,但是这个小数部分的5要如何去进行转换呢?对于0.5
来说我们刚才看了小数部分的权重之后知道是2^-1^,所以直接使这一位为1即可 - 接着我们就要去求出S、M和E,对于M来说是>= 1并且< 2的,不过这里的
101.1
却远远大于1,所以我们可以通过在操作符中学习的【移位】操作将这个数进行左移两位,但是左移之后又要保持与原来的数相同,所以可以再乘上2^2^使得与原来的二进制相同。接着根据公式就可以写出(-1)^0^ * 1.011 * 2^2^这个式子,然后可以得出S、M、E为多少了
- v = -5.5
- 这个和上面的一样,就是前面加了一个负号,那只是符号位进行了修改的话我们只需要变动S就可以了,即
S == 1
- v = 9.5
- 如果知道了第一题如何计算的话这题也是同样的道理,只是整数部分发生了一个变化而已
- 可并不是所有数的小数位都是
0.5
,如果是0.25
的话你可以将【2^-2^】置为1,依次类推。。。可以对于下面这个3.3
里面的0.3
你会如何去凑数呢,首先0.5
肯定不能,那只能有0.25
,但若是再加上0.125
的话那就多出来了,那应该配几呢? - 其实你将后面的数一个个地去列出来就可以发现是没有数字可以配成
0.3
的。所以我们可以得出这个数字其实是无法在内存中进行保存。==这也是为什么浮点数在内存中容易发生精度丢失的原因==
③ 进一步探索指数E与尾数M的特性🔍
在上面,我们通过知晓了一些概念和案例,对浮点数首先有了一个基本的了解,其实呢对于浮点数来说是有一个统一标准来进行保存的
==IEEE 754标准规定:==
📚对于32位
的浮点数【float】,最高的1位是符号位S
,接着的8位是指数E
,剩下的23位为有效数字M
📚对于64位
的浮点数【double】,最高的1位是符号位S
,接着的11位是指数E
,剩下的52位为有效数字M
==IEEE 754对有效数字M和指数E,还有一些特别规定:==
首先是对于有效数字(尾数)M
- 前面说过
1 ≤ M < 2
,也就是说,M可以写成1.xxxxxx
的形式,其中xxxxxx表示小数部分 - IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是
1
,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01
,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。 - 以32位浮点数为例,留给M只有
22位
,但若是将第一位的1舍去以后,等于可以保存23位
有效数字,精度相当于又高了一位
==至于指数E,情况就比较复杂==
- 首先,E为一个无符号整数(
unsigned int
) - 那对于无符号整数来说,我们在上面有介绍过,如果E为8位,它的取值范围为
[0 - 255]
;如果E为11位,它的取值范围为[0 - 2047]
- 但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023
好,通过上面的叙述,相信你对浮点数如何放到内存中有了一个了解,接下去我们马上来练习巩固一下:computer:
- 简单一点,还是上面讲到过的
5.5
,注意如果定义成【float】类型的变量的话要在后面加上一个f
作为和【double】类型的区分
float f = 5.5f;
- 然后我们对
5.5
这个数字去进行分析它存入到内存中的样子。通过上面算出来的S = 0, M = 1.011, E = 2
去写出这32位浮点数存放到内存中是一个怎样的形式
- 对于符号数S来说就是把0存进去,占一位
- 对于指数E来说为2,32位浮点数要加上一个中间值127,所以要存入的十进制数为129,再将其转换为8位二进制即为
10000001
- 对于尾数M来说,需要舍去整数位1,然后将【小数部分】的
011
这三位放到内存中,但是规定了M为23位,此时我们只需要在后面补上20个0即可
- 然后便可以对这个32个比特位进行划分,8位一个字节,得出
40 b0 00 00
- 还有一点莫要忘了!还记得我们上面讲到的【大小端】存放吗?要存放到内存中的最后一步就是将其进行小端存放【这是我的机器】,即为
00 00 b0 40
- 到VS中的【内存】来观察一下👀
好,了解了如何将浮点数存放到内存中,先来我们来考虑一下如何将浮点数从内存中【读取】出来呢👈
==指数E从内存中取出还可以再分成三种情况:==
1. E不全为0或不全为1
- 对于这种情况就是最普通的,若是E存放在内存中的8位二进制数不全为0或者不全为1的话,那么直接按照上面说到过多一些M与E【写入内存】的规则进行一个逆推即可
- 以32位浮点数为例,因为我们在计算指数E的时候加上了一个
127
,那么此时减去127即可;在计算尾数M的时候舍去了整数部分的1,那次此时再补上这个1即可
2. E全为0
- 对于E全0的这种情况很特殊,也就是意味着8位二进制全为0即
00000000
,这个情况是在指数E加上127之后的结果,那么原先最初的指数是多少呢?那也只能是-127
了。那如果这个指数是-127的话也就相当于是【1.xxxx * 2^-127^】,是一个非常小的数字,几乎是和0没有什么差别 - 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值;对于尾数M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示
±0
,以及接近于0的很小的数字
3. E全为1
- 最后一种就是当E全为1的时候即
11111111
,这个情况也是在指数E加上127之后的结果,那么原先最初的指数是多少呢?那便能是128
了,那也只能是-127
了。那如果这个指数是-127的话也就相当于是【1.xxxx * 2^128^】,是一个非常大的数字 - 这时,如果尾数M全为0,表示±无穷大(正负取决于符号位s);
以上就是有关浮点数如何【写入】内存和从内存中【读取】的所有相关知识,你学会︿( ̄︶ ̄)︿了吗
3、开局疑难解答
讲了这么多 ,相信你也看烦了,还记得在本模块一开头我们遗留下来的那道题,现在通过学习了浮点数存与取的相关知识,我们再来做一下这道题
- 首先写出【n = 9】在内存中的补码
0 0000000 00000000 00000000 00001001
。然后是将这个n的地址存放到了一个浮点型的指针中去
int n = 9; float* pFloat = (float*)&n;
- 那么此时进行一个打印,以
%d
的形式打印n不用考虑就是9;但是后一个就不一样了,对浮点型的指针进行解引用,那也就是要将存放在内存中的浮点数进行读取出来
printf("n的值为:%d\n", n); printf("*pFloat的值为:%f\n", *pFloat);
- 那编译器此时就会站在浮点数的角度去看待这个
00000000000000000000000000001001
,将第一位看做是符号位0,即S == 0
,然后接下去就是8个比特位E,不过可以发现这个E是全0呀00000000
,就是我们上面所讲到的这种特殊情况 - 那此时后面的尾数M就不可以添上前面的整数部分1了,而是应该写成
0.xxxxxx
的形式即0.000000000000000000001001
。那对于指数E应该等于1-127为【-126】。所以最后写出来v的形式为
(-1)^0 * 0.000000000000000000001001 * 2^ (-126)
- 此时我们再去计算这个值打印的结果是多少,其实完全不需要计算,仔细观察就可以发现S为1,M为一个很小的数,若E再去乘上这个很小的数那只会更小,然后无限接近0,。那最后根据这个浮点数小数点后6位有效,便可以看出最终的结果为
0.000000
- 然后我们再来看下面的。此时pFloat进行解引用,然后将
9.0
存放到n这块地址中去,那也就相当于是我们最先学习了如何将一个浮点数存放到内存中去
*pFloat = 9.0;
- 那我们可以很快将其转换为二进制的形式
1001.0
,然后通过v的公式得出(-1)^0 * 1.001 * 2^3
- 此时再去将其转换为IEEE 754标准规定的32位浮点数。首先看到指数E为3,加上127之后为130,那么二进制形式即为
10000010
,尾数M也是同理,舍去1后看到001
,后面添上20个0补齐23个尾数位。最后的结果即为
——> 0 10000010 00100000000000000000000
- 然后去执行打印语句,那我们以浮点数的形式放进去,但是以
%d
的形式打印n,那么这一串二进制就会被编译器看做是补码,既然是打印就得是原码的形式,不过看到这个符号位为0,那我们也不需要去做一个转换,它就是原码
printf("num的值为:%d\n", n);
- 那么最后机器就会将二进制形式的原码转换为十进制的形式然后打印。一样,我们可以将它放到【程序员】计计算器进行运行,然后找到十进制的形式,便是最后打印输出在屏幕上的结果
01000001000100000000000000000000 —— 1,091,567,616
最后再来看一下运行结果
五、总结与提炼
最后来总结一下本文所学习的内容:book:
- 首先我们介绍了有关C语言中的所有数据类型,将他们总体地分成了五大类
- 接下去重点讨论了整型在内存中的存储。
- 首先提到了【原码】、【反码】、【补码】的概念,知道了原来在计算机内部都是以补码的形式进行存放
- 但是在内存中进行观察的时候却发生是倒着的,从而就引出了大小端的概念,知道了原来还有这样一种存放到内存中的方式。
- 接着我们讲到了有关有符号数和无符号数的数据范围,对于有符号数来说存在一个轮回,范围是- 2^n^ ~ 2^n^ - 1;对于无符号来说不存在负数,所以范围是0 ~ 2^n^ - 1。接着又通过一张对比图展示了原、反、补码三者的有效数据范围,对内存中数据的分布又进一步有了了解
- 最后我们通过七道非常经典的笔试题,再一次对以上所学的内容进行了一个巩固,将其运行到了实际中来
- 压轴的部分当然是留给最难的【浮点数】,相信认真看完的老铁应该可以感觉到浮点数在内存中的存储比整数不是难处一点半点,如果你没有静下心来研究过,真的遇到了很多稀奇古怪的数字连怎么去推导排查都不知道,规则这里就不再过多赘述,如果觉得理解起来困难的可以先放一放,在前面的所有知识都理解的基础上再去做深入探究
以上就是本文要介绍的所有内容,由衷得感谢您的阅读:rose::rose::rose: