为什么0.1+0.2不等于0.3

为什么负数要用补码表示

十进制数转二进制采用的是除 2 取余法,比如数字 8 转二进制的过程:

整数类型的数字在计算机的存储⽅式,就是将十进制的数字转换成二进制即可。

int类型的数字作为例,int类型是 32 位的,其中最⾼位是作为「符号标志位」,正数的符号位是 0 ,负数的符号位是 1 ,剩余的 31 位则表示二进制数据。

那么,对于int类型的数字 1 的二进制数表示如下:

而负数就比较特殊了点,负数在计算机中是以补码表示的,所谓的补码就是把正数的二进制全部取反再加 1,比如 -1 的二进制是把数字 1 的二进制取反后再加 1:

为什么计算机要用补码的⽅式来表示负数?在回答这个问题前,我们假设不用补码的⽅式来表示负数,而只是把最⾼位的符号标志位变为 1 表示负数,如下图过程:

如果采用这种⽅式来表示负数的二进制的话,试想⼀下-2 + 1的运算过程:

按道理,-2 + 1 = -1,但是上⾯的运算过程中得到结果却是 -3,可以发现,这种负数的表示⽅式是不能用常规的加法来计算了,需要特殊处理,要先判断数字是否为负数,如果是负数就要把加法操作变成减法操作才可以得到正确对结果。

到这⾥,我们就可以回答前⾯提到的「负数为什么要用补码⽅式来表示」的问题了。

如果负数不是使用补码的⽅式表示,则在做基本对加减法运算的时候,还需要多⼀步操作来判断是否为负数,如果为负数,还得把加法反转成减法,或者把减法反转成加法,这就⾮常不好了,毕竟加减法运算在计算机⾥是很常使用的,所以为了性能考虑,应该要尽量简化这个运算过程。

而用了补码的表示⽅式,对于负数的加减法操作,实际上是和正数加减法操作⼀样的。你可以看到下图,用补码表示的负数在运算-2+1过程的时候,其结果是正确的:

十进制小数与二进制的转换

整数十进制转二进制我们知道了,接下来看看小数是怎么转二进制的,小数部分的转换不同于整数部分,它采用的是乘 2 取整法,将十进制中的小数部分乘以 2 作为二进制的⼀位,然后继续取小数部分乘以 2 作为下⼀位,直到不存在小数为止。

以 8.625 转二进制作为例:

最后把「整数部分 + 小数部分」结合在⼀起后,其结果就是1000.101

但是,并不是所有小数都可以用二进制表示,前⾯提到的 0.625 小数是⼀个特例,刚好通过乘 2 取整法的⽅式完整的转换成二进制。

如果我们用相同的⽅式,来把 0.1 转换成二进制,过程如下:

可以发现, 0.1 的二进制表示是⽆限循环的。

由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用近似值来表示,就是在有限的精度情况下,最⼤化接近 0.1 的二进制数,于是就会造成精度缺失的情况。

对于二进制小数转十进制时,需要注意⼀点,小数点后⾯的指数幂是负数。

比如,二进制0.1转成十进制就是 2-1,也就是十进制 0.5 ,二进制 0.01 转成十进制就是2-2,也就是十进制 0.25,以此类推。

举个例⼦,二进制1010.101转十进制的过程,如下图:

计算机是怎么存小数的

1000.101这种二进制小数是「定点数」形式,代表着小数点是定死的,不能移动,如果你移动了它的小数点,这个数就变了,就不再是它原来的值了。

然而,计算机并不是这样存储小数的,计算机存储小数采用的是浮点数,名字⾥的「浮点」表示小数点是可以浮动的。

比如1000.101这个二进制数,可以表示成 1.000101x23,类似于数学上的科学记数法。

科学记数法在小数点左边只有⼀个数字,而且把这种整数部分没有前导 0 的数字称为规格化,比如 1.0x10-9 是规格化的科学记数法,而 0.1x10-9 和 10.0 x 10-9 就不是了。

因此,如果二进制要用到科学记数法,同时要规范化,那么不仅要保证基数为 2,还要保证小数点左侧只有 1 位,而且必须为 1。

所以通常将1000.101这种二进制数,规格化表示成 1.000101x23,其中,最为关键的是000101和 3 这两个东⻄,它就可以包含了这个二进制小数的所有信息:

  • 000101称为尾数,即小数点后⾯的数字;
  • 3 称为指数,指定了小数点在数据中的位置;

现在绝⼤多数计算机使用的浮点数,⼀般采用的是 IEEE 制定的国际标准,这种标准形式如下图:

这三个重要部分的意义如下:

  • 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
  • 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的⻓度越⻓则数值的表达范围就越⼤;
  • 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011x2-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,相加后就是指数位的值了,即指数位这 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.3 ?

并不是所有小数都可以用「完整」的二进制来表示的,比如十进制 0.1 在转换成二进制小数的时候,是⼀串⽆限循环的二进制数,计算机是⽆法表达⽆限循环的二进制数的,毕竟计算机的资源有限。

因此,计算机只能用近似值来表示该二进制,那么意味着计算机存放的小数可能不是⼀个真实值。现在基本都是用 IEEE 754 规范的单精度浮点类型或双精度浮点类型来存储小数的,根据精度的不同,近似值也会不同。

那计算机是存储 0.1 是⼀个怎么样的二进制浮点数呢?

可以看到,8 位指数部分是01111011,23 位的尾数部分是10011001100110011001101,可以看到尾数部分是0011是⼀直循环的,只不过尾数是有⻓度限制的,所以只会显示⼀部分,所以是⼀个近似值,精度十分有限。

接下来,我们看看 0.2 的float浮点数:

可以看到,8 位指数部分是01111100,稍微和 0.1 的指数不同,23 位的尾数部分是10011001100110011001101和 0.1 的尾数部分是相同的,也是⼀个近似值。

0.1 的二进制浮点数转换成十进制的结果是0.100000001490116119384765625

0.2 的二进制浮点数转换成十进制的结果是0.20000000298023223876953125

这两个结果相加就是0.300000004470348358154296875

所以,你会看到在计算机中0.1+0.2并不等于完整的 0.3。

这主要是因为有的小数⽆法可以用「完整」的二进制来表示,所以计算机⾥只能采用近似数的⽅式来保存,那两个近似数相加,得到的必然也是⼀个近似数。

我们在 JavaScript ⾥执⾏0.1+0.2,你会得到下⾯这个结果:

结果和我们前⾯推到的类似,因为 JavaScript 对于数字都是使用 IEEE 754 标准下的双精度浮点类型来存储的。

而我们二进制只能精准表达 2 除尽的数字 1/2, 1/4, 1/8,但是对于 0.1(1/10) 和 0.2(1/5),在二进制中都⽆法精准表示时,需要根据精度舍⼊。

我们⼈类熟悉的十进制运算系统,可以精准表达 2 和 5 除尽的数字,例如 1/2, 1/4, 1/5(0.2), 1/8,1/10(0.1)。

当然,十进制也有⽆法除尽的地⽅,例如 1/3, 1/7,也需要根据精度舍⼊。

总结

为什么负数要用补码表示?

负数之所以用补码的方式来表示,主要是为了统一和正数的加减法操作一样,毕竟数字的加减法是很常用的一个操作,就不要搞特殊化,尽量以统一的方式来运算。

十进制小数怎么转成二进制?

十进制整数转二进制使用的是「除 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,这主要是因为这两个小数无法用「完整」的二进制来表示,只能根据精度舍入,所以计算机里只能采用近似数的方式来保存,那两个近似数相加,得到的必然也是一个近似数。

打赏
  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信