为什么0.1+0.2≠0.3
为什么0.1+0.2≠0.3
dleei在计算机中大家都知道数据都是以二进制的形式进行存储的,所有的数字都被转换成了一串数字0和1
十进制转二进制
十进制转换二进制的方法相信大家都熟能生巧了,如果你说你还不知道,我觉得你还是太谦虚,可能你只是忘记了,即使你真的忘记了,不怕,我们一起来回忆一下。
对于大于1的十进制采用的是除2取余,比如数字8转二进制过程如下图:
而对于小于1十进制小数转为二进制则采用的是乘2取整法,将十进制中的小数部分乘以 2 作为二进制的一位,然后继续取小数部分乘以 2 作为下一位,直到不存在小数为止
如果我们用相同的方式,来把 0.1
转换成二进制,过程如下:
可以发现,0.1 的二进制表示是无限循环的。
由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况。
计算机是如何存储二进制小数的?
计算机存储小数的采用的是浮点数,通俗的理解「浮点」表示小数点是可以浮动的
比如 1000.101
这个二进制数,可以表示成 1.000101 x 2^3
,类似于数学上的科学记数法
可能有的同学距离初中时期已经非常久远了,可能已经忘了什么是科学记数法,我们来复习一下
比如有个很大的十进制数 1230000,我们可以也可以表示成 1.23 x 10^6
,这种方式就称为科学记数法
该方法在小数点左边只有一个数字,而且把这种整数部分没有前导 0 的数字称为规格化,比如 1.0 x 10^(-9)
是规格化的科学记数法,而 0.1 x 10^(-9)
和 10.0 x 10^(-9)
就不是了
因此,如果二进制要用到科学记数法,同时要规范化,那么不仅要保证基数为 2,还要保证小数点左侧只有 1 位,而且必须为 1。
所以通常将 1000.101
这种二进制数,规格化表示成 1.000101 x 2^3
,其中,最为关键的是 000101 和 3 这两个东西,它就可以包含了这个二进制小数的所有信息:
000101
称为尾数,即小数点后面的数字;3
称为指数,指定了小数点在数据中的位置;
现在绝大多数计算机使用的浮点数,一般采用的是 IEEE 制定的国际标准,这种标准形式如下图:
这三个重要部分的意义如下:
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;
用 32
位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float
变量,而用 64
位来表示的浮点数,称为双精度浮点数,也就是 double
变量,它们的结构如下:
- double 的尾数部分是 52 位,float 的尾数部分是 23 位,由于同时都带有一个固定隐含位(这个后面会说),所以 double 有 53 个二进制有效位,float 有 24 个二进制有效位,所以所以它们的精度在十进制中分别是
log10(2^53)
约等于15.95
和log10(2^24)
约等于7.22
位,因此 double 的有效数字是15~16
位,float 的有效数字是7~8
位,这些有效位是包含整数部分和小数部分; - double 的指数部分是 11 位,而 float 的指数位是 8 位,意味着 double 相比 float 能表示更大的数值范围;
那二进制小数,是如何转换成二进制浮点数的呢?
我们就以 10.625
作为例子,看看这个数字在 float 里是如何存储的
首先,我们计算出 10.625 的二进制小数为 1010.101。
然后把小数点,移动到第一个有效数字后面,即将 1010.101 右移 3
位成 1.010101
,右移 3 位就代表 +3,左移 3 位就是 -3。
float 中的「指数位」就跟这里移动的位数有关系,把移动的位数再加上「偏移量」,float 的话偏移量是 127,加3后就是指数位的值了,即指数位这 8 位存的是 10000010
(十进制 130),因此你可以认为「指数位」相当于指明了小数点在数据中的位置。
1.010101
这个数的小数点右侧的数字就是 float 里的「尾数位」,由于尾数位是 23 位,则后面要补充 0,所以最终尾数位存储的数字是 01010100000000000000000
。
在算指数的时候,你可能会有疑问为什么要加上偏移量呢?
前面也提到,指数可能是正数,也可能是负数,即指数是有符号的整数,而有符号整数的计算是比无符号整数麻烦的,所以为了减少不必要的麻烦,在实际存储指数的时候,需要把指数转换成无符号整数。
float 的指数部分是 8 位,IEEE 标准规定单精度浮点的指数取值范围是 -126 ~ +127
,于是为了把指数转换成无符号整数,就要加个偏移量,比如 float 的指数偏移量是 127
,这样指数就不会出现负数了。
比如,指数如果是 8,则实际存储的指数是 8 + 127(偏移量)= 135,即把 135 转换为二进制之后再存储,而当我们需要计算实际的十进制数的时候,再把指数减去「偏移量」即可。
细心的朋友肯定发现,移动后的小数点左侧的有效位(即 1)消失了,它并没有存储到 float 里。
这是因为 IEEE 标准规定,二进制浮点数的小数点左侧只能有 1 位,并且还只能是 1,既然这一位永远都是 1,那就可以不用存起来了。
于是就让 23 位尾数只存储小数部分,然后在计算时会自动把这个 1 加上,这样就可以节约 1 位的空间,尾数就能多存一位小数,相应的精度就更高了一点。
那么,对于我们在从 float 的二进制浮点数转换成十进制时,要考虑到这个隐含的 1,转换公式如下:
举个例子,我们把下图这个 float 的数据转换成十进制,过程如下:
搞清楚了计算机是如何存储小数的我们就来看看0.1和0.2转为二进制到底是多少?
0.1 的二进制浮点数转换成十进制的结果是 0.100000001490116119384765625
:
0.2 的二进制浮点数转换成十进制的结果是 0.20000000298023223876953125
:
这两个结果相加就是 0.300000004470348358154296875
:
!]
所以,你会看到在计算机中 0.1 + 0.2 并不等于完整的 0.3。
这主要是因为有的小数无法可以用「完整」的二进制来表示,所以计算机里只能采用近似数的方式来保存,那两个近似数相加,得到的必然也是一个近似数。
我们在 JavaScript 里执行 0.1 + 0.2,你会得到下面这个结果:
1 | console.log(0.1 + 0.2 == 0.3) //false |
我们检查 0.1
和 0.2
的总和是否为 0.3
,我们会得到 false
我擦!想象一下,你创建了一个电子购物网站,如果访问者将价格为 ¥ 0.10
和 ¥ 0.20
的商品放入了他的购物车。订单总额将是 ¥ 0.30000000000000004
。这会让任何人感到惊讶
解决方案
最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
1 | let sum = 0.1 + 0.2; |
请注意,toFixed
总是返回一个字符串。它确保小数点后有 2 位数字。如果我们有一个电子购物网站,并需要显示 ¥ 0.30
,这实际上很方便。对于其他情况,我们可以使用一元加号将其强制转换为一个数字:
1 | let sum = 0.1 + 0.2; |
我们可以将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。当我们使用整数进行数学运算时,误差会有所减少,但仍然可以在除法中得到:
1 | alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 |
因此,乘/除法可以减少误差,但不能完全消除误差
总结
十进制小数怎么转成二进制?
十进制整数转二进制使用的是「除 2 取余法」,十进制小数使用的是「乘 2 取整法」。
计算机是怎么存小数的?
计算机是以浮点数的形式存储小数的,大多数计算机都是 IEEE 754 标准定义的浮点数格式,包含三个部分:
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;
用 32 位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float 变量,而用 64 位来表示的浮点数,称为双精度浮点数,也就是 double 变量。
0.1 + 0.2 == 0.3 吗?
不是的,0.1 和 0.2 这两个数字用二进制表达会是一个一直循环的二进制数,比如 0.1 的二进制表示为 0.0 0011 0011 0011… (以0011为循环节无限循环),对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
因此,IEEE 754 标准定义的浮点数只能根据精度舍入,然后用「近似值」来表示该二进制,那么意味着计算机存放的小数可能不是一个真实值。
0.1 + 0.2 并不等于完整的 0.3,这主要是因为这两个小数无法用「完整」的二进制来表示,只能根据精度舍入,所以计算机里只能采用近似数的方式来保存,那两个近似数相加,得到的必然也是一个近似数。