为什么 `~5` 是 -6?——从电灯开关到补码的奇妙旅程
你有没有想过,一个简单的“取反”操作,为什么在 Python 里会给你一个看起来毫不相干的负数?
~5 的结果是 -6,而不是你直觉中的“某个很大的数”。如果你第一次见到这个结果时一脸懵,恭喜你,你正在触碰计算机最底层、最优雅的秘密之一。
今天,我想带你从电灯开关出发,一路走到补码和位运算。你会发现,这些看似抽象的概念,其实就藏在我们每天用到的每一个程序里。
一切从开关开始
想象你面前有一排电灯开关。每个开关只有两种状态:开或关。如果我用“开”表示 1,“关”表示 0,那么这一排开关就能表示一个二进制数。
这就是计算机的底层逻辑:数以亿计的微型开关(晶体管)通过“有电/没电”两种状态存储和运算数据。为什么用二进制?因为电路实现最简单、最可靠——只需要判断高低电平,不需要区分 10 种不同的电压等级。
我见过一道非常经典的题目,问“计算机内部为什么要采用二进制”。很多人会选“二进制运算规则最简单”,但正确答案是 “二进制容易用电路的高低电平实现” 。运算规则简单只是副产品,核心原因是物理实现的便利性。记住这一点,你就抓住了计算机设计的根本逻辑。
二进制:计算机的母语
在 Python 中,用 0b 前缀表示二进制数:
a = 0b101 # 十进制的 5
b = 0b110 # 十进制的 6
print(a + b) # 输出 11,二进制是 0b1011
转换也很简单:bin(10) 得到字符串 '0b1010',int('0b1010', 2) 转回十进制。
但这里有个新手常踩的坑:bin() 返回的是字符串,不能直接做数学运算。你得先 int(..., 2) 转回整数。
另一个经典题目是“二进制数 (1011)₂ 转十进制”。答案是 11,计算过程是 1×2³ + 0×2² + 1×2¹ + 1×2⁰ = 8+0+2+1 = 11。这题看似简单,但考试时总有人算错——要么忘了最高位的权值是 8,要么把中间那个 0 的位权搞混。我的建议是:从右往左,逐位标记权值,写出完整算式,别心算。
负数怎么表示?——补码的魔法
正数好办,负数呢?计算机里没有负号,只有 0 和 1。于是天才的工程师们发明了补码。
先看原码和反码,它们是理解补码的阶梯:
- 原码:最左边一位是符号位,0 正 1 负。+5 是
00000101,-5 是10000101。直观,但做减法会出大问题。 - 反码:正数不变,负数符号位不变、其他位取反。-5 的反码是
11111010。解决了部分问题,但 0 有两种表示,加法还是别扭。 - 补码:正数不变,负数是反码加 1。-5 的补码是
11111011。
为什么补码是最终赢家?因为它让减法变成加法。计算 5 + (-5),用补码就是 00000101 + 11111011 = 1 00000000,最高位溢出丢弃,剩下 00000000,正好是 0。
这就像时钟:现在是 3 点,想知道 2 小时前是几点,可以逆时针拨 2 格(3-2=1),也可以顺时针拨 10 格(3+10=13,mod 12 = 1)。-2 的补码等价于 +10,因为 10 是 12 减去 2。在 8 位二进制里,-5 的补码 11111011 当无符号数看是 251,而 251 + 5 = 256 = 2⁸,溢出归零,完美。
有个生活化的比喻:你的钱包只能装 0~255 元。你有 5 元,妈妈要扣掉 5 元。她不是直接拿走 5 元,而是给你加了一笔“反向”的 251 元——你的钱变成 256 元,钱包溢出归零,最后空了。绕是绕了点,但计算机用这套逻辑做减法,电路简单又可靠。
在 Python 里模拟 8 位补码:
def to_complement8(n):
if n >= 0:
return format(n & 0xFF, '08b')
else:
return format(((-n) ^ 0xFF) + 1 & 0xFF, '08b')
print(to_complement8(5)) # 00000101
print(to_complement8(-5)) # 11111011
注意 & 0xFF 是为了截断到 8 位。Python 的整数无限精度,负数补码前面有无数个 1,不截断会得到一长串 1。
位运算:为什么 ~5 是 -6?
有了补码,位运算的“怪事”就讲得通了。
Python 的整数是无限精度的,所以负数以无限长的补码存在(前面无数个 1)。~5 的过程是:
- 5 的补码(无限位):
...00000101 - 按位取反:
...11111010 - 这是一个负数的补码。转回十进制:先减 1 得
...11111001,再取反得...00000110,即 6,加上负号就是 -6。
所以 ~5 = -6,~(-5) = 4。
我常跟学生说:把数字想象成无限长的黑白棋盘,白色是 0,黑色是 1。5 的棋盘是“全白、白、黑、白、黑”(从右往左:101)。全部翻转颜色,得到“全黑、黑、白、黑、白”,这个新棋盘代表的数就是 -6。这个比喻虽然有点抽象,但一旦建立起画面感,位运算就不再神秘。
其他位运算也基于补码:
a = 5 # 101
b = 3 # 011
print(a & b) # 1
print(a | b) # 7
print(a ^ b) # 6
print(~a) # -6
print(a << 1) # 10(左移1位相当于乘2)
print(a >> 1) # 2(右移1位相当于整除2)
特别注意:负数右移时,高位补 1(算术右移),所以 -5 >> 1 结果是 -3,而不是 -2。这是新手最容易踩的坑。
一个完整的例子
把上面所有知识串起来:
num = int(input("请输入一个整数:"))
print(f"bin({num}) = {bin(num)}")
print(f"8位补码:{to_complement8(num)}")
print(f"{num} & 0b1111 = {num & 0b1111}")
print(f"{num} ^ 10 = {num ^ 10}")
print(f"~{num} = {~num}")
print(f"{num} << 2 = {num << 2}")
print(f"{num} >> 2 = {num >> 2}")
输入 -5,你会看到 -5 ^ 10 = -15,因为 ...11111011 ^ ...00001010 = ...11110001,这是 -15 的补码。这个例子很有价值:位运算的结果取决于操作数的无限精度补码,不是简单的 8 位或 32 位。
进阶建议
掌握了二进制、补码和位运算,你就摸到了计算机底层的大门。接下来可以探索:
- 浮点数编码:为什么
0.1 + 0.2 != 0.3?答案藏在 IEEE 754 标准里。 - 字符编码:ASCII 和 Unicode 如何用二进制表示文字。
- 位运算实战:权限控制(Unix 文件权限)、状态标志、图像处理中的像素操作。
- CPU 加法器:补码如何让硬件实现减法。
每当你看到 ~5 = -6 这种“怪事”,别慌,在脑海中画出那个无限长的黑白棋盘,一切都会变得清晰。二进制是计算机的母语,学会它,你和机器的对话就更深了一层。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)