参考笔记三,P56.1、P57.2。
【版权声明】本文中引用的所有图片,来源于# Mekeater 的博客《浮点数(小数)在计算机中如何用二进制存储?》,感谢原作者的创作与分享,图片版权归原作者所有。如需使用该图片,请遵循原博主的版权声明。
注:为了讲述得更加严谨,本篇文章将使用一些二进制的相关概念,出自博文《二进制相关概念、运算与应用》。
在解析# Float类的源码时,我对MAX_VALUE/MIN_VALUE的值很好奇,它们是怎么得出的?于是利用我所知的二进制知识,尝试运算。一开工就发现没辙,因为我压根不知道浮点数的二进制是怎样表示、又是如何存储的。于是寻得一方案(序言【版权声明】中指定的博客)。
Mekeater 的阐述专业且详细,下面通过我的个人理解,尽量简明扼要地为大家阐明这个知识点。
正文
在开始之前,大家先看一张图。

float 是单精度,就是说 float 变量由32位(4字节)二进制表示。现在大家对这张图有所疑惑,无妨,往下看。
浮点数的二进制由三部分组成,与整数完全不同。换言之,给我们一组浮点数的二进制,我们无法直接看出它的真值是多少。因此,需要使用一种特殊的二进制数表现形式——浮点数二进制的十进制表现形式。
如何运算出浮点数二进制的十进制表现形式?
所谓十进制,就是带小数点的二进制数写法,也就是使用整数二进制的表现形式去表现浮点数。Mekeater 是这样说的:
二进制转换为十进制的方法就是各个位的数字与位权乘积之和。
无论整数、还是浮点数,都遵循这一规则。
什么是“位权”?这张图给出了答案。

就是底数的指数幂。
答案很清楚了,计算浮点数小数部分的二进制的方法就是基于“位权乘积之和”进行逆运算。不过,不太方便。
从另一位博主那儿“取经”得一方法,使用上图示例。整数部分同样,是1011,将小数部分0.1875进行以下运算:
0.1875 * 2 = 0.3750 → 0
0.3750 * 2 = 0.7500 → 0
0.7500 * 2 = 1.5000 → 1
0.5000 * 2 = 1.0000 → 1
得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这个浮点数,你能得出哪些等式?
等式成立,但有没有问题?这里是二进制,n 是2,不是10,故等式应该改动一下:
可是要满足这些等式,m 是多少?
看到这样的等式,大家是否似曾相识?没错,位运算,也就是这样:
明白了么?
可这里有个问题,因为位运算移动的位数e是任意的,故 m 任意,则必然存在一个规范,用于限制e的值。
这个规范就是“科学计数法”,所以等式只能是这样:
PS:把这个等式拿过来是为了展示科学计数法,后续计算不用这个等式。
遵循科学计数法,e明确了,可m是多少?大家回到上文“浮点数二进制的十进制表现形式”那儿,我最后提了一句:“成功了一半”。成功在哪?
其实,尾数是经过浮点数二进制的十进制表现形式运算得出。
以11.1875为例:
这样,难道 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?
难道按照上文所述方法进行“逆运算”?可行,但效率低。
之前说过,浮点数内部构造的三部分正好与公式中的三个变量对应,也就是这样:
尾数目前是二进制(不要忘了“科学计数法”的那个1.),显然不能这么运算,需要先转换成十进制。
小数点前就不必多言,只能是1。所以,只需将小数点后这一串转换成十进制即可。这就很简单了,就是各个位的数字与位权乘积之和。
我偷个懒,写个 java 方法循环一下,就得出:
套进去就是:
证明成立。
最后
- 如果采用双精度,同理,只是二进制位数增加了而已。
- Mekeater 运用 c++ 代码进行了验证,我把他提供的 code copy test 了一下,同样验证无误。我用 java 也手搓了一个工具类进行了验证,就不展示出来了,大家可自行造轮子。
至于Float.MAX_VALUE/Float.MIN_VALUE是如何获得的,暂未研究。
本文完结。