电脑里负数存的是补码:取反加一。 为什么非要补码?因为它能让减法变成加法——硬件只要一个加法器。 八位有符号整数的范围是 -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。
- 算 -128 的补码,看它和 128 的位模式是不是同一个。
- 把
bits的位数改成 16,看 -5 变成什么,验证「位数多,道理一样」。
常见误区
- 「负数就是把最高位改成 1。」那是原码,不能直接做加法。电脑里存的是补码。
- 「补码里的最高位是符号位。」把它当 -128 这个权重更准,规则才自洽。
- 「8 位有符号范围是 -127 到 127。」是 -128 到 127,负数多一个。
自测三问(点开对答案)
1. -5 的八位补码怎么算? —— +5 是 `00000101`,各位取反得 `11111010`,加一得 `11111011`。 2. 补码为什么能让减法变加法? —— 负数的位模式和正数相加,溢出的高位扔掉,结果正好是差。硬件只需加法器。 3. 为什么 8 位范围是 -128 到 127,不是对称的? —— +0 和 -0 在补码里是同一个 0,省下的一个位模式给了 -128。最后一节讲计算机的另一半地基:逻辑运算。 与、或、非三块积木,加上一条德摩根定律,就是所有判断的底子。
- ☐ (本篇完,下一篇 8.4 逻辑运算与德摩根定律)