介绍两种存储数值类型的方式:定点数和浮点数

1,269 阅读10分钟

不知觉间,2021 年已经过完了,这篇博客就当作是我今年的年终博客 :)

在计算机的领域中,有两种表示数值的方法,一种叫做定点数,另外一种是浮点数。我们今天首先来介绍定点数,然后再介绍浮点数(或许算是一句没有信息量的废话)。

相信大家可能或多或少听过浮点数的问题,表示的数据可能会不准。在像银行这样的金融领域,它们的系统可一点点数字的误差也不允许。所以他们往往采取定点数的方式存储他们的数据。定点数存储数据的时候就不会有误差了。

计算机是采用二进制存储数据的,为了将一个数存到计算机中,我们先要有一个对照表。

且看下面这张表:

十进制BCD 代码
00000
10001
20010
30011
40100
50101
60110
70111
81000
91001

上面的 BCD 代码是一种编码规则,我们把十进制转化为定点数的规则可以是对照着上面的表,把十进制数字一个一个的转。

假如有一个数字是 100.20。

通过查表,我们可以转为:

1    0    0    2    0
0001 0000 0000 0010 0000

在上面,小数点是不存储在这的,一般来说,使用定点数存数值的人,会在程序中额外的地方存它。怎么存储小数点,我们可以暂且不关心。

由于数字还有正负之分,所以,我们还要在最左边开辟四个位,标识符号位,和往常一样,1 就是整数,0 就是负数。上面那个 100.20 就会变成:

0001 0001 0000 0000 0010 0000

计算机中一个字节含有 8 个比特,即 8 个位。这样计算一下,上面的数据存储一共消耗了三个字节,虽然不如上面那种四个一组的直观,但是像下面这样更符合真实的情况:

00010001 00000000 00100000

定点数会根据系统的需要固定好存储的字节大小,也就是说,会固定好当前定点数用几个字节存储,一般来说会根据最大值和最小值来确定。

假设我们系统最大的数是 999.99,最小数为 -999.99,它可以用三个字节来表示当前系统内部的数。999.99 会表示为

00011001 10011001 10011001

在当前系统存储 -0.01 这个数字

00000000 00000000 00000001

从结果来看,-0.01 这个数浪费了两个半的字节。但是由于在银行呀这种地方,精度是大于一切的。并且,表示钱的数字的长度也不是长的无法想象。举一个更具象化的例子,假设一个系统最多能支撑的是 1 亿人民币,那每一个数需要多少个字节呢,其实也才 5 个字节。

    1    0   0    0   0    0   0    0   0
           
符号位
00010001 00000000 00000000 00000000 00000000

一般来说, int 型数据也是四个字节,长整形 long 要 8 个字节,表示双精度浮点数的 double 也要 8 个字节。

所以,定点数在这种对精度要求贼高但是数据长度又没有很长的场景很有效。

但放在平常使用就不是这样了,我们定义数的时候可能随意设置小数点的位置,可能想定义一 个随意大的数。

定点数不能随意设置小数的位置,而我们可不想定义一个二位小数的数据和定义三位小数的数据使用两个类型。

一门语言为了兼容所有使用者使用数的场景,如果使用定点数,它得为定点数预设多大的空间?难以想象。所以我们再继续在平常的场景下使用定点数可谓是困难重重。

数据存储方式一直在精度和内存两者之间权衡,更注重后者的存放方式就是浮点数了。

如果简单的概括浮点数是什么,就是用科学记数法格式的二进制数表示一个数。

在初中的时候,我们就已经学会怎么在十进制下使用科学技术法了,如果表示 1000,我们会记成:

1.001031.00 * 10 ^3

数字 1 后面带着两个 0,就代表是精度为小数点后两位(想起因为马虎大意忽略精度,被这道数学题支配的恐惧了吗,哈哈?)

1.00 也叫做有效数,它在十进制中的范围是大于等于 1 且小于 10 。

10 我们暂且叫做基(我忘记了,隐约记得叫做这个名词)

二进制下的科学记数法也是一样的逻辑,只不过,有效数的范围是大于等于二进制的 1 且小于 二进制的 10,也就是说,大于等于十进制的 1 且小于十进制的 2 。

而基也不是 10 了,而是 2。

对于一个二进制数 10001(十进制 17 ),我们可以表示为

1.0001241.0001 * 2^4

其实上面这种记法可能会让一些同学产生误解。

有些同学可能会疑惑,为什么 1.0001 * 16 结果是 16.016 不是 17 ?

大家可别指望上面那个式子的结果是 17,因为按照我们现在的思维计算是十进制的计算思维,二进制下乘法的进位规则和十进制不一样,也就是说按照十进制算的 1.0001 * 16 不是 17 很正常。

要想真正算上面的结果,我们需要也把 16 转为二进制,乘的过程也是满二进制的 10 进 1 。最后真正的结果是:

1.0001 * 10000 = 10001

再把上面的 10001 转为十进制才是 17。

在上面的二进制的科学记数法中,我们还可以推断出这样一条隐含的规则:二进制的有效数规定大于等于二进制的 1 且 小于二进制的 10,那也就是说有效数小数点左边肯定是 1 。这时候我们在计算机存储的时候可以不存储这一位,只考虑小数点部分就好了。

接下来,介绍浮点数。

浮点数就是按照上面二进制科学记数法的基本结构,还分为了两个基本格式:以 4 个字节表示的单精度浮点数,以 8 个字节表示的双精度浮点数。单精度所用的字节要少,相应的,所表示的精准数字的范围也比双精度的要小。

我们可以借由十进制体会一下这里精度的含义。

有一个数据格式规定有效位是两位,另一个数据格式规定有效位是4位,如果表示一个整数如 10,二者都可以很精准的表示,没有差别。

假设我们要表示 π。前者会用 3.14 表示,后者我们就可以用 3.1415 来表示。虽然二者都有误差,但是后者使用了更多的有效位,比前者更的精度要高。

上面也体现出了单精度和双精度的两个差别:双精度可表示的数字范围更大,对于一个肯定有误差的数据,双精度的误差会小。

接下来我们先来介绍单精度的格式:

上面说了,它有 4 个字节,也就是32 位比特,这四个字节的分配如下:

s=1位符号位 | e=8位指数(0~255) | f=23位有效数

它是如何用这三部分表示一个数的?

(1)s1.f2e127(-1)^s * 1.f * 2^{e-127}

继续那 10001 的例子,它就是:

(1)01.000125(-1)^0 * 1.0001 * 2^{5}

再来看一下双精度的格式,它有 8 个字节,64 个比特:

s=1位符号位 | e=11位指数(0~2047) | f=52位有效数

表示数的格式和单精度一致,只不过 f 的位数变多了,e 也变大了:

(1)s1.f2e1023(-1)^s * 1.f * 2^{e-1023}

我们再来依次介绍一下 s、e、f 对一个浮点数格式有什么影响。

s 就是符号位,它只有一位比特,取值只有 0 或 1。

当取 0, (1)s(-1)^s 结果为 1;取 1,结果为 -1,从而起到表示符号的作用。

e 为指数部分,直观的说,它控制了有效位的偏移量。就比如 1.0001251.0001 * 2^{5},就代表 1.0001 向右偏移 5 位。

f 为科学记数法格式里的有效位,它和精度的关系比较大。我们再直观的理解一下。

且拿单精度举例。

按上面单精度的公式,它可以表达最大的值是:

(1)s1.f2127(-1)^s * 1.f * 2^{127}

换算成 10 进制就是 3.40282346610383.402823466 * 10^{38}。这个数还是蛮大的,但是在它范围内的一个数 16777216 表示为二进制是:

1.000000000000000000000002241.00000000000000000000000 * 2^{24}

由于计算机中能表示的数不是连续的,是离散的,紧接着上面这个二进制,下一个二进制数是(即上一个二进制数变大最小刻度):

1.000000000000000000000012241.00000000000000000000001 * 2^{24}

这个数再转换成十进制其实是 16777218。(16777216, 16777218) 这个集合内所有的数据都不见了。事实上,这之间的数都被表示成了一个数,和 16777216 存储结构一样。也就是说,这个区间的数无法精准表示。

如上说的,也就是浮点数面临的精度问题。和单精度浮点数一样,双精度浮点数一样会有精度的问题,但是由于扩大了有效位,比起单精度浮点数,可以说的大大改善了。

不过比较有效位是有限的, 如果存储无理数或者长度超过有效位的数字的话,就会不准,比如 存储 0.1 ,它转换为二进制是一个无限循环小数:

0.0001 1001 1001 1001 1001 1001 ...

为了存储下来,只好截取前面那部分,但这样以来,存储下的数据就已经是不准确的,也不能指望做四则运算是准的。

最后,在 JavaScript 中,数值类型的表示采用的是浮点数的双精度格式存储。除了正常的数值,还有几个特殊的数值 NaN、Infinity。对比这下面这个公式看一下:

(1)s1.f2e1023(-1)^s * 1.f * 2^{e-1023}
  1. 如果 e == 2047 且 f != 0,就被表示为 NaN。
  2. 如果 e == 2047 且 f == 0,就被表示为无穷大或者无穷小(具体看符号位)

对于这些特殊的值,也不用关心它最终算出的结果,它只是一个记法。

如何在计算机中表示

PS: 关于这部分有疑问的同学,可以看这个视频教程。下面我就只是简单的介绍一下。

我们拿双精度浮点数来说。对于 3.5。它的科学记数法表示可以是:

(1)01.11210241023(-1)^0 * 1.11 * 2^{1024-1023}

其中,f 为 11,这个本身就是二进制了;e 为 1024,表示为二进制就是 10000000000 。

接下来就直接表示就行了,我刻意把符号位、e。 f分开写了:

0 10000000000 1100...{后面全是 0,省略}

为了方便起见,我再用十六进制表示一下:

40 0c 00 00 00 00 00 00

好了,这就是本文关于定点数、浮点数的全部内容了。

谢谢阅读,撒花~