分数换算小数补0法_计算机存储整数和小数

9a9df1db5d87168e240ea9e3bc28e13e.png

前言

有两个问题一直让我疑惑,第一个计算机存储整数为什么用反码来表示?第二个js中0.1+0.2为什么等于0.30000000000000004,计算机小数是如何存储的?

计算机的原码反码以及补码

对于整数来说第一个代表符号位(0为正1为负),其余为表示数字

例如:对于一个8位的二进制来说可以表示范围是:11111111~01111111 = (-1)(1*2^6 + 1*2^5....+1*2^0) ~ (+1)(1*2^6 + 1*2^5....+1*2^0) = (-1)(64 + 32 + 16 + 8 + 4 + 2 + 1) ~ (+1)(64 + 32 + 16 + 8 + 4 + 2 + 1) = -127 ~ +127  在这之间一共有255个数,这也就是一字节能代表的数字最大值和最小值(对应c语言中uint8_t范围),同理可计算int代表四个字节32位时候的范围。

对于正数来说

原码 = 反码 = 补码

例如:我们以8位二进制1来说
原码【00000001】 = 反码【00000001】= 补码【00000001】

对于负数来说

反码 = 原码符号位不变其他位取反
补码 = 反码 + 1

例如:我们以8位二进制-1来说
原码【10000001】= 反码【11111110】= 补码【11111111】

为什么用补码

了解了整数原码反码以及补码计算后我们来分析为什么使用补码。
第一点:
对于0来说科学计数法中只有一个,也就是说+0和-0都是0实际是一个数。那么如果采用原码方式存的话+0 = 00000000,-0 = 10000000,很明显这实际上是一个数另种表示方式(00000000与10000000),这样的话计算机对于0就有了两种存储方式,这是不合理的因为0就有一个。那么如果使用反码来存储+0 = 01111111,-0 = 11111111很明显这对于计算机来说0还是两种存储方式,也是不对的。如果用补码来存储+0 = 0000000,-0 = 00000000,可以看出补码来存储0只有一种,所以计算机采用补码存储。

tips:
对于-0计算补码是这样的首先原码【10000000】,反码【11111111】,在反码基础+1产生进位得到100000000,最高位1是第九位,而我们表示时候是8位,所以1被舍弃就是00000000

第二点:
对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单。计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂, 于是人们想出了将符号位也参与运算的方法。我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了。
计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2 : 可以看出原码计算后很明显是错的  

1 - 1 = 1 + (-1) = [00000001]反 + [11111110]反 = [11111111]反 = [10000000]原 = -0 : 可以看出计算结果是-0但是对于计算机来说0就是0符号位是没有什么意义的,而且反码对于0还会出现00000000和10000000 两种格式,所以反码不能表示。  

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原 = 0 : 可以看出补码可以解决符号位参与运算以及0唯一的问题

计算机存储小数(float:单精度,double:双精度)

首先我们需要了解整数和小数是如何转成二进制的

对于整数转成二进制

算法:"除2取余,逆序排列"。

例如:10转成二进制:
10 / 2 = 5 --> 余数0
5 / 2 = 2 --> 余数1
2 / 2 = 1 --> 余数0
1 / 2 = 0 --> 余数1  
然后逆序排列就是:1010,所以对于10如果用一个字节也就是8位来存储就是:0000 1010

对于小数转成二进制

算法:"乘2取整,顺序排列"。

例如:0.1转成二进制:
0.1 * 2 = 0.2 --> 取整0
0.2 * 2 = 0.4 --> 取整0
0.4 * 2 = 0.8 --> 取整0
0.8 * 2 = 1.6 --> 取整1
0.6 * 2 = 1.2 --> 取整1
0.2 * 2 = 0.4 --> 取整0
0.4 * 2 = 0.8 --> 取整0
0.8 * 2 = 1.6 --> 取整1
...........  
然后顺序排列就是:0.00011001100110011001100110011001100110011001100110011001100110011001.....(这里就是说明小数如何转化成的二进制,计算机存储浮点数时候不是这样存储的)

分析

上面介绍了计算机存储整数,那么计算机如何存储小数的呢?弄清楚了如何存储小数就懂了为什么0.1+0.2 != 0.3了。
根据IEEE754(二进制浮点数算术标准) 浮点标准,任意一个二进制浮点数V可以表示成下面的形式:

V = (-1)^s × M × 2^E
  (1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

  (2)M表示有效数字,大于等于1,小于2。

  (3)2^E表示指数位。

IEEE 754规定,对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

7c4bfc159843230f37fb29c2bd08602c.png

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

898ca4cc2a39e89e7dcb19198cf20095.png


对于M:IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。这样做的目的是可以更加准确保留一位精度。,其中要注意的是,类似四舍五入,如果末位后是1会产生进位。

对于E:我们都知道E指数是有正(小数点左移)有负(小数点右移)的,所以IEEE 754规定,E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。如果一个E值是-4那么计算机真实存储就为1023 + (- 4) = 1019。

下面举例0.1如何在计算机存储的:

js是64位浮点数来存储的,对于0.1(0.000110011001100110011001...)的存储过程是这样的:首先为了保证M的范围需要将0.000110011...右移4位,这个时候M就是1.1001100....,而E就是-4: v = (-1)^0 * 1.1001100..... * 2^(-4) 所以s=0,M=1.1001100.....,E=-4,存储为:
[s]0[E]01111111011[M]1001100110011001100110011001100110011001100110011010
这里存储M的时候我们发现实际上只存储了52位,而0.1的二进制数远远比这个要多,这里就发生了精度的损失。精度丢失第二点是在M产生进位:1001100110011001100110011001100110011001100110011001(前52位) 100110011001.....(52位以后),我们知道M在计算的时候如果第53位如果是1会产生进位,因此产生进位后M为:1001100110011001100110011001100110011001100110011010,很明显这产生进位后的值要比真实值大。

小数二进制转回十进制

最后0.1的存储结果是:[s]0[E]01111111011[M]1001100110011001100110011001100110011001100110011010,那么转回十进制: 1. 补1(M舍弃了起始位的1,所以要补回来):1.1001100110011001100110011001100110011001100110011010 2. 计算E:E=-4,所以转回十进制左移4位,0.00011001100110011001100110011001100110011001100110011010。 3. 转为十进制: 02^(-1) + 02^(-2) + 02^(-3) + 12^(-4) + 12^(-5) + .....+ 02^(-52) 4. 结果:0.00011001100110011001100110011001100110011001100110011010 => 0.100000000000000005551(这里最终数参考网上计算结果,由于他是使用js计算的所以最终值也是不准的,但是基本可以验证0.1在转为二进制后再转成十进制数值已经发生了改变)

这里可以知道0.1在转成二进制后已经不是0.1了,精度产生了丢失,主要原因有两点,第一点小数转二进制时候位数是无限的而计算机表示是有限的(最高52位),第二点由于计算机表示位数是有限的而且还会产生进位导致精度丢失,所以在计算0.1+0.3的时候自然不会是0.4。

js安全问题

我们知道js是使用双精度(64位)来存储数字的,在浮点数64位中存储数字的位数是52位,那么可以表示最大的数范围就是(-)1111111111 1111111111 1111111111 1111111111 1111111111 11 ~ (+)1111111111 1111111111 1111111111 1111111111 1111111111 11 = -2^53 ~ 2^53 = (-)Math.pow(2, 53) ~ (+)Math.pow(2, 53) = -9007199254740992 ~ +9007199254740992
这里就存在安全性问题,如果浮点数超过了这个范围那么所表达的数就是一个。举个例子:

Math.pow(2, 53) === Math.pow(2, 53) + 1.0 // true

因此浮点数的安全数最大就是:9007199254740991。超过这个数就不安全了,因为都存在多个浮点数与之对应。
在业务中我们也会碰到如何后台java同学返回的是一个long类型的数字,而我们用浮点数去就接收可能就出现这种问题。

总结

  1. 我们了解了原码、反码、补码计算过程,以及计算机为什么使用补码来表示,主要原因是因为:第一点0在存储的时候只有补码能把+0和-0保存为一个值。第二点补码可以让符号位参与运算而且真值不变。
  2. 我们了解了浮点数在计算机存储过程,0.1在转为二进制后精度丢失,以及计算机为什么计算0.1+0.3!=0.4,主要是因为第一点小数转二进制时候位数是无限的而计算机表示是有限的(最高52位),第二点由于计算机表示位数是有限的而且还会产生进位导致精度丢失。
  3. 可以看出凡是采用IEEE754标准存储浮点数的都会产生精度丢失(c/c++语言都是采用这个标准)
  4. 浮点数表示和计算解决办法虽然不断有新的数据类型(例如js的BigInt)以及一些第三方库(math.js 、big.js )可以借鉴,但是个人感觉还是转成整数来表示和计算比较好,这样给计算机带来的计算量会小很多。

参考

http://www. ruanyifeng.com/blog/201 0/06/ieee_floating-point_representation.html https:// zh.wikipedia.org/wiki/I EEE_754 https:// juejin.im/post/5b372f10 6fb9a00e6714aa21 https://www. cnblogs.com/zhangziqiu/ archive/2011/03/30/ComputerCode.html https:// segmentfault.com/a/1190 000016586981
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值