6. 小数的二进制存储

免责声明:

我已经尽量简化了,由于观看本节内容导致的头晕、脱发、恶心、呕吐等生理症状,本人概不负责

在现实世界中,小数的书写方式非常自然,只需要使用.即可代表之后的数字全是小数,例如3.1415926

但是计算机存储小数的时候,麻烦就来了。

比如3.14,整数部分的二进制是11,小数部分的二进制是1011,合在一起就是111011,你能说这个数是3.14吗?

问题的根源就在它难以准确的表述小数点的位置。

因此,必须另寻他法。

浮点数基本概念

聪明的人想出了一个巧妙的办法

在现实世界中,任何数字都可以表示为a.xxxx10n,其中,a的取值范围是1~9

这叫做科学计数法

比如:

1024.5678=1.0245678103,它表示,1.0245678的小数点向右移动3位,即最终的进制小数

952.7=9.527102,它表示,9.527的小数点向右移动2位,即最终的进制小数

那二进制是否也可以这样表示呢?

当然可以,在二进制的世界中,任何数字(包括整数)都可以表示为a.xxxx2n,a只能取1

比如:

110.101=1.1010122,它表示,1.10101的小数点向右移动2位,即最终的进制小数

0010.00011=1.00001121,它表示,1.000011的小数点向右移动1位,即最终的进制小数

可以看出,二进制如果也使用科学计数法,以下东西都是固定的:

  • 底数2
  • 整数部分1

而不固定的部分是:

  • 指数部分
  • 尾数部分(小数点后面的部分)

因此,我们可以使用下面的方式来表示一个数字

第一部分第二部分第三部分
符号阶码尾数
0为正,1为负这部分表示指数部分这部分表示小数点后面的部分

这种表示数字的方法,叫做浮点数表示法

比如,110.101=1.1010122,改数字的符号是0,阶码是2,阶码的二进制格式是10,尾数是10101,因此,在计算机中可以用浮点数表示为:

符号阶码尾数
01010101

是不是很简单。

但这样一来,容易导致CPU搞不清楚阶码和尾数是在哪里分割的,我们可以轻松的从表格中看出,但计算机哪有什么表格,它是存在一起的:01010101

为了解决这个问题,阶码和尾数的长度就必须固定

比如,阶码的长度规定为3,尾数的长度规定为4,加上一个符号位,刚好是8位,一个字节

如果按照这种约定,计算机就可以轻松的取出第一个符号位,然后轻松的取出后三位阶码,继续取出后四位的尾数

符号(1)阶码(3)尾数(4)
00101010

可以看到,这种情况下,尾数的最后一位被丢弃了,从10101变成了1010,因为它只能存储4位。

所以,使用浮点数存储数字时,可能导致存储的数字不精确

以上,就是浮点数存储数字的方式。

数字到浮点数的转换

我们知道,二进制科学计数法是浮点数的基石,只要有了二进制的科学计数法,就可以变成浮点数的存储了。

然而,我们平时接触更多的是十进制的小数

现在的问题是:如何把十进制的小数转换为浮点数的科学计数法?

下面将一步一步进行分析

二进制小数到十进制小数

要理解十进制小数是如何转换成二进制小数的,就必须要先理解相反的情况:二进制小数是如何转换成十进制小数的。

我们知道,任何一个十进制的小数(包括整数)都可以书写为下面的格式:

21.25=2101+1100+2101+2102

二进制的小数也可以用同样的规则,只不过把底数10换成底数2

下面的示例就是把一个二进制小数11.01转换成了十进制小数3.25

11.012=121+120+021+122=3.2510

十进制小数到二进制小数

知道了二进制小数转十进制,反过来也是一样的

省略了具体的数学推导(数学好的朋友自行完成),我们按照下面的方式来转换

比如十进制数3.25

首先转换整数部分:

310=11

整数部分的转换在之前的章节已经说的很详细了,不再重复

然后转换小数部分

现有小数乘以2取整数部分
0.250.50
0.511
0不再处理不再处理

最终得到的二进制小数部分是01,即把每次取整部分从上到下依次罗列即可

0.2510=0.012

把最终的整数部分加入进去,就形成了

3.2510=11.012

无法精确转换

有的时候,这种转换是无法做到精确的

比如0.3这个十进制数,转换成二进制小数按照下面的过程进行

现有小数乘以2取整数部分
0.30.60
0.61.21
0.20.40
0.40.80
0.81.61
0.61.21
0.20.40
0.40.80
0.81.61
0.61.21
.........
0.310=0.01001100110011001...=0.01001

在转换的过程中,可能导致十进制的现有小数永远无法归零,于是转换成了一个无限的二进制小数。

同时,计算机无法存储一个无限的数据,因此,总有一些数据会被丢弃,这就造成了计算机存储的小数部分可能是不精确的

进一步,如果一个小数无法精确的存储,那么他们之间的运算结果也是不精确的

这就是计算机对小数的运算不精确的原因

js
// js语言中运行
5.3 - 5.2; // 得到0.09999999999999964

转换成二进制的科学计数

现在,按照以上所述的规则,我们已经可以轻松的把一个十进制的数字转换成二进制格式了

然后,我们再在它的基础上,把它变化为二进制的科学计数格式

3.2510=11.012=1.10121

注意,1.10121是二进制的科学计数表示,你并不能把它当成十进制的方式运算,如果你要将其转换成十进制,应该:

  1. 将1.101的小数点向右移动1位,得到11.01
  2. 11.01=1\*21+1\*20+0\*21+1\*22=3.25

当我们拿到这个数的二进制科学计数后,就可以轻松的将其存储下来了

3.25=11.01=1.10121=1.101021的存储

因为尾数是4位,所以不足在后面补0

符号(1)阶码(3)尾数(4)
00011010

然而

请允许我做一个悲伤的表情

还有一种情况没有考虑到...

指数偏移量

建议先读完本节内容,然后再反复推理和思考

我们来聊一聊还有什么情况没有考虑到

现在,我们有一个十进制的数字0.25,它转换成二进制的格式应该是0.01,科学计数法表示的结果是122,即小数点应该向左移动2位

现在的问题是,指数部分出现了负数!

注意,不是数字是负数,是指数是负数

问题在于,我难道对指数也要使用一个符号位来处理负数的情况吗?

实际上没有必要,在计算机运算浮点数时,对于指数部分,更多的操作是比较,即比较两个指数哪个大

如果使用符号位的话,在比较时就必须考虑符号的问题,这样会给比较带来很多麻烦

因此,IEEE 754规定,使用指数偏移量来处理这个问题

IEEE 754是对浮点数存储、运算的国际标准,绝大部分计算机语言的浮点数都遵循该标准

它规定,如果一个浮点数的指数位数为e,则它的指数偏移量为2e11,不管存储什么指数值,都需要加上这个偏移量后再进行存储。

比如,指数的位数是3,则指数的偏移量为2311=3,当存储指数-2时,需要加上偏移量3再进行存储,因此,指数-2实际上存储的是1,即001

再比如,当存储指数2时,需要加上偏移量3再进行存储,因此,指数2实际上存储的是5,即101

如果比较-22哪个大,就直接比较两个二进制即可,001显然比101要小,指数部分完全没有符号位,这样比较起来就轻松多了。

当然,当需要还原它的真实指数时,只需要减去偏移量即可

于是,有了这样的规则后:

0.2510=0.012=1.000022
符号(1)阶码(3)尾数(4)
00010000
3.2510=11.012=1.101021
符号(1)阶码(3)尾数(4)
01001010

由于有了偏移量的存在,浮点数的指数范围就可以很轻松的算出来了

最小能存储的指数是000,减去偏移量后真实的指数是-3

最大能存储的指数是111,减去偏移量后真实的指数是4

稍微的总结一下,就是:

阶码为n位的浮点数,指数的真实范围是2n1+12n1

特殊值

在浮点数的标准中,有下面两个特殊值:

  • NaN:Not a Number,表示不是一个数字,它通常来自一些错误的运算,比如 3.14 * "你好"
  • Infinity:正向的无穷大,相当于数学中的
  • -Infinity:负向的无穷大,相当于数学中的

为了表示这三个值,IEEE 754标准规定,当使用一种特殊的数字存储来表示:

NaN

符号(1)阶码(3)尾数(4)
无所谓全1非零

比如:

符号(1)阶码(3)尾数(4)
01111010

上面这个数字可不是1.101024,它是一个NaN

无穷

符号(1)阶码(3)尾数(4)
0:正无穷,1:负无穷111全0

比如:

符号(1)阶码(3)尾数(4)
01110000

上面这个数字可不是1.000024,它是一个Infinity

由于特殊值的存在,让阶码的最大值用于表示特殊值,因此,正常的数字阶码是不能取到最大值的

因此,正常数字的阶码取值范围少了一个:2n1+12n11

比如,3位的阶码,它能表示的正常指数范围是-33

单精度和双精度

很多计算机语言中都有单精度和双精度的概念,它们的区别在于阶码和尾数的位数是不一样的

Java的float是单精度浮点数,double是双精度浮点数

JS的所有数字均为双精度浮点数

类型符号阶码尾数共计
单精度1位8位23位32bit,4byte
双精度1位11位52位64bit,8byte

总结

由于浮点数这种特别的存储方式,因此,不同的阶码和尾数的位数,决定了:

  1. 阶码位数越多,可以取到的指数越大,因此可以表示的数字越大
  2. 尾数位数越多,可以表示的数字位数越多,因此可以表示的更加精确