CC++ & Algorithm

二进制与计算机

中等7
语言版本:C++Python
概述:计算机为什么只认识0和1?用开关灯比喻,带你了解二进制的基础知识,并用Python代码体验二进制数的表示和转换。

从电灯开关到计算机大脑:二进制、补码与位运算

你玩过最老的游戏机吗?它的画面可能只是简单的黑白色块,但背后却藏着计算机最核心的秘密:一切信息,最终都变成了 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 会出现两种表示(0000000011111111),而且加法仍然不够简单。

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(算术右移),所以结果仍然是负数。左移时,负数左移也会保持补码特性。

?‍? 新手容易犯的错误

  • 以为 ~50b11111010(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,都可以在脑海中把数字想象成无限长的黑白棋盘,这样就不会迷惑了。二进制是计算机的母语,学会了它,你和计算机的交流就更深了一步!

例题精讲

1单选题

计算机内部为什么要采用二进制来表示数据?

A二进制运算规则最简单
B二进制容易用电路的高低电平实现
C人类习惯使用二进制
D二进制可以节省存储空间
2单选题

二进制数(1011)₂转换为十进制数是多少?

A9
B11
C13
D7
3判断题

在Python中,执行int('1010', 2)的结果是10。

4填空题
下面代码将十进制数25转换为二进制字符串(不含前缀'0b'),请补全:\ndef dec_to_bin(n):\n    return ___\n\nprint(dec_to_bin(25))
5填空题
计算二进制数1101和1011的和,结果以十进制形式输出,请补全代码:\nresult = ___\nprint(result)