二进制与计算机
中等7从电灯开关到计算机大脑:二进制、补码与位运算
你玩过最老的游戏机吗?它的画面可能只是简单的黑白色块,但背后却藏着计算机最核心的秘密:一切信息,最终都变成了 0 和 1。就像家里的电灯开关,只有“开”和“关”两种状态,计算机里数不清的微小开关(叫做晶体管)就是用“开”表示 1,“关”表示 0。我们把这种只用 0 和 1 表示数字的方法叫做 二进制。
而我们平时用的 0~9 的数字叫做十进制。二进制和十进制可以互相转换。例如,十进制数 5 在二进制中就是 101:意思是 1 个 4(2 的 2 次方)、0 个 2、1 个 1,加起来等于 5。
计算机内部所有数据(数字、文字、图片、声音)最终都被转换成了 0 和 1 的序列。二进制是计算机世界的“通用语言”。学会了它,你就迈出了理解计算机内部工作原理的第一步。
1. 在 Python 中玩转二进制
Python 非常友好,它直接用 0b 开头来写二进制数。比如 0b101 就是十进制的 5。Python 还能用 bin() 函数把一个十进制数转换成二进制字符串(带 0b 前缀)。
代码示例:
# Python中的二进制表示
a = 0b101 # 这就是十进制的5
b = 0b110 # 十进制的6
print(a) # 输出5
print(b) # 输出6
print(a + b) # 输出11,二进制是0b1011
# 用bin()把十进制转成二进制字符串
num = 10
bin_str = bin(num)
print(bin_str) # 输出0b1010
# 也可以从二进制字符串转回十进制
c = int('0b1010', 2)
print(c) # 输出10
?? 新手容易犯的错误
- 忘记
0b前缀:直接写101会被当成十进制 101,不是二进制。 - 把
bin()的结果当成整数来计算:bin(5)返回的是字符串'0b101',不能直接做数学运算。如果要计算,要先用int(..., 2)转成整数。 - 误以为二进制只能表示正数:其实二进制也能表示负数,但需要用到“补码”这个技巧。
2. 计算机怎么表示负数?——原码、反码、补码
我们知道了计算机用二进制表示正数,但负数怎么表示呢?这就要用到 原码、反码和补码 这三个概念。为了简单,我们先假设用 8 位二进制(一个字节)来举例,最左边的一位是符号位(0 表示正,1 表示负)。
2.1 原码:直接戴个“符号”帽子
原码就是直接给数字戴上符号。例如:
- +5 的原码是
00000101 - -5 的原码是
10000101
这种表示方法最直观,但有个大问题:减法很难做。比如 5 + (-5) 如果用原码直接加,会得到 00000101 + 10000101 = 10001010(即 -10),明显错了。
2.2 反码:把“正”变成“反”
- 正数的反码和原码一样。
- 负数的反码:符号位不变,其他位取反(0 变 1,1 变 0)。
-5 的反码就是 11111010(因为原码 10000101 除了符号位,其他位取反得到 11111010)。
反码解决了部分问题,但还有一个小 bug:0 会出现两种表示(00000000 和 11111111),而且加法仍然不够简单。
2.3 补码:让减法变成加法
补码 才是计算机真正使用的负数表示法:
- 正数的补码和原码一样。
- 负数的补码:反码加 1。
-5 的补码 = 反码 11111010 再加 1 = 11111011。
为什么计算机最终使用补码来表示负数呢?因为补码可以让 减法变成加法,电路设计更简单。就像时钟:如果现在是 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^8,结果溢出后变成 0,相当于 5 + (-5) = 0。
生活中的例子:你的零花钱本来有 5 元,妈妈要扣掉 5 元。如果用“补码”思路,妈妈不是直接拿走 5 元,而是给你加一笔“反方向”的 251 元,结果你的钱变成了 256 元,因为钱包只能放 0~255 元,超出 256 就归零,最后钱包空了。这听起来绕,但计算机用这种方式做减法又快又准。
2.4 Python 模拟固定位数的补码
Python 中的整数是无限精度的,但我们可以用位运算来模拟固定位数的补码。下面的代码展示了如何求一个数的 8 位补码表示:
# 模拟8位补码表示
def to_complement8(n):
# 如果n在-128到127之间,返回其8位补码的二进制字符串(含符号位)
if n >= 0:
# 正数的补码就是原码,直接取低8位
return format(n & 0xFF, '08b')
else:
# 负数的补码:先取绝对值,取反,加1,并与0xFF取低8位
return format(((-n) ^ 0xFF) + 1 & 0xFF, '08b')
print(to_complement8(5)) # 输出 00000101
print(to_complement8(-5)) # 输出 11111011
通过这个例子,你可以看到 +5 和 -5 的补码互为“互补”,相加正好得到 1 00000000(高位溢出忽略)。这正是计算机处理负数的神奇之处。
?? 新手容易犯的错误
- 以为 Python 中负数直接用原码:打印
bin(-5)得到-0b101,这其实是 Python 为了方便显示,内部存储仍然是补码。 - 弄错补码的位数:如果不指定位数(比如只做 8 位),求补码时一定要用
& 0xFF截断,否则无限精度下负数补码的前面有无数个 1。 - 认为反码和补码在实际中还用:现代计算机只使用补码,原码和反码只是学习过程中的过渡概念。
3. 补码与位运算:为什么 ~5 等于 -6?
有了补码的知识,我们就能理解 Python 中那些“奇怪”的位运算结果了。位运算直接操作二进制位,速度很快。Python 支持 6 种位运算符:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
但是 Python 的整数是无限精度的,所以负数以无限长的补码形式存在(前面有无穷多个 1)。因此,~5 的结果是 -6,而不是我们可能以为的 0b...11111010(8 位取反)。我们来一步步分析:
- 5 的补码(无限位):
...00000101(前面无数个 0) - 按位取反:
...11111010,这是一个负数的补码。我们把它转回十进制:先减 1 得...11111001,再取反(除符号位)得...00000110,即 6,加上负号就是 -6。所以~5 = -6。
类似地,~(-5) 得到 4,因为 ...11111011 取反为 ...00000100,即 4。
生活中的例子:假设你有一个无限长的黑白棋盘,白色格子表示 0,黑色格子表示 1。5 的棋盘是……全白、白、黑、白、黑(从右往左:101)。你把它全部翻转颜色,得到全黑、黑、白、黑、白。这个新棋盘代表的数就是 -6。
3.1 其他位运算举例
其他位运算同样基于补码。下面用代码展示一些典型例子:
a = 5 # 二进制 101
b = 3 # 二进制 011
print(a & b) # 按位与:101 & 011 = 001 => 1
print(a | b) # 按位或:101 | 011 = 111 => 7
print(a ^ b) # 按位异或:101 ^ 011 = 110 => 6
print(~a) # 按位取反:~5 = -6
print(~(-5)) # 按位取反:~(-5) = 4
# 移位运算
print(a << 1) # 左移1位:101 << 1 = 1010 => 10(相当于乘以2)
print(a >> 1) # 右移1位:101 >> 1 = 10 => 2(相当于整除2,但注意负数右移)
print(-5 >> 1) # 负数右移:-5补码右移,高位补1,结果是-3
特别注意:负数右移时,高位补的是 1(算术右移),所以结果仍然是负数。左移时,负数左移也会保持补码特性。
?? 新手容易犯的错误
- 以为
~5是0b11111010(8 位取反):因为 Python 整数无限精度,实际结果是 -6,而不是一个很大的正数。 - 混淆逻辑运算符
not和位运算符~:not 5返回False,~5返回 -6。 - 移位时忘记负数的符号扩展:正数右移高位补 0,负数右移高位补 1。
4. 完整示例:综合运用二进制、补码和位运算
下面是一个完整的程序,它展示了如何读入一个十进制整数,输出它的二进制表示、8 位补码,并对它进行简单的位运算。
# 综合示例:二进制、补码与位运算
def to_complement8(n):
"""返回一个整数在8位补码下的二进制字符串(含符号位)"""
if n >= 0:
return format(n & 0xFF, '08b')
else:
return format(((-n) ^ 0xFF) + 1 & 0xFF, '08b')
# 输入一个整数
num = int(input("请输入一个整数(比如-5或12):"))
# 显示Python内部的二进制表示(注意:负数是带负号的二进制)
print(f"bin({num}) = {bin(num)}")
# 模拟8位补码
complement8 = to_complement8(num)
print(f"8位补码表示:{complement8}")
# 进行一些位运算
print(f"{num} & 0b1111 = {num & 0b1111}") # 按位与低4位
print(f"{num} ^ 0b1010 = {num ^ 0b1010}") # 按位异或
print(f"~{num} = {~num}") # 取反
print(f"{num} << 2 = {num << 2}") # 左移2位(乘以4)
print(f"{num} >> 2 = {num >> 2}") # 右移2位(除以4取整,注意负数)
尝试输入 5,你会看到:
bin(5) = 0b101
8位补码表示:00000101
5 & 0b1111 = 5
5 ^ 0b1010 = 15
~5 = -6
5 << 2 = 20
5 >> 2 = 1
尝试输入 -5,你会看到:
bin(-5) = -0b101
8位补码表示:11111011
-5 & 0b1111 = 11 (因为-5的补码低4位是1011,即11)
-5 ^ 0b1010 = -1 (因为11111011 ^ 00001010 = 11110001,即-15?不对,我们实际算一下:...11111011 ^ ...00001010 = ...11110001,这是-15的补码,所以结果是-15?但程序输出是-1?等一下,我们需要检查:-5的补码无限位...11111011,与0b1010(即...00001010)异或得到...11110001,这是-15的补码。但为什么示例写-1?我错了,需要纠正)
注意:上面的例子中 -5 ^ 0b1010 并不是你想象的那样简单,因为 0b1010 是正数 10(前面无数个0),异或后得到的是 -15。为了演示清晰,最好用固定长度的比较。我们可以改成 -5 ^ 10,结果确实是 -15。
更正后的示例(使用变量):
# 修正后的位运算示例(更清晰)
x = -5
y = 10 # 二进制 1010
print(f"{x} ^ {y} = {x ^ y}") # 实际结果:-15
5. 相关指引
现在你已经掌握了二进制、补码和位运算的基础知识。接下来可以学习:
- 数据编码:如何用二进制表示小数(浮点数)、字符(ASCII/Unicode)、图片(RGB像素)。
- 逻辑运算与条件判断:位运算经常用在权限控制(如 Unix 文件权限)、状态标志中。
- 底层优化:用位运算代替乘除法(左移乘2、右移除2)可以加速游戏或图形处理。
- 计算机组成:CPU 中的加法器是如何利用补码实现减法的。
记住,每次你看到 Python 中奇怪的 ~5 = -6,都可以在脑海中把数字想象成无限长的黑白棋盘,这样就不会迷惑了。二进制是计算机的母语,学会了它,你和计算机的交流就更深了一步!
例题精讲
计算机内部为什么要采用二进制来表示数据?
二进制数(1011)₂转换为十进制数是多少?
在Python中,执行int('1010', 2)的结果是10。
下面代码将十进制数25转换为二进制字符串(不含前缀'0b'),请补全:\ndef dec_to_bin(n):\n return ___\n\nprint(dec_to_bin(25))计算二进制数1101和1011的和,结果以十进制形式输出,请补全代码:\nresult = ___\nprint(result)