🧮 第8章 算法和计算机
📄 一句话概括

电脑里负数存的是补码:取反加一。 为什么非要补码?因为它能让减法变成加法——硬件只要一个加法器。 八位有符号整数的范围是 -128 到 127,负数比正数多一个。

📝 本章和前面几章的关系

上一篇转的都是正数。负数在二进制里怎么存?这是本章最费脑子的一节。 教材的路线是先讲原码、再讲反码、 最后讲补码——前两个是推导路上的两步,真正存进电脑的只有补码。 这一篇的重点不是背「取反加一」,是理解「为什么这么算就对了」。

一、八位二进制里,负数怎么存

最直觉的想法是「留一位当符号」:最高位 0 表示正、1 表示负,剩下七位存大小。这叫原码。 所以 -5 写成 10000101(最高位 1,后面是 5 的七位)。看着很合理,但它有两个毛病。 第一个毛病:00000000 和 10000000 都表示 0,一个 +0 一个 -0。同一件事两种写法,判断起来麻烦。

第二个毛病更致命:这样存的负数没法直接做加法。5 加 -3 得 2, 但 00000101 加 10000011 算出来完全不是 2。

概念 +5 的八位 -5 的八位 怎么来的
原码 00000101 10000101 最高位当符号位,1 表示负
反码 00000101 11111010 负数的各位取反
补码 00000101 11111011 反码加 1

所以有了反码(各位取反)和补码(反码加一)。电脑里存的是补码,原码和反码只是推导路上的两步。

二、自己算一遍 -5 的补码

+5 的八位是 00000101。第一步,各位取反得 11111010;第二步,加 1 得 11111011。 11111011 就是 -5。 验证的办法是「按有符号读」:最高位当 -128,后面七位照常加。

11111011 等于 -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1,算出来正好 -5。 这一步是理解补码的钥匙:最高位不是「符号」,是「负的 128」。把它当成权重 -128, 整套规则就自洽了。 这也解释了为什么取反加一能用:取反把每一位的贡献都反过来,加一补上那个差, 最后效果等于「把最高位的权重从 +128 改成 -128」。

三、为什么非要补码:让减法变加法

算 5 减 3,机器实际做的是 5 加 -3。用补码写出来: 00000101 加 11111101,竖着加得 100000010——九位。 第 9 位溢出来直接扔掉,剩下 00000010,也就是 2。结果对了。

这不是巧合。补码的设计目标就是:让负数的位模式和正数做加法时,自动得到减法的结果。 所以硬件只要一个加法器,不用专门做减法器。这是补码最大的实惠——省的不是代码,是电路。 顺带一句:那个「第 9 位扔掉」的动作在硬件里就是「寄存器只有八位,装不下就丢」。 它不是错误处理,是设计的一部分。

四、范围为什么是 -128 到 127

八位能表示 256 个不同的位模式,这一点无论怎么解释都变不了。

八位里最高位 能表示的范围 个数
不用来当符号 0 到 255 256
用来当符号(补码) -128 到 127 256

同一个位模式 10000000:当无符号读是 128,当有符号读是 -128。 两边都是 256 个数,只是分法不同。 那为什么负数比正数多一个? 因为 0 占了一个位置,而 +0 和 -0 在补码里是同一个 0,省下来的那个位置给了 -128。

所以 8 位有符号整数的范围不对称:-128 有,+128 没有。16 位、32 位都是同一套道理,只是位数多。

五、这一节的实用价值

知道补码,能解释几个日常现象。 第一,为什么 int 的最大值加一会变成最小值。01111111 加 1 得 10000000, 从最大正数跳到最小负数——这叫溢出。 第二,为什么绝对值最小的负数没有对应的正数。-128 取绝对值还是 128,而 8 位装不下 +128。

第三,为什么位运算对负数看着很怪。-1 的八位是 11111111,取反得 0——这不是 bug, 这是补码的必然。 这些问题在真实代码里会遇到,尤其是处理时间戳、文件大小、以及从别的系统读来的二进制数据时。 知道底层怎么存,才不会把「溢出」当成「玄学」。

六、动手 Demo:算一遍 -5 的补码,再用补码做一次减法

先把 +5 的原码、反码、补码三种都写出来,看 -5 是怎么一步步来的。 再把 11111011 按「最高位当 -128」读一遍,确认是 -5。 最后用补码算 5 减 3,看第九位怎么被扔掉。

def bits(n, w=8):
    return format(n & ((1 << w) - 1), '0%db' % w)

print('+5', bits(5))
print('原码', bits(-5))
print('补码', bits(-5))

b = bits(-5)
val = -128 + sum(int(c) * (1 << (6 - i)) for i, c in enumerate(b[1:]))
print(b, '-> 按有符号读', val)

x, y = 5, -3
print(bits(x), '+', bits(y), '=', bits(x + y), '十进制', x + y)
print('第九位', bin(x + y + 256))

print(bits(127), '加一得', bits(127 + 1), '也就是 -128')

实跑输出:

--------------------------------------------------------------------
负数怎么表示:原码、反码、补码
--------------------------------------------------------------------


  ① 八位二进制里,负数怎么存
  --------------------------------------------------------------------

| 概念 | +5 的八位 | -5 的八位 | 怎么来的 |
|---|---|---|---|
| 原码 | 00000101 | 10000101 | 最高位当符号位,1 表示负 |
| 反码 | 00000101 | 11111010 | 负数的各位取反 |
| 补码 | 00000101 | 11111011 | 反码加 1 |

  电脑里存的是补码。原码和反码只是推导路上的两步,实际不用它们存。

  ② 自己算一遍 -5 的补码
  --------------------------------------------------------------------
  +5 的八位:00000101
  各位取反  :11111010
  再加 1    :11111011   <- 这就是 -5
  验证:同一位模式按有符号读,等于 -5
  最后那一步是理解补码的钥匙:最高位是 1 时,把它当成 -128,后面七位照常加。
  11111011 = -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5。

  ③ 为什么非要补码:让减法变加法
  --------------------------------------------------------------------
  算 5 - 3,机器实际做的是 5 + (-3):
    00000101  (5)
  + 11111101  (-3 的补码)
  ---------
    00000010  = 2
  结果第 9 位如果溢出来就直接扔掉,剩下正好是 2。
  所以硬件只要一个加法器,不用专门做减法器——这是补码最大的实惠。

  ④ 范围为什么是 -128 到 127
  --------------------------------------------------------------------

| 八位里最高位 | 能表示的范围 | 个数 |
|---|---|---|
| 不用来当符号 | 0 到 255 | 256 |
| 用来当符号(补码) | -128 到 127 | 256 |

  同一个位模式 10000000:当无符号是 128,当有符号是 -128
  两边都是 256 个数,只是分法不同。
  负数比正数多一个,因为 0 占了一个位置,而 -0 和 +0 在补码里是同一个 0,
  省下来的那个位置给了 -128。
  这就是为什么 8 位有符号整数的范围不对称:-128 有,+128 没有。
  16 位、32 位都是同一套道理,只是位数多。

①n & 0xff 是把负数截成八位的位模式,format(..., '08b') 打成八位二进制。 Python 的负数本来就按补码存,所以 bits(-5) 直接就是补码。 ②第二段按 -128 + 后面七位的权重和 算,得 -5。这就是「最高位当负权重」的验证。 ③bits(x + y) 直接给 2,因为 Python 内部按补码算,溢出的位自然丢掉。 ④最后一行是溢出的样子:127 的八位加一变成 -128。这就是「最大值加一变最小值」。

30 秒实验

  1. 算一下 -1 的八位补码,看是不是全是 1。
  2. 算 -128 的补码,看它和 128 的位模式是不是同一个。
  3. 把 bits 的位数改成 16,看 -5 变成什么,验证「位数多,道理一样」。

常见误区

  1. 「负数就是把最高位改成 1。」那是原码,不能直接做加法。电脑里存的是补码。
  2. 「补码里的最高位是符号位。」把它当 -128 这个权重更准,规则才自洽。
  3. 「8 位有符号范围是 -127 到 127。」是 -128 到 127,负数多一个。
自测三问(点开对答案) 1. -5 的八位补码怎么算? —— +5 是 `00000101`,各位取反得 `11111010`,加一得 `11111011`。 2. 补码为什么能让减法变加法? —— 负数的位模式和正数相加,溢出的高位扔掉,结果正好是差。硬件只需加法器。 3. 为什么 8 位范围是 -128 到 127,不是对称的? —— +0 和 -0 在补码里是同一个 0,省下的一个位模式给了 -128。

最后一节讲计算机的另一半地基:逻辑运算。 与、或、非三块积木,加上一条德摩根定律,就是所有判断的底子。

相关笔记