6. 小数的二进制存储
免责声明:
我已经尽量简化了,由于观看本节内容导致的头晕、脱发、恶心、呕吐等生理症状,本人概不负责
在现实世界中,小数的书写方式非常自然,只需要使用.即可代表之后的数字全是小数,例如3.1415926
但是计算机存储小数的时候,麻烦就来了。
比如3.14,整数部分的二进制是11,小数部分的二进制是1011,合在一起就是111011,你能说这个数是3.14吗?
问题的根源就在它难以准确的表述小数点的位置。
因此,必须另寻他法。
浮点数基本概念
聪明的人想出了一个巧妙的办法
在现实世界中,任何数字都可以表示为
这叫做科学计数法
比如:
那二进制是否也可以这样表示呢?
当然可以,在二进制的世界中,任何数字(包括整数)都可以表示为
比如:
可以看出,二进制如果也使用科学计数法,以下东西都是固定的:
- 底数2
- 整数部分1
而不固定的部分是:
- 指数部分
- 尾数部分(小数点后面的部分)
因此,我们可以使用下面的方式来表示一个数字
| 第一部分 | 第二部分 | 第三部分 |
|---|---|---|
| 符号 | 阶码 | 尾数 |
| 0为正,1为负 | 这部分表示指数部分 | 这部分表示小数点后面的部分 |
这种表示数字的方法,叫做浮点数表示法
比如,0,阶码是2,阶码的二进制格式是10,尾数是10101,因此,在计算机中可以用浮点数表示为:
| 符号 | 阶码 | 尾数 |
|---|---|---|
| 0 | 10 | 10101 |
是不是很简单。
但这样一来,容易导致CPU搞不清楚阶码和尾数是在哪里分割的,我们可以轻松的从表格中看出,但计算机哪有什么表格,它是存在一起的:01010101
为了解决这个问题,阶码和尾数的长度就必须固定
比如,阶码的长度规定为3,尾数的长度规定为4,加上一个符号位,刚好是8位,一个字节
如果按照这种约定,计算机就可以轻松的取出第一个符号位,然后轻松的取出后三位阶码,继续取出后四位的尾数
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 010 | 1010 |
可以看到,这种情况下,尾数的最后一位被丢弃了,从10101变成了1010,因为它只能存储4位。
所以,使用浮点数存储数字时,可能导致存储的数字不精确
以上,就是浮点数存储数字的方式。
数字到浮点数的转换
我们知道,二进制科学计数法是浮点数的基石,只要有了二进制的科学计数法,就可以变成浮点数的存储了。
然而,我们平时接触更多的是十进制的小数
现在的问题是:如何把十进制的小数转换为浮点数的科学计数法?
下面将一步一步进行分析
二进制小数到十进制小数
要理解十进制小数是如何转换成二进制小数的,就必须要先理解相反的情况:二进制小数是如何转换成十进制小数的。
我们知道,任何一个十进制的小数(包括整数)都可以书写为下面的格式:
二进制的小数也可以用同样的规则,只不过把底数10换成底数2
下面的示例就是把一个二进制小数11.01转换成了十进制小数3.25:
十进制小数到二进制小数
知道了二进制小数转十进制,反过来也是一样的
省略了具体的数学推导(数学好的朋友自行完成),我们按照下面的方式来转换
比如十进制数3.25
首先转换整数部分:
整数部分的转换在之前的章节已经说的很详细了,不再重复
然后转换小数部分
| 现有小数 | 乘以2 | 取整数部分 |
|---|---|---|
| 0.25 | 0.5 | 0 |
| 0.5 | 1 | 1 |
| 0 | 不再处理 | 不再处理 |
最终得到的二进制小数部分是01,即把每次取整部分从上到下依次罗列即可
把最终的整数部分加入进去,就形成了
无法精确转换
有的时候,这种转换是无法做到精确的
比如0.3这个十进制数,转换成二进制小数按照下面的过程进行
| 现有小数 | 乘以2 | 取整数部分 |
|---|---|---|
| 0.3 | 0.6 | 0 |
| 0.6 | 1.2 | 1 |
| 0.2 | 0.4 | 0 |
| 0.4 | 0.8 | 0 |
| 0.8 | 1.6 | 1 |
| 0.6 | 1.2 | 1 |
| 0.2 | 0.4 | 0 |
| 0.4 | 0.8 | 0 |
| 0.8 | 1.6 | 1 |
| 0.6 | 1.2 | 1 |
| ... | ... | ... |
在转换的过程中,可能导致十进制的现有小数永远无法归零,于是转换成了一个无限的二进制小数。
同时,计算机无法存储一个无限的数据,因此,总有一些数据会被丢弃,这就造成了计算机存储的小数部分可能是不精确的
进一步,如果一个小数无法精确的存储,那么他们之间的运算结果也是不精确的
这就是计算机对小数的运算不精确的原因
// js语言中运行
5.3 - 5.2; // 得到0.09999999999999964转换成二进制的科学计数
现在,按照以上所述的规则,我们已经可以轻松的把一个十进制的数字转换成二进制格式了
然后,我们再在它的基础上,把它变化为二进制的科学计数格式
注意,
是二进制的科学计数表示,你并不能把它当成十进制的方式运算,如果你要将其转换成十进制,应该:
- 将1.101的小数点向右移动1位,得到11.01
当我们拿到这个数的二进制科学计数后,就可以轻松的将其存储下来了
因为尾数是4位,所以不足在后面补0
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 001 | 1010 |
然而
请允许我做一个悲伤的表情
还有一种情况没有考虑到...
指数偏移量
建议先读完本节内容,然后再反复推理和思考
我们来聊一聊还有什么情况没有考虑到
现在,我们有一个十进制的数字0.25,它转换成二进制的格式应该是0.01,科学计数法表示的结果是
现在的问题是,指数部分出现了负数!
注意,不是数字是负数,是指数是负数
问题在于,我难道对指数也要使用一个符号位来处理负数的情况吗?
实际上没有必要,在计算机运算浮点数时,对于指数部分,更多的操作是比较,即比较两个指数哪个大
如果使用符号位的话,在比较时就必须考虑符号的问题,这样会给比较带来很多麻烦
因此,IEEE 754规定,使用指数偏移量来处理这个问题
IEEE 754是对浮点数存储、运算的国际标准,绝大部分计算机语言的浮点数都遵循该标准
它规定,如果一个浮点数的指数位数为
比如,指数的位数是3,则指数的偏移量为-2时,需要加上偏移量3再进行存储,因此,指数-2实际上存储的是1,即001
再比如,当存储指数2时,需要加上偏移量3再进行存储,因此,指数2实际上存储的是5,即101
如果比较-2和2哪个大,就直接比较两个二进制即可,001显然比101要小,指数部分完全没有符号位,这样比较起来就轻松多了。
当然,当需要还原它的真实指数时,只需要减去偏移量即可
于是,有了这样的规则后:
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 001 | 0000 |
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 100 | 1010 |
由于有了偏移量的存在,浮点数的指数范围就可以很轻松的算出来了
最小能存储的指数是000,减去偏移量后真实的指数是-3
最大能存储的指数是111,减去偏移量后真实的指数是4
稍微的总结一下,就是:
阶码为n位的浮点数,指数的真实范围是
特殊值
在浮点数的标准中,有下面两个特殊值:
- NaN:Not a Number,表示不是一个数字,它通常来自一些错误的运算,比如
3.14 * "你好" - Infinity:正向的无穷大,相当于数学中的
- -Infinity:负向的无穷大,相当于数学中的
为了表示这三个值,IEEE 754标准规定,当使用一种特殊的数字存储来表示:
NaN
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 无所谓 | 全1 | 非零 |
比如:
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 111 | 1010 |
上面这个数字可不是NaN
无穷
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0:正无穷,1:负无穷 | 111 | 全0 |
比如:
| 符号(1) | 阶码(3) | 尾数(4) |
|---|---|---|
| 0 | 111 | 0000 |
上面这个数字可不是Infinity
由于特殊值的存在,让阶码的最大值用于表示特殊值,因此,正常的数字阶码是不能取到最大值的
因此,正常数字的阶码取值范围少了一个:
比如,3位的阶码,它能表示的正常指数范围是-3到3
单精度和双精度
很多计算机语言中都有单精度和双精度的概念,它们的区别在于阶码和尾数的位数是不一样的
Java的float是单精度浮点数,double是双精度浮点数
JS的所有数字均为双精度浮点数
| 类型 | 符号 | 阶码 | 尾数 | 共计 |
|---|---|---|---|---|
| 单精度 | 1位 | 8位 | 23位 | 32bit,4byte |
| 双精度 | 1位 | 11位 | 52位 | 64bit,8byte |
总结
由于浮点数这种特别的存储方式,因此,不同的阶码和尾数的位数,决定了:
- 阶码位数越多,可以取到的指数越大,因此可以表示的数字越大
- 尾数位数越多,可以表示的数字位数越多,因此可以表示的更加精确