接上一篇博客《为什么会有补码?》,我仅仅分析了整数在计算机中表示方式,但是计算机中的浮点数又是如何表示的呢?

引子

首先,使用之前博客的程序,可以看到如下的这些实数在计算机中的计算结果是

实际数值 数值类型 计算机中的表示
102.3235 float 42CCA5A2
-3.256 float C050624E
120.254 double 405E104189374BC7
-56.2441 double C04C1F3EAB367A10

右边的计算机中的数值表示是按照《IEEE 754-2008》的标准存储数据的,具体的规定如下所示。

IEEE 754 标准

表示形式

IEEE 754 规定了二进制浮点数在计算中的存储方式,我们以 C 语言中的 float 为例来具体说明。无论是 32 位系统还是 64 位系统,计算机中的 float 占用 4 个字节,我们就使用这些字节来存储任意的浮点数,可以参考下图

转化成对应的数学表示形式,浮点数 VV $$ V = (-1)^s\times M\times 2^E $$

  • ss 表示符号位,占据 1 个 bit 位,如果是负数则 s=1s=1,如果是非负数则 s=0s=0;
  • MM 表示 mantissa,占据 23 个比特,表示有效数字,表示的数字介于 1 和 2 之间;
  • EE 为非负整数,表示基于 2 为基数的指数大小,占据 8 个比特。

因此,如果确认了上面 3 个参数,也就唯一确定了浮点数在计算机中的存储形式。我们以102.3235为例,来看看上面的这几个数字是如何表示出来的?

  1. 102.3235的二进制原码形式是1100110.01010010110100001110 = 1.10011001010010110100001110*2^6
  2. 确认 ss。因为是正数,因此 s=0s=0。
  3. 确认 MM。MM 表示1.xxxxxx之后的xxxxxx的部分,即计算机内部保存 MM 时,默认表示的第一位总是 1,可以舍弃表示 1 的这一位,而仅仅存储小数点之后的部分。因此 M=10011001010010110100001110M=10011001010010110100001110,因为只能存储 23 个比特,将多余的位数部分截断得到 M=10011001010010110100001M=10011001010010110100001。
  4. 确认 EE。它是个肺腑正数,按照第 1 步计算出来的结果,我们的指数应该是 6。但是,IEEE 规定根据二进制计算浮点数时,需要给指数减去一个偏置值,对于 float 类型这个数为 127,对于 double 类型,这个数是 1023。因此反过来,在将数字转换成二进制存储的时候,要加上这个偏置值,因此 E=6+127=133E=6+127=133。
  5. 综合以上的所有计算结果,最后在计算机中存储的形式是01000010110011001010010110100001,转换成 16 进制就是42CCA5A2

特别规定

依照上面的方法,可以依次确认其他 3 个浮点数的表示形式。这都是比较常规的规格化数据的处理方法,IEEE 针对一些特殊的数字(绝对值特别接近 0 的数字或者无穷大无穷小),引入了一些特殊的规定,称为非规格化表示方法,总结如下。

  • 规格化数据。如果指数部分既不是 0 也不是 255(EE 不全为 0 或者不全为 1),就是规格化存储方式,具体的计算方法与之前介绍的相同。
  • 非规格化数据。EE 全为 0 就是非规格化的数据,此时的指数固定为 1-127 或者 1-1023,有效数字 MM 的计算不再舍弃第一位的 1,而是 0.xxxxxxxx 的小数部分,这样就可以表示 0 和非常接近 0 的小数字。需要注意的是,0 有两种表示。
  • 特殊数字。EE 全为 1 表示特殊的数字
    • 如果 MM 全为 0,表示无穷大,正负取决于符号 ss;
    • 如果 MM 不全为 0,表示这是一个非数 NaN(Not a Number)。

精度误差及表示范围

如果明白之前的内容,那就可以计算出来每种表示方法的取值范围以及表示的数据误差。在计算之前,有如下的一些假设:

  1. 正数和负数的表示方法是对称的,因此我们仅仅关注正数即可;
  2. 不考虑特殊值的计算,因为其仅仅是计算了不同几个数值,没有计算的价值。

如果需要计算最大的规格化数字,那么其在内存中的表示应该是 s=1,M=1.11…1,E=111…10s=1,M=1.11…1,E=111…10,最后的结果就是 1.1111…1×2127=3.4×10381.1111…1×2127=3.4×1038,其他的基本可以推测出来了,如下表所示

参考资料