浮点数(小数)在计算机中如何用二进制存储?

155 阅读6分钟

参考笔记三,P56.1、P57.2。

【版权声明】本文中引用的所有图片,来源于# Mekeater 的博客《浮点数(小数)在计算机中如何用二进制存储?》,感谢原作者的创作与分享,图片版权归原作者所有。如需使用该图片,请遵循原博主的版权声明。

注:为了讲述得更加严谨,本篇文章将使用一些二进制的相关概念,出自博文《二进制相关概念、运算与应用》。

在解析# Float类的源码时,我对MAX_VALUE/MIN_VALUE的值很好奇,它们是怎么得出的?于是利用我所知的二进制知识,尝试运算。一开工就发现没辙,因为我压根不知道浮点数的二进制是怎样表示、又是如何存储的。于是寻得一方案(序言【版权声明】中指定的博客)。

Mekeater 的阐述专业且详细,下面通过我的个人理解,尽量简明扼要地为大家阐明这个知识点。

正文

在开始之前,大家先看一张图。

在这里插入图片描述

float 是单精度,就是说 float 变量由32位(4字节)二进制表示。现在大家对这张图有所疑惑,无妨,往下看。

浮点数的二进制由三部分组成,与整数完全不同。换言之,给我们一组浮点数的二进制,我们无法直接看出它的真值是多少。因此,需要使用一种特殊的二进制数表现形式——浮点数二进制的十进制表现形式。

如何运算出浮点数二进制的十进制表现形式?

所谓十进制,就是带小数点的二进制数写法,也就是使用整数二进制的表现形式去表现浮点数。Mekeater 是这样说的:

二进制转换为十进制的方法就是各个位的数字与位权乘积之和。

无论整数、还是浮点数,都遵循这一规则。

什么是“位权”?这张图给出了答案。

在这里插入图片描述

就是底数的指数幂

答案很清楚了,计算浮点数小数部分的二进制的方法就是基于“位权乘积之和”进行逆运算。不过,不太方便。

从另一位博主那儿“取经”得一方法,使用上图示例。整数部分同样,是1011,将小数部分0.1875进行以下运算:

0.1875 * 2 = 0.37500
0.3750 * 2 = 0.75000
0.7500 * 2 = 1.50001
0.5000 * 2 = 1.00001

0011,这就是小数部分的十进制表现形式。

因此,最后得出11.1875的十进制表现形式是1011.0011

运算逻辑:

将小数部分乘以2,取结果的整数部分,如此反复,直至结果为0,最后依次得到的整数部分就是小数部分的二进制。

PS:暂未究其原理,实用。

补充一点

从运算逻辑可以推断,大部分有限浮点数转换成二进制,都是无限的,故进行其他计算的结果也必然得不到原浮点数。

Mekeater 将100个 float 类型的0.1相加,最终结果不是10.0

在这里插入图片描述

大家便可明了,无论采用哪种运算方法、无论单双精度,由于无法表示完全,必然有所缺失或增加(四舍五入),0.1都是无限浮点数,故是10.000002

得出了浮点数二进制的十进制表现形式,成功了一半。

浮点数(小数)在计算机中如何用二进制存储?

回到第一张图,浮点数的二进制由符号、指数、尾数三部分组成,这说明必然有一个公式,将这三部分进行运算,从而得到“真值”。

公式是这样的:

在这里插入图片描述

二进制中基数(又称“底数”)是2,不必考虑。浮点数内部构造的三部分正好与图中三个变量对应,下面我一一剖析。(以单精度为例)

符号部分占1位,即0/1

指数部分(8位)与尾数部分(23位)又是如何表示浮点数的?

我们来探讨一下,看到这个公式,给你11.1875这个浮点数,你能得出哪些等式?

11.1875=111.875101m111.875n10e111.1875 = 111.875 * 10^{-1},m是111.875,n是10,e是-1
11.1875=1.11875101m1.11875n10e111.1875 = 1.11875 * 10^1,m是1.11875,n是10,e是1
............

等式成立,但有没有问题?这里是二进制,n 是2,不是10,故等式应该改动一下:

11.1875=m121e111.1875 = m1* 2^{-1},e是-1
11.1875=m221e111.1875 = m2* 2^1,e是1
............

可是要满足这些等式,m 是多少?

看到这样的等式,大家是否似曾相识?没错,位运算,也就是这样:

11.1875=m121=m1>>111.1875 = m1* 2^{-1} = m1 >> 1
11.1875=m221=m<<111.1875 = m2* 2^1 = m << 1
............

明白了么?

可这里有个问题,因为位运算移动的位数e是任意的,故 m 任意,则必然存在一个规范,用于限制e的值。

这个规范就是“科学计数法”,所以等式只能是这样:

11.1875=1.11875101m1.11875e111.1875 = 1.11875 * 10^1,m是1.11875,e是1

PS:把这个等式拿过来是为了展示科学计数法,后续计算不用这个等式。

遵循科学计数法,e明确了,可m是多少?大家回到上文“浮点数二进制的十进制表现形式”那儿,我最后提了一句:“成功了一半”。成功在哪?

m2em*2^e

其实,尾数是经过浮点数二进制的十进制表现形式运算得出

11.1875为例:

11.18751011.0011=1.0110011<<3=1.01100112311.1875 → 1011.0011 = 1.0110011 << 3 = 1.0110011 * 2^3

这样,难道 m 是1.0110011?当然不是,还有一些步骤,Mekeater 已阐明:

在这里插入图片描述

因此,m 是01100110000000000000000。e 是3

对应到浮点数的内部构造,11.1875的二进制是:(Note:浮点数没有“补码”之说)

0 00000011 01100110000000000000000

这是正确答案吗?还不是。

指数部分还有点“门道”,其采用的是“无符号二进制”。

Mekeater 阐述:

指数部分使用了“EXCESS系统表现”(也称为“偏移值编码”)。

什么是“EXCESS系统表现”?Mekeater 已讲述得很清楚,我就不赘述了。

因此,最后得出11.1875的二进制是0 10000010 01100110000000000000000

反证:如何将0 10000010 01100110000000000000000转换成11.1875

难道按照上文所述方法进行“逆运算”?可行,但效率低。

mnem*n^e

之前说过,浮点数内部构造的三部分正好与公式中的三个变量对应,也就是这样:

11.1875=1.011001100000000000000002311.1875 = 1.01100110000000000000000 * 2^3

尾数目前是二进制(不要忘了“科学计数法”的那个1.),显然不能这么运算,需要先转换成十进制。

小数点前就不必多言,只能是1。所以,只需将小数点后这一串转换成十进制即可。这就很简单了,就是各个位的数字与位权乘积之和。

我偷个懒,写个 java 方法循环一下,就得出:

1.01100110000000000000000=1.39843751.01100110000000000000000 = 1.3984375

套进去就是:

11.1875=1.39843752311.1875 = 1.3984375 * 2^3

证明成立。

最后

  1. 如果采用双精度,同理,只是二进制位数增加了而已。
  2. Mekeater 运用 c++ 代码进行了验证,我把他提供的 code copy test 了一下,同样验证无误。我用 java 也手搓了一个工具类进行了验证,就不展示出来了,大家可自行造轮子。

至于Float.MAX_VALUE/Float.MIN_VALUE是如何获得的,暂未研究。

本文完结。