浮点型的原理介绍及在内存中的存储形式
C语言提供了浮点数据类型,单精度浮点数float和双精度浮点数double。浮点数属于不精确的数据类型,本文将通过float类型的原理和在内存中的存储形式来介绍浮点型不精确的原因。以float类型为例,来展示C语言中浮点型的神秘之处。
float类型介绍
float是C语言的基本数据类型中的一种,表示单精度浮点数。C语言规定单精度浮点型在内存占用4个字节,精度为7位,取值范围为:3.4*10^-38 ~3.4*10^38或者-(3.4*10^-38 ~3.4*10^38)。依据IEEE规定 :float在存储中都分为三个部分:
1、符号位(Sign) : 0代表正,1代表为负
2、指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
3、尾数部分(Mantissa):尾数部分
其中float和double的存储方式如下图所示:
数据类型 | 符号位 | 指数 | 尾数 |
float | 1bit | 8bit | 23bit |
double | 1bit | 11bit | 52bit |
float在内存中的存储规则
计算机只能识别二进制,所以float类型在内存中也是通过二进制存储的。因此float类型需要转换为二进制形态,转换步骤如下:
1)先将这个实数的绝对值化为二进制格式。
2)将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。
3)从小数点右边第一位开始数出二十三位数字放入第22到第0位。
4)如果实数是正的,则在第31位放入“0”,否则放入“1”。
5)如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
6)如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。如果n是右移得到的或n=0,则将n化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位
注意:将绝对值转换为二进制之后,小数点左移n位,n即为该数的指数,指数为阶码为n+127。这其实就是第5和6步骤运算得到的结果。
示例:
通过上述转换规则计算两个浮点数的二进制。数字 13.6 和 数据 1.3
符号位比较简单,存储的是正数那么符号数就是0。如果是负数,则为1。
下面以13.6为例说明指数与尾数的表示方法。首先,我们取出13.6的整数部为13。对其使用短除法(对该数除以2,直至不能再除的一种方法)结果如下:
将各余数自下而上排列,则得到了13的二进制表示。之后,取出13.6的小数部分为0.6对其每次乘2取出整数留下小数,直至得到1。结果如下:
这样我们就得到了13.6的二进制表示。为1101.100110011001....... 之后我们需将小数点移动至整数部只有一位。移动后得到1.101100110011001....... 。在此我们将小数点移动了三位。因而三即是该数的指数。而阶码则为指数+127(加127是C语言的内在逻辑,在此我们并不深究。)因而我们得到了13.6的指数,为130。二进制表示10000010。最后的尾数,将原先得到的无线循坏的二进制取前23位即可,如果第24位为0直接舍弃,如果第24为1,则第23位加1。(float类型的尾数有23位,对于没有循环的数字,在后补齐0即可)。因此我们得到13.6的存储数据:
0 | 1000 0010 | 101 1001 1001 1001 1001 1010 | 1001......(第24位为1,所以尾数加1变成10,其他的舍弃) |
同理得到1.3在内存的存储数据如下:
0 | 0111 1111 | 010 0110 0110 0110 0110 0110 | 0110......(第24为0,后边的直接舍弃) |
接下来通过C程序调试,来查看浮点数在内存中的存储形式:
#include <stdio.h> int main() { float a = 13.6; printf("%p ", &a); printf("%.9f ", a); return 0; }
对上述程序进行调试,根据打印出变量a的地址在内存中找出该浮点数的存储形式:
可以看出变量a在内存中的存储形式为 41 59 99 9a。(小端存储:较高的有效字节存放在较高的存储器地址,较低的有效字节存放在较低的存储器地址)将上述十六进制转换为二进制 0100 0001010110011001100110011010 。该结果与上文中我们计算出的数据一致。对于数字 1.3 使用同样的方式验证。
从内存读取float类型
从上文已经知道了浮点型在内存中如何存储,那是如何读取呢,读取之后为什么会出现精度丢失的现象。本小节来演示下float类型的读取。规则如下:
1)将第22位到第0位的二进制数写出来,在最左边补一位“1”,得到二十四位有效数字。将小数点点在最左边那个“1”的右边。
2)取出第29到第23位所表示的值n。当30位是“0”时将n各位求反。当30位是“1”时将n增1。
3)将小数点左移n位(当30位是“0”时)或右移n位(当30位是“1”时),得到一个二进制表示的实数。
4)将这个二进制实数化为十进制,并根据第31位是“0”还是“1”加上正号或负号即可
注意:读取和存储是相反的过程,阶码也可以通过当前值 -127 来进行转换。
示例:
从上文中,我们已经从内存中获取到 13.6 的存储形式为 0100 0001 0101 1001 1001 1001 1001 1010 现在通过步骤进行该数据的读取:
1、获取尾数得到24位有效数字 1.101 1001 1001 1001 1001 1010
2、00 0001 0 值为2,第31位是1,所以将n+1。得到n为 00 0001 1
3、将小数点左移3位得到二进制实数 1101. 1001 1001 1001 1001 1010
4、将小数点前后分别计算十进制数字,1101转换为十进制为13。0. 1001 1001 1001 1001 1010 转为10进制数字如下所示:
得到最终实数为 13.6000003814697266。
所以当获取数字时,如果小数位的有效数字小于7位时,该数字是精确的,超过7位数据就出现了不精确位,所以说浮点型是不精确的数据类型。
同样的办法,我们可以获取到实数 1.3 经过转换后得到的实数为 1.2999999523162842
我们通过这两个例子可以得出结论,当读取数据时,根据需要获取的精度,之后的数据进行四舍五入之后舍弃。
结论:
经过对程序的调试,我们已经验证了浮点型不精确的原因以及float有效精度为6位。所以在使用浮点型时应特别注意精度丢失现象,同样的double类型有效精度为15位。计算方式和float类似,有兴趣的同学可以自行验证。
本文参考:https://blog.csdn.net/zcliatb/article/details/41078595