计算机中的二进制

04 Sep 2016 , 4794 words

计算机并非生来就是二进制的天下。Babbage 最初的 Analytical Engine 计算机构想就是十进制;最早的现代计算机之一 ENIAC 也是十进制。把二进制推广开来的,可能是世界上第一台能储存程序的电子计算机 Manchester Mark 1。现如今大部分计算机都是采用二进制,人们熟知的计算机中存放每个 0/1 的「位」 bit ,就是 Binary Digit 的简写。

在计算机中,二进制对应的是信号的两个状态:有和无。初看起来只用 10 来完成所有工作效率很低,如同绘画时颜料盒里只有三原色,想要别的都需要自己从头调配起。但实际上这样保证了信号的精准。

比如我们规定 2.9V 的电压代表 1, 0V 代表 0,但实际运行中电压不会那么精准 —— 本该是 2.9V 的时候,可能波动产生的是 2.5V/2.6V/…. 而本该是 0V 的时候,可能还会有微弱的 0.1V/0.2V…。 这种情况下,我们可以把捕捉到的电压匹配为非 01,比如1V 以下的都算作 0,2V以上的都算作 1,这样就大大提高了容错率。

更直观的一个例子是,如果有一个可调节亮度的灯泡,把它的亮度范围划分为 N 档,然后让一个人来观察判断灯泡当前亮度是第几档,很可能会判断错。但如果我们只让他判断灯是亮是灭,就很难出错了。

因此,电子信号本身的特点,让二进制成为了当前计算机的最佳选择。随着量子计算机的发展,将来计算机采用的进制,可能是由可精确控制的量子状态数决定的。

说回目前来。用二进制记数本身并不会比十进制的功能弱,凡是我们现实中用十进制能表示的内容,二进制都可以完成,只是往往占用更多数位,且习惯了十进制的人类不太易读罢了。

所以很多时候要用到二进制与十进制之间的转换。

二进制与十进制换算 (正整数)

二进制换算为十进制

大部分人看到123 这个十进制数字都能脱口念出「一百二十三」,也很清楚它到底代表了多大的一个数。但实际上这里边是有一个对数位权重 (magnitude) 的计算过程的:1 在百位, 2 在十位, 3 在个位,因此 123 这个十进制数字的大小为 1 x 100 + 2 x 10 + 3 * 1。

十进制数中,每个数位权重从右向左为 100 (1), 101 (10), 102 (100)… 递增

其实二进制也是一样:

二进制数中,每个数位权重从右向左为 20 (1), 21 (2), 22(4)… 递增

既然十进制的 123 可以变成 1 x 100 + 2 x 10 + 3 * 1, 也就是 1 x 102 + 2 x 101 + 3 * 100。那如果你看到以下这个二进制数字:

101

也很完全可以按照相同方法计算:1 x 22 + 0 x 21 + 1 x 20。在我们正常人的记数方式,也就是十进制下,它的值就是 4 + 0 + 1 = 5。

下图为 000~111 范围内二进制转换为十进制的值(这里只考虑正整数所以无符号位。符号位会在下边提到。):

Binary 000 to 111

二进制换算十进制方法:

  1. 从右至左,确定每个数位的权重,最右为 2 的 0 次方,然后为 2 的 1 次方…… 如此迭加。
  2. 每个数位上的值(在二进制中就是 01),乘以权重。
  3. 各位的值相加

十进制整数换算为二进制

思路就是跟上边二进制转换为十进制相对,反其道而行之:

比如把十进制数字 43 化为二进制。

43 表示这个数中有 4 个 101, 3 个 100。现在只需要从高位到低位,看看里边有多少个 2n… 20:

43 to binary

十进制换算二进制更简单的一个方法:

  1. 将十进制数重复除以 2, 每次记录余数。 (除以 2 的余数只可能为 10
  2. 除至商 为 0 之后,将所有余数从最后到最前连起来。

43 to Binary_shortcut

二进制中的负数、小数

确定了用二进制之后并非万事大吉。正整数的表示如上边所述很直观,但还有其它问题等待解决:负数该怎么表示?分数、小数该怎么表示?数字的数位过多,bit 不够用了该怎么办?

负数与补码

负数该如何表示在计算机中是个重要问题,因为加减乘除运算说到底都是通过累加器完成。比如减法,其实 a - b 就是 a + (-b)。

最容易想到的负数表示方案,就是专门用一个 bit 位的 10 来记录数字的正负号。目前计算机记数也的确采用了类似设计:对于带符号 (signed) 的二进制数,若此数为正数,则最左边的数位为 0 (也就是说,上边的 43 在有符号位的情况下应该表示为 01010011);若为负数,则最左边的符号位是 1

比如 0101 ,最左边是 0, 所以是正数。其余部分为1011 x 22 + 0 x 21 + 1 x 20 = 5。所以 0101 就是 +5。

正数的情况很好理解。然而如果换掉上边 +5 的符号位,即1101,得到的却不是想象中的 -5。

…… ?

这是因为在表示负数的时候,我们不是直接变换符号位即可,而是采用了一个稍微复杂点的 2 的补码或称「二补数」 (2’s complement) 的方式。在这规则下,虽然依旧满足最左边的数位在整数时为 0, 负数时为 1,但这个数位却不只是简单的「符号位」了。

这部分看起来有点绕,但只需要记住下面的实际操作方法即可。

具体来说,在 2 的补码规则中,如果要把一个正数(如 +5, 也就是0101 )表示成负数,需要这样做:

two's complement: 5

  1. 把包括符号位在内的每个位数字取反,1001。(0101变为1010
  2. 给取反之后的结果加 1。(1010变为1011 。也就是说 -5 一般表示为 1011

2 的补码这种奇怪的表示负数的方式完全是人为选择的。但当初的工程师们决定绕这么一个弯子有其道理 —— 假设如果我们采用一般人想到最直观的表示方式:只变换符号位,其他部分不变。那么就会有这么两个问题:

  1. 表示 0 的情况重复了。0000 和 1000,一个表示 + 0, 一个表示 - 0。
  2. 正负相加的运算不能用直观的累加完成了。 比如 5 和 -5 相加结果为 0,如果写成只以符号位区分的二进制,就是 0101 和 1101相加,结果为 10010 而不是本应用来表示 0 的 0000(或 1000)。再比如 -5 + 1,就是 1101 + 0001,结果为 1110,而不是本应用来表示 -4 的 1100。

这两个问题的共同结果是让数字电路设计复杂程度大大增加了。

现在用 2 的补码试试,发现这两个问题都解决了:

  1. 只有 0000 才是表示 0, 1000 在 2 的补码方式中不再是 - 0。按照上边讲的运算方式反推可以发现,1000 表示的是 -8。
  2. 正负相加的运算可以用直观的累加完成。还以 5 和 -5 为例,5 为 0101,-5 这时候变成了 1011,相加为 10000。只需要忽略最左边溢出的那一位,右边四位正好是 0000,也就是 0。

所以对计算机来说,2 的补码确实是更好用的表示负数的方式。

下表是「变换符号位」,「1 的补码 (对整数的所有数位取反来表示负数)」,「2 的补码」三种负数表示方式在 0000 ~ 1111 范围内表示的十进制数值对比。

十进制值 变换符号位 1 的补码 2 的补码
+7 0111 0111 0111
+6 0110 0110 0110
+5 0101 0101 0101
+4 0100 0100 0100
+3 0011 0011 0011
+2 0010 0010 0010
+1 0001 0001 0001
+0 0000 0000 0000
-0 1000 1111 -
-1 1001 1110 1111
-2 1010 1101 1110
-3 1011 1100 1101
-4 1100 1011 1100
-5 1101 1010 1011
-6 1110 1001 1010
-7 1111 1000 1001
-8 - - 1000

十进制与二进制负整数的换算

负整数的换算与前边的正整数换算方法类似:

二进制转十进制:

依然是各数位上的值乘以权重相加,但最左边权重最大的那个 1 x 2n 为负值。如 1010 = - 1 x 23 + 0 x 22 + 1 x 21 + 0 x 20 = - 6。

当然也可以先对这个负二进制数反向取其 2 的补码,得出其绝对值,接下来只要算出这个绝对值的十进制值然后加上负号即可。

十进制转二进制: 先把其绝对值换算为二进制数,然后按照前边 2 的补码规则把正数转化为负数即得出。

小数

二进制中小数与十进制并无差别。十进制中小数点后从左至右依次是 10-1, 10-2… 二进制小数点后则为 2-1, 2-2

二进制小数转十进制小数与前边整数的转换方式完全相同,不过就是数位权重从 2 的正数次幂扩展到了负数次幂。比如 01.01 就等于 1 x 20 + 0 x 2-1 + 1 x 2-2 = 1.25。

十进制小数转二进制稍微复杂一些,方法如下:

  1. 先忽略小数部分,只换算整数部分,接下来再针对小数部分
  2. 把小数部分乘以 2。若乘积 ≥ 1,则小数点后一位值为 1;若乘积 < 1,则小数点后一位值为 0
  3. 重复步骤 2,将得到的 10向右继续添加。每次重复时都忽略上一次乘 2 时可能产生的整数 1,只计算小数部分。
  4. 若出现乘积 = 1 或循环,则换算结束。

以 3.625 为例:

  1. 整数部分 3 换算为 0011。
  2. 小数部分 0.625 x 2 = 1.25,乘积 > 1,故小数点后第一位数字为 1
  3. 重复上边步骤,去掉 1.25 的整数部分,继续用 0.25 x 2 = 0.5。乘积 < 1,故下一位数字为 0;重复上边步骤,继续用 0.5 x 2 = 1,故下一位数字为 1,且这里出现了乘积 = 1 的情况。
  4. 换算结束,把刚才的结果连起来为:0011.101

循环的情况可以试试把十进制的 0.1 转化为二进制。(这里有个有趣的现象就是,计算机虽强大却无法准确表示像 0.1 这样简单的十进制数字。另外要说明的是浮点不在本文范围内。)

长度与溢出

本文中出现的二进制数字大多是 4 位的,这只是为了行文方便随便举的例子,在计算机运算中会涉及到多种长度的二进制数。

比如一个字节 (byte) 等于 8 位 (bit,即 binary digits),也就是说一个字节可以存储 8 个二进制数。再比如平时所说的 32 位/ 64 位 CPU,指的是该 CPU 一次能处理的数据长度,所以 x 位的 CPU 指令往往就是个 x 位长的二进制数。 P

补位

在十进制运算中,遇到不同长度的数,只需要把「个位」对齐就可以计算了。这里边有个隐含的动作是给短的数补上了很多 0。如 123 + 4,其实是当作 123 + 004 来计算。在一个数字前补上多少个 0 都不会影响其实际大小。

但计算机在表示负数时不光是把十进制换成二进制,还用了 2 的补码。因此补位的规则就不能是简单的补 0 —— 假如把 1011 (-5) 补成 8 位, 直接补上 0 会变成 00001011, 符号位是 0, 变成了 +11。

因此,2 的补码规则下的二进制补位时,应该是补上符号位的值,即正数补 0,负数补 1。如上边的 1011 应该补为 11111011,值依然是 -5 不变。

溢出

数字相加时会出现进位。既然有进位,那么在长度有限的情况下有可能发生溢出。

正数的溢出很容易判断:只要最高位发生了进位,就一定是溢出了。

负数则不然,如 1110(-2) + 1110(-2) = 11100(-4),虽然最高位的 1 进出了一位,但结果仍然是正确的。负数溢出判断规则:如果最高位进入的和进出的一样,则没有发生溢出,否则溢出。上边的 1110 + 1110,最高位会收到次高位进过来的 1,然后又向进出了一个 1。所以并未溢出。