计算机是怎么存储数字的

0.096字数 3022阅读 3578

从一个最简单的例子开始吧。

我们来做几个小学生的加法题。8 + 9 等于多少?0.8 + 0.9 等于多少?在 Chrome 里面简单用 Javascript 这门语言简单测试一下,我们得到的结果是:
8 + 9 = 17
0.8 + 0.9 = 1.7000000000000002。
是的,你没有看错,计算机给出了 1.7000000000000002 这个答案。难道计算机错了吗?

计算机错了,又没错。为什么呢?因为计算机是用二进制来表示这些数字的,有很多数在计算机里面是没法精确表示的。先来看看什么是二进制。

十进制、二进制

我们都知道,计算机只能懂 0 和 1,也就是说计算机里面所有的数字,都是二进制形式的。那么一个普通的数字,用二进制的形式究竟该如何表示呢?譬如:

  • 78 如何使用二进制表示?

对于正整数,数学上有一个转换的公式,就是「除 2 取余,逆序排列」法。具体做法是:用 2 整除十进制整数,可以得到一个商和余数;再用 2 去除商,又会得到一个商和余数,如此进行,直到商为 0 时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。对于 78 这个十进制整数来说,对应的二进制是 1001110.

  • 0.5 如何使用二进制表示?

对于纯小数,也有一个固定的转换法:乘 2 取整,顺序排列。具体做法就是:用 2 乘十进制小数,可以得到积,将积的整数部分取出,再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时 0 或 1 为二进制的最后一位。对于十进制的 0.5 来说,对应的二进制表示就是 0.1

  • 8.5 如何使用二进制表示?

好了,有零有整的小数,在计算机里面怎么表示呢?可能大家已经知道了,分成整数和纯小数两部分,分别转换,然后相加起来。那么 8.5 的二进制形式可以表示为:1000.1

好了,这是基础,后面的内容就开始慢慢接近计算机的工作模式了。

二进制能准确表示十进制数字吗

首先看这个问题:

  • 0.1 这个小数,用二进制该如何表示

按照前面的转换规则,我们先看看看 0.1 怎么表示:
0.1 * 2 = 0.2 ---- 0 (1)
0.2 * 2 = 0.4 ---- 0 (2)
0.4 * 2 = 0.8 ---- 0 (3)
0.8 * 2 = 1.6 ---- 1 (4)
0.6 * 2 = 1.2 ---- 1 (5)
0.2 * 2 = 0.4 ---- 0 (6)

算到这里,我们得到了 0.000110 这样的一段二进制串,但是不能再算下去了,因为我们看到第 6 行和第 2 行完全一样,再计算就一直循环下去了。所以,0.1 这样一个十进制小数,在计算机的二进制里面根本没法精确表示。

思考一下,0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 这 9 个小数,有几个是可以精确表示的?

  • 精确表示 Vs 精确显示

通过前面的代码我们知道,0.8 + 0.9 的结果,计算机并不能精确表示,但是我们在 Javascript 中定义这样一个数值变量

var ha = 1.7

在控制台打印的话,结果又确实是 1.7。

好像很矛盾。显然,通过后面的例子可以知道 Javascript 可以精确显示 1.7(哪怕 1.7 不能被精确表示),这不是和最开始我们的结果矛盾吗?

其实这是一个想当然的错误。在直观上我们认为 0.8 + 0.9 = 1.7 是必然成立的(数学上确实如此),而且 1.7 又能被精确显示,那最开始的结果就应该等于 1.7,对不对?

而实际上,在计算机里 0.8 + 0.9 根本不等于 1.7(什么?)!因为 0.8 和 0.9 都不能被精确表示,数值的精度丢失在了每一个环节,而不是最后的结果上。

我们可以用数学中四舍五入的概念类比一下。我们计算 1.6 + 2.8 保留整数,我们觉得结果应该是 4(4.4 舍入)。但是我们用另一种方法:先把 1.6 舍入成 2,再把 2.8 舍入成 3,最后得到 5。通过不同的运算,我们得到了完全不一样的结果!所以,在 0.8 + 0.9 的运算中,参与运算的两个数精度一开始已经丢失了,所以他们的结果也不再是 1.7 了。

看到这里,我们是不是要怀疑计算机的一切结果了?连几个小数都无法精确表示的话,其他数值计算结果如何保证准确呢?其实,大家不用担心,计算机在做浮点运算的时候,常常会因为无法精确表示而进行近似或舍入,而其结果在我们日常生活所需的精度范围内,都还是准确的。

刚才说到了浮点运算,照字面的理解就是浮点数的运算了。但是,浮点数是什么呢?计算机里面是怎么表示这些数字的呢?

浮点数是什么

为了弄清楚浮点数,我们需要先看定点数。所谓定点数,是计算机中采用的一种数的表示方法,参与运算的数的小数点位置固定不变。那么小数点位置可变的自然就是所谓的「浮点数」了。这里要先澄清一个概念:浮点数并不一定等于小数,定点数也并不一定就是整数;整数和小数是我们小学数学中使用的,在计算机的世界里,只有定点数和浮点数。

  • 定点整数

小数点位固定在最后一位之后称为定点整数。若机器字长为 8 位,那么它能表示的整数范围是 -127 - 127(考虑正负数的符号)。例如 0111 表示 7。

  • 定点小数

小数点固定在最高位之后称为定点小数。若机器字长为 8 位,数值表示范围是[-(1-2^(-7)), 1-2^(-7)]。例如 1111 表示 -0.875。

  • 浮点数

在计算机的硬件中是没有小数点这个东西的。CPU 能处理的东西都是整的。所以,小数就要用类似科学计数法的方式来表示。如 1.234 在计算机里面,可以理解成用 1234 和 -3 两个整数来表示:1234 * 10 的 -3 次方,这类数就叫「浮点数」。当然,计算机里面的浮点数是二进制的。任意一个二进制浮点数 V 都可以表示成下面的形式:

V = (-1)^s x M x 2^E

这里:

  1. (-1)^s 表示符号位,当s = 0,V 为正数;当 s = 1,V 为负数;
  2. M 表示有效数字,必须大于等于 1、小于 2;
  3. 2^E 表示指数位。

举例来说,十进制的 5.0,写成二进制是 101.0,相当于 1.01 x 2^2。那么按照上面的格式,可以得出 s = 0, M = 1.01, E = 2。

CPU 在处理这类数的运算时需要比整数运算复杂得多的电路设计,且速度比整数运算慢很多(所以很多年前,浮点运算能力是衡量 CPU 性能的重要指标)。

浮点数的更多问题

历史上计算机科学家们曾提出过多种解决方案,最终获得广泛应用的是 IEEE 754 标准中的方案。IEEE 754 规定,对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M:

32 位浮点数的分段表示

IEEE 754 对有效数字 M 和指数 E,还有一些特别规定。

前面说过,1≤M<2,也就是说,M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。IEEE 754 规定,在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面的 xxxxxx 部分。比如保存 1.01 的时候,只保存 01,等到读取的时候,再把第一位的 1 加上去。这样做的目的,是节省 1 位有效数字。以 32 位浮点数为例,留给 M 只有 23 位,将第一位的 1 舍去以后,等于可以保存 24 位有效数字。

至于指数 E,情况就比较复杂。

首先,E 是没有符号的。这意味着,如果 E 为 8 位,它的取值范围为 0~255;但是,我们知道,科学计数法中的 E 是可以出现负数的,所以 IEEE 754 规定,E 的真实值必须再减去一个中间数(偏移值)。IEEE 754 标准规定该固定值为 2^(E-1) - 1,对于 8 位的 E,这个中间数是 127。

比如,2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成10+127=137,即 10001001。

然后,指数 E 还可以再分成三种情况:

  1. E不全为 0 或不全为 1。这时,浮点数就采用上面的规则表示,即指数 E 的计算值减去 127,得到真实值,再将有效数字 M 前加上第一位的 1;
  2. E全为 0。这时,浮点数的指数 E 等于 1-127,有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数。这样做是为了表示 ±0,以及接近于 0 的很小的数字;
  3. E 全为 1。这时如果有效数字 M 全为0,表示 ±无穷大(正负取决于符号位s);如果有效数字M不全为0,表示这个数不是一个数(NaN)

浮点数表示范围与表示个数
对于计算机来说,浮点数可表示的范围相当大,但这并不等于表示个数。从数量级分析一下,32bit 浮点数的表示范围是 10 的 38 次方,而表示个数呢,是 10 的 10 次方。 能够被表示的数只有 1/100000000…. (大概有30个零)。

总之,计算机中数字的存储、表示、计算是非常复杂的,涉及到的内容,都够单独出一本大部头的书了,大家有兴趣可以自己找资料深入研究一下。

推荐阅读更多精彩内容