计算机并非生来就是二进制的天下。Babbage 最初的 Analytical Engine 计算机构想就是十进制;最早的现代计算机之一 ENIAC 也是十进制。把二进制推广开来的,可能是世界上第一台能储存程序的电子计算机 Manchester Mark 1。现如今大部分计算机都是采用二进制,人们熟知的计算机中存放每个 0
/1
的「位」 bit ,就是 Binary Digit 的简写。
在计算机中,二进制对应的是信号的两个状态:有和无。初看起来只用 1
和 0
来完成所有工作效率很低,如同绘画时颜料盒里只有三原色,想要别的都需要自己从头调配起。但实际上这样保证了信号的精准。
比如我们规定 2.9V 的电压代表 1
, 0V 代表 0
,但实际运行中电压不会那么精准 —— 本该是 2.9V 的时候,可能波动产生的是 2.5V/2.6V/…. 而本该是 0V 的时候,可能还会有微弱的 0.1V/0.2V…。 这种情况下,我们可以把捕捉到的电压匹配为非 0
即 1
,比如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
范围内二进制转换为十进制的值(这里只考虑正整数所以无符号位。符号位会在下边提到。):
二进制换算十进制方法:
- 从右至左,确定每个数位的权重,最右为 2 的 0 次方,然后为 2 的 1 次方…… 如此迭加。
- 每个数位上的值(在二进制中就是
0
或1
),乘以权重。 - 各位的值相加
十进制整数换算为二进制
思路就是跟上边二进制转换为十进制相对,反其道而行之:
比如把十进制数字 43
化为二进制。
43 表示这个数中有 4 个 101, 3 个 100。现在只需要从高位到低位,看看里边有多少个 2n… 20:
十进制换算二进制更简单的一个方法:
- 将十进制数重复除以 2, 每次记录余数。 (除以 2 的余数只可能为
1
或0
) - 除至商 为 0 之后,将所有余数从最后到最前连起来。
二进制中的负数、小数
确定了用二进制之后并非万事大吉。正整数的表示如上边所述很直观,但还有其它问题等待解决:负数该怎么表示?分数、小数该怎么表示?数字的数位过多,bit 不够用了该怎么办?
负数与补码
负数该如何表示在计算机中是个重要问题,因为加减乘除运算说到底都是通过累加器完成。比如减法,其实 a - b 就是 a + (-b)。
最容易想到的负数表示方案,就是专门用一个 bit 位的 1
和 0
来记录数字的正负号。目前计算机记数也的确采用了类似设计:对于带符号 (signed) 的二进制数,若此数为正数,则最左边的数位为 0
(也就是说,上边的 43 在有符号位的情况下应该表示为 01010011
);若为负数,则最左边的符号位是 1
。
比如 0101
,最左边是 0
, 所以是正数。其余部分为101
即 1
x 22 + 0
x 21 + 1
x 20 = 5。所以 0101
就是 +5。
正数的情况很好理解。然而如果换掉上边 +5 的符号位,即1101
,得到的却不是想象中的 -5。
…… ?
这是因为在表示负数的时候,我们不是直接变换符号位即可,而是采用了一个稍微复杂点的 2 的补码或称「二补数」 (2’s complement) 的方式。在这规则下,虽然依旧满足最左边的数位在整数时为 0, 负数时为 1,但这个数位却不只是简单的「符号位」了。
这部分看起来有点绕,但只需要记住下面的实际操作方法即可。
具体来说,在 2 的补码规则中,如果要把一个正数(如 +5, 也就是0101
)表示成负数,需要这样做:
- 把包括符号位在内的每个位数字取反,
1
变0
,0
变1
。(0101
变为1010
) - 给取反之后的结果加 1。(
1010
变为1011
。也就是说 -5 一般表示为1011
)
2 的补码这种奇怪的表示负数的方式完全是人为选择的。但当初的工程师们决定绕这么一个弯子有其道理 —— 假设如果我们采用一般人想到最直观的表示方式:只变换符号位,其他部分不变。那么就会有这么两个问题:
- 表示 0 的情况重复了。0000 和 1000,一个表示 + 0, 一个表示 - 0。
- 正负相加的运算不能用直观的累加完成了。 比如 5 和 -5 相加结果为 0,如果写成只以符号位区分的二进制,就是 0101 和 1101相加,结果为 10010 而不是本应用来表示 0 的 0000(或 1000)。再比如 -5 + 1,就是 1101 + 0001,结果为 1110,而不是本应用来表示 -4 的 1100。
这两个问题的共同结果是让数字电路设计复杂程度大大增加了。
现在用 2 的补码试试,发现这两个问题都解决了:
- 只有 0000 才是表示 0, 1000 在 2 的补码方式中不再是 - 0。按照上边讲的运算方式反推可以发现,1000 表示的是 -8。
- 正负相加的运算可以用直观的累加完成。还以 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。
十进制小数转二进制稍微复杂一些,方法如下:
- 先忽略小数部分,只换算整数部分,接下来再针对小数部分
- 把小数部分乘以 2。若乘积 ≥ 1,则小数点后一位值为
1
;若乘积 < 1,则小数点后一位值为0
。 - 重复步骤 2,将得到的
1
或0
向右继续添加。每次重复时都忽略上一次乘 2 时可能产生的整数 1,只计算小数部分。 - 若出现乘积 = 1 或循环,则换算结束。
以 3.625 为例:
- 整数部分 3 换算为 0011。
- 小数部分 0.625 x 2 = 1.25,乘积 > 1,故小数点后第一位数字为
1
。 - 重复上边步骤,去掉 1.25 的整数部分,继续用 0.25 x 2 = 0.5。乘积 < 1,故下一位数字为
0
;重复上边步骤,继续用 0.5 x 2 = 1,故下一位数字为1
,且这里出现了乘积 = 1 的情况。 - 换算结束,把刚才的结果连起来为:
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。所以并未溢出。