您的位置 首页 java

javascript小数的二进制存储

免责声明:

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

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

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

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

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

因此,必须另寻他法。

浮点数 基本概念

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

在现实世界中,任何数字都可以表示为a.xxxx * 10^na.xxxx∗10n,其中,a的取值范围是1~9

这叫做科学计数法

比如:

1024.5678 = 1.0245678 * 10^31024.5678=1.0245678∗103,它表示,1.0245678的小数点向右移动3位,即最终的十进制小数

952.7 = 9.527 * 10^2952.7=9.527∗102,它表示,9.527的小数点向右移动2位,即最终的十进制小数

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

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

比如:

110.101 = 1.10101 * 2^2110.101=1.10101∗22,它表示,1.10101的小数点向右移动2位,即最终的二进制小数

0010.00011 = 1.000011 * 2^10010.00011=1.000011∗21,它表示,1.000011的小数点向右移动1位,即最终

的二进制小数

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

底数2

整数部分1

而不固定的部分是:

指数部分

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

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

第一部分第二部分第三部分

符号 阶码 尾数

0为正,1为负 这部分表示指数部分 这部分表示小数点后面的部分

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

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

符号阶码尾数

0 10 10101

是不是很简单。

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

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

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

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

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

0 010 1010

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

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

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

数字到浮点数的转换

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

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

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

下面将一步一步进行分析

二进制小数到十进制小数

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

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

21.25 = 2 * 10^1 + 1 * 10^0 + 2 * 10^{-1} + 2 * 10^{-2}21.25=2∗101+1∗100+2∗10−1+2∗10−2

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

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

11.01_2 = 1 * 2^1 + 1 * 2^0 + 0 * 2^{-1} + 1 * 2^{-2} = 3.25_{10}11.012=1∗21+1∗20+0∗2−1+1∗2−2=3.2510

十进制小数到二进制小数

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

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

比如十进制数3.25

首先转换整数部分:

3_{10} = 11310=11

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

然后转换小数部分

现有小数乘以2取整数部分

0.25 0.5 0

0.5 1 1

0 不再处理 不再处理

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

0.25_{10} = 0.01_20.2510=0.012

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

3.25_{10} = 11.01_{2}3.2510=11.012

无法精确转换

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

比如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

… … …

0.3_{10} = 0.0 1001 1001 1001 1001 … = 0.0\overline{1001}0.310=0.01001100110011001…=0.01

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

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

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

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

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

转换成二进制的科学计数

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

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

3.25_{10} = 11.01_2 = 1.101 * 2^13.2510=11.012=1.101∗21

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

将1.101的小数点向右移动1位,得到11.01

$11.01 = 1

2^1 + 1

2^0 + 0

2^{-1} + 1

2^{-2} = 3.25$

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

3.25 = 11.01 = 1.101 * 2^1 = 1.1010 * 2^13.25=11.01=1.101∗21=1.1010∗21的存储

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

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

0 001 1010

然而

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

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

指数偏移量

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

0.25_{10} = 0.01_2 = 1.0000 * 2^{-2}0.2510=0.012=1.0000∗2−2

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

0 001 0000

3.25_{10} = 11.01_2 = 1.1010 * 2^13.2510=11.012=1.1010∗21

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

0 100 1010

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

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

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

稍微的总结一下,就是:

阶码为n位的 浮点 数,指数的真实范围是-2^{n-1}+1−2n−1+1 到 2^{n-1}2n−1

特殊值

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

NaN :Not a Number,表示不是一个数字,它通常来自一些错误的运算,比如

3.14 * “你好”

Infinity:正向的无穷大,相当于数学中的

\infty

-Infinity:负向的无穷大,相当于数学中的

-\infty

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

NaN

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

无所谓

比如:

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

0 111 1010

上面这个数字可不是1.1010*2^{4}1.1010∗24,它是一个NaN

无穷

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

0:正无穷,1:负无穷 111

比如:

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

0 111 0000

上面这个数字可不是1.0000*2^{4}1.0000∗24,它是一个Infinity

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

因此,正常数字的阶码取值范围少了一个:-2^{n-1}+1−2n−1+1 到 2^{n-1} – 12n−1−1

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

单精度和双精度

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

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

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

类型符号阶码尾数共计

单精度 1位 8位 23位 32bit,4 byte

双精度 1位 11位 52位 64bit,8byte

总结

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

阶码位数越多,可以取到的指数越大,因此可以表示的数字越大

尾数位数越多,可以表示的数字位数越多,因此可以表示的更加精确

文章来源:智云一二三科技

文章标题:javascript小数的二进制存储

文章地址:https://www.zhihuclub.com/186775.shtml

关于作者: 智云科技

热门文章

网站地图