大家基本都知道二进制是怎么回事了——0 和 1,每一位代表 2 的几次方。但光知道二进制表示数字还不够,还得知道计算机怎么用它来算加减乘除。
这事的关键在于三个东西:原码、反码、补码。把这三个弄明白,计算机的运算就没什么神秘的了。
原码:最朴素的二进制表示
原码就是最直接的写法。最高位(最左边的那位)是符号位——0 表示正数,1 表示负数。剩下的位表示数值的大小。
比如用 8 位二进制(一个字节)来存。先约定一个基础规则:8 位二进制,每一位从右到左有一个编号。最右边的那位叫作 bit 0,往左依次是 bit 1、bit 2……一直到最左边的 bit 7。bit 0 代表 2⁰ = 1,bit 1 代表 2¹ = 2,bit 7 代表 2⁷ = 128。这个编号方式后面会反复用到。
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0 0 0 0 0 1 0 1 = +5
- +5 怎么表示?符号位是 0,剩下 7 位是 0000101(二进制 101)。组合起来:0000 0101。
- -5 怎么表示?符号位是 1,剩下 7 位同样是 0000101。组合起来:1000 0101。
简单吧。但原码有一个很要命的问题:如果用原码直接算加法,遇到正负数混合的时候,结果是不对的。
比如 5 + (-3),按原码来:0000 0101 + 1000 0011 = 1000 1000。最高位是 1,看起来结果应该是 -8。但 5 + (-3) 应该是 2。
问题出在符号位也参与了运算。我们需要一种表示法,让符号位和数值位可以统一处理、加法器和减法器能合成同一个电路。这就是补码的由来。但在讲补码之前,先看它的过渡形态:反码。
反码:把负数翻转一下
反码的规则很简单:
- 正数的反码就是它自己(和原码一样)。
- 负数的反码:符号位保持 1,其余每一位取反——0 变成 1,1 变成 0。
还是用 8 位来看:
-
+5 的反码:0000 0101(和原码一样)。
-
-5 的反码:先写出 +5 的原码 0000 0101,然后符号位保持,其余全部取反。
text
原码: 0 0 0 0 0 1 0 1 (正数的原码)
|←——数值——→|
反码: 1 1 1 1 1 0 1 0 (符号位变1,其余取反)
所以 -5 的反码是 1111 1010。
来验证一下:反码的 +5(00000101) 加上 -5(11111010) 等于多少?
0000 0101 (+5 的反码)
+ 1111 1010 (-5 的反码)
-----------
1111 1111 (全是1)
11111111 按反码还原回去,就是 -0。反码算出来的 5 + (-5) = -0。方向是对了,但有两个零(00000000 是 +0,11111111 是 -0),这就很不方便。你一个系统里有两个零,比大小又麻烦了。所以还要进一步优化:补码。
补码:负数 = 反码 + 1
补码是现在计算机里真正用的表示方法。规则也很简单:
- 正数的补码 = 它自己(和原码、反码一样)。
- 负数的补码 = 它的反码 + 1。
按这个规则重算 -5 的补码:
- +5 的原码:0000 0101
- 取反得反码:1111 1010
- 加 1:1111 1010 + 1 = 1111 1011
所以 -5 的补码是 1111 1011。
现在用补码算 5 + (-5):
0000 0101 (+5 的补码)
+ 1111 1011 (-5 的补码)
-----------
10000 0000 (最高位的进位溢出了,8位以内就是 0000 0000)
结果正好是 0——完美的,两个零的问题解决了。而且补码表示的范围比原码多一个数:8 位补码可以表示 -128 到 +127,而原码只能表示 -127 到 +127。
补码让你能用同一个加法器算所有的加减法,不需要单独的减法电路。这就是为什么计算机都用补码。
用补码做减法
有了补码之后,减法就变成了加法:A - B = A + (-B 的补码)。
计算机里没有减法器,只有一个加法器。算减法的时候,先把减数转成补码,然后直接当加法算。
看几个例子。比如:7 - 3 = ?
- 7 的 8 位补码:0000 0111
- 3 的 8 位补码:0000 0011
- -3 的补码:把 3 取反加 1 → 1111 1101
- 加法:0000 0111 + 1111 1101 = 1 0000 0100(溢出位丢掉,所以是 0000 0100 = 4)✓
再看一个:3 - 7 = ?
- 3 的补码:0000 0011
- -7 的补码:把 7(0000 0111) 取反加 1 → 1111 1001
- 加法:0000 0011 + 1111 1001 = 1111 1100
- 结果最高位是 1,说明是负数。把这个负数的补码还原成原码:减 1 再取反。1111 1100 - 1 = 1111 1011,取反 = 0000 0100 = 4,所以结果是 -4。✓
就这样,不管你是正数加正数、正数加负数、两个负数相加,用补码都能直接用同一个加法器算对。这是补码最美的地方。
乘法:就是重复加,但加了点技巧
乘法原理上就是连加。3 × 5 就是把 5 加三次:5 + 5 + 5 = 15。但计算机不会傻到真的加几十上百次——它用手算竖式乘法的思路。
二进制乘法比十进制更简单,因为每一位只能是 0 或 1:
- 如果乘数当前位是 1 → 加上被乘数。
- 如果乘数当前位是 0 → 不加,直接跳过。
手算一下 13 × 6(都用 8 位无符号二进制):13 = 0000 1101,6 = 0000 0110。
0000 1101 (被乘数 = 13)
× 0000 0110 (乘数 = 6)
-------------------
0000 0000 (乘数第0位=0,不加)
0000 1101 (乘数第1位=1,加上 13,左移1位)
0000 1101 (乘数第2位=1,加上 13,左移2位)
+ 0000 0000 (乘数第3位及以后=0,不加)
= 0100 1110 (64+8+4+2=78)
对应十进制公式也是对的:
所以你看到没有,乘法在计算机里其实是一系列"判断某一位是否为 1,如果是就加被乘数的移位"的操作。需要加多少次?取决于乘数有多少个 1。乘数里 1 越少,乘法越快。
除法:从长除法到计算机的做法
除法是乘法的逆运算。你还记得小时候列竖式做除法吗?比如 78 ÷ 6,你会在纸上写:
6 能乘以多少能刚好不超过 78?6×10=60,6×13=78——刚好。所以你得到商 13,余数 0。
计算机也是这个思路,只不过它用的是二进制。而且二进制除法比十进制简单很多——因为每一位商只能是 0 或 1。你不需要像十进制那样去试"6 能乘以多少不超过 78"——你只需要试"除数的这个倍数能减吗",能减商就是 1,不能减商就是 0。
下面用 78 ÷ 6 来一步步走一遍。先统一用 8 位二进制:78 = 0100 1110,6 = 0000 0110。
第一步:把除数对齐到被除数
先找一个"合适的"位置把除数放上去——把除数左移,让它尽可能靠近被除数但不超过它。这和十进制竖式里把除数写到被除数的左边几位对齐是一个道理。
6 = 0000 0110。左移 1 位(×2)就是 0000 1100 = 12,左移 2 位(×4)= 0001 1000 = 24,左移 3 位(×8)= 0011 0000 = 48,左移 4 位(×16)= 0110 0000 = 96。
96 > 78,超出了,不行。退回到左移 3 位:48。48 < 78,可以。这相当于 6 × 8 = 48。
好,商从最高位(bit 3 的位置)开始定。
除数左移到 0011 0000 (6 × 8 = 48),刚好不超过 78。
第二步:逐位确定商
现在把除数的这个移位版本(0011 0000 = 48)和被除数(0100 1110 = 78)放在一起,从左到右逐位定商。
第一位(bit 3):48 能减吗? 78 - 48 = 30 ≥ 0,可以。商 bit3 = 1,新的余数变成 30。
第二位(bit 2):除数再右移一位,48 → 24(0011 0000 右移一位 = 0001 1000 = 24)。30 - 24 = 6 ≥ 0,可以减。商 bit2 = 1,余数变成 6。
第三位(bit 1):除数再右移一位,24 → 12(0000 1100)。6 - 12 = -6 < 0,不能减。商 bit1 = 0,余数保持 6。(能减就置 1 然后减,不能就跳过,和除法恢复算法对上了。)
第四位(bit 0):除数再右移一位,12 → 6(0000 0110)。6 - 6 = 0 ≥ 0,可以减。商 bit0 = 1,余数变成 0。
整个过程用一张表格看得更清楚:
被除数: 0100 1110 = 78
除数: 0000 0110 = 6
对齐: 0011 0000 = 6 × 8 = 48 (左移3位, 刚好不超过78)
bit3: 0100 1110 - 0011 0000 = 0001 1110 = 30 ≥ 0 → 商 bit3 = 1
余数 = 30
↓ 除数右移1位
bit2: 0001 1110 - 0001 1000 = 0000 0110 = 6 ≥ 0 → 商 bit2 = 1
余数 = 6
↓ 除数右移1位
bit1: 0000 0110 - 0000 1100 = 负数 < 0 → 商 bit1 = 0
余数保持 6
↓ 除数右移1位
bit0: 0000 0110 - 0000 0110 = 0000 0000 = 0 ≥ 0 → 商 bit0 = 1
余数 = 0
商 = 1101 (从高到低读出) = 8 + 4 + 0 + 1 = 13 ✓
余数 = 0 ✓
你看,和十进制做长除法的过程是一模一样的,只是每一步的判断——能减吗——变得特别简单,因为减得动的结果非负、减不动的结果是负,直接把符号位拿出来就定了商 bit 是 1 还是 0。不需要去猜测商应该取几,也不用乘法试商,这是二进制除法和十进制除法相比最舒服的一个地方。
最后总结
这些东西一长串地看下来,其实逻辑很简单。计算机是从三种表示数字的方式——原码、反码、补码——一步步推导下来的:
- 原码:最直接,但正负混合加算出错。
- 反码:通过取反解决了符号带来的部分问题,但仍然有两个 0。
- 补码:反码加 1,把"两个 0"的问题也解决,让同一个加法器能做所有的加减运算。
有了补码,减法通过加上一个补码形式的减数变成了不需要区分的统一的加法。乘法在底层被拆解成"加数和移位"——乘数的每一位是不是 1 决定要不要加,这其实是小学竖式乘法的二进制版本。除法则是先从小到大去比较乘了一个倍数的除数和被除数的大小,把商的每一位定好,也就是长除法在计算机里的样子。
回过头来看,计算机的加减乘除,全都建立在二进制的 0 和 1 上——和你列竖式的思路没什么不同,只是它只用到了 0 和 1 两种数字,比十进制的心算口诀简单得多,反而更规整。