计算机是怎么用二进制做加减乘除的——从原码反码补码讲起

大家基本都知道二进制是怎么回事了——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 的补码:

  1. +5 的原码:0000 0101
  2. 取反得反码:1111 1010
  3. 加 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 = ?

  1. 7 的 8 位补码:0000 0111
  2. 3 的 8 位补码:0000 0011
  3. -3 的补码:把 3 取反加 1 → 1111 1101
  4. 加法:0000 0111 + 1111 1101 = 1 0000 0100(溢出位丢掉,所以是 0000 0100 = 4)✓

再看一个:3 - 7 = ?

  1. 3 的补码:0000 0011
  2. -7 的补码:把 7(0000 0111) 取反加 1 → 1111 1001
  3. 加法:0000 0011 + 1111 1001 = 1111 1100
  4. 结果最高位是 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)

对应十进制公式也是对的:

\[ 13 \times 6 = 13 \times (2^1 + 2^2) = 13 \times 2 + 13 \times 4 = 26 + 52 = 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 两种数字,比十进制的心算口诀简单得多,反而更规整。