Python中原码、反码和补码
困难4计算机里的正负数:原码、反码和补码
你有没有想过,计算机里面只认识0和1,它是怎么表示“负数”的呢?比如 -5 这个数,如果直接写成 101(二进制),那它到底是“负三”还是“五”?聪明的计算机科学家发明了三套“密码”——原码、反码和补码,专门用来解决这个问题。
其实这三套码就像我们生活中的两种常见工具:温度计和钟表。温度计告诉我们0度以上和以下怎么区分;钟表告诉我们怎么用加法来算减法。今天我们就用这两个比喻,加上Python代码,一起来认识它们。
为什么需要三种码?
计算机原本想得很简单:用最左边一个比特(bit)表示正负。0表示正,1表示负,剩下的比特表示数值大小。这就是原码。比如 +5 写成 00000101,-5 写成 10000101。看起来挺直接,对吧?
但是计算机做加减法的时候出问题了。如果想让计算机算 3 + (-5) ,你得先判断符号,再决定是做加法还是减法,还得考虑借位,非常麻烦。能不能让加减法都用同一个加法电路完成呢?
大家想到了钟表的妙处:钟表盘上只有1到12,如果现在是3点,想回拨5小时(也就是减5小时),我们可以直接加上7(因为 3-5 相当于 3+7,12点算作0,结果就是10点)。在数学里,这就是 模12 的运算。补码就是模仿钟表:把一个负数表示成它的“补数”,这样减法就变成了加法。
为此,人们先发明了反码作为过渡,最后得到了补码。计算机实际存储的都是补码。
原码:最直接的表示
规则:
- 最高位(最左边)是符号位,0表示正数,1表示负数。
- 剩下的位表示数值的绝对值(用二进制)。
生活例子:就像成绩单上的“+15分”和“-3分”,数字前面的正负号就是符号位,分数就是数值。
Python模拟(以8位为例):
对于正数,直接在前面补0到8位。
对于负数,先求出绝对值的二进制(用7位),然后在最前面加一个1。
def yuan_ma(num, bits=8):
"""返回原码的二进制字符串,长度为bits"""
if num >= 0: # 正数或0
return format(num, '0{}b'.format(bits))
else: # 负数
abs_bin = format(abs(num), '0{}b'.format(bits-1)) # 绝对值用后7位
return '1' + abs_bin # 前面加符号位1
print("+5的原码:", yuan_ma(5))
print("-5的原码:", yuan_ma(-5))
print(" 0的原码:", yuan_ma(0))
运行结果:
+5的原码: 00000101
-5的原码: 10000101
0的原码: 00000000
注意:0有两种原码表示:+0是00000000,-0是10000000。但计算机不喜欢有两个0,所以后面有了补码。
反码:为补码做准备
规则:
- 正数和0的反码 = 原码(不变)。
- 负数的反码 = 符号位保持1,其余各位(数值位)全部取反(0变1,1变0)。
生活例子:就像考试答案的“反着看”——把选择题选项全部反过来,但最前面的题号不变。
Python模拟:我们可以根据原码来生成反码。
def fan_ma(num, bits=8):
"""返回反码的二进制字符串"""
original = yuan_ma(num, bits) # 先得到原码
if num >= 0: # 正数直接返回原码
return original
else: # 负数:符号位不变,后面取反
inv_body = ''.join('1' if c == '0' else '0' for c in original[1:]) # 数值位取反
return '1' + inv_body
print("+5的反码:", fan_ma(5))
print("-5的反码:", fan_ma(-5))
print(" 0的反码:", fan_ma(0))
运行结果:
+5的反码: 00000101
-5的反码: 11111010
0的反码: 00000000
这里 +0 和 -0 的反码也不同:+0是00000000,-0是11111111。还是有两个0。
补码:计算机真正用的编码
规则:
- 正数和0的补码 = 原码(不变)。
- 负数补码 = 反码 + 1(并且忽略最高位的进位)。
生活例子:就像钟表上的“-3点”可以用“9点”表示(12-3=9)。计算机用补码,把负数变成它的“补数”,这样加减法就统一了。比如 5 + (-5) 用补码算就是 00000101 + 11111011 = 1 00000000,忽略最高位的进位1,得到00000000,正好是0。
Python模拟:可以用一种很巧妙的方法——直接用Python整数做位与运算(&)。因为Python的整数无限大,但我们可以让它只保留最后bits位:num & ((1 << bits) - 1)。
def bu_ma(num, bits=8):
"""返回补码的二进制字符串"""
if num >= 0: # 正数直接返回原码
return format(num, '0{}b'.format(bits))
else:
# 用位运算:num & (2^bits - 1) 得到补码对应的整数
comp_val = num & ((1 << bits) - 1) # 例如 -5 & 255 得到 251
return format(comp_val, '0{}b'.format(bits))
print("+5的补码:", bu_ma(5))
print("-5的补码:", bu_ma(-5))
print(" 0的补码:", bu_ma(0))
运行结果:
+5的补码: 00000101
-5的补码: 11111011
0的补码: 00000000
注意:+0和-0的补码都是00000000!这就是补码的好处:只有一个0,没有正负0的麻烦。
小窍门:负数补码也可以用“原码数值位取反再加1”来手工计算。比如 -5 原码 10000101,数值位 0000101 取反得 1111010,再加1得 1111011,整体符号位不变,就是 11111011。和上面结果一致。
总结与常见错误
总结
| 数字 | 原码 | 反码 | 补码 |
|---|---|---|---|
| +5 | 00000101 | 00000101 | 00000101 |
| -5 | 10000101 | 11111010 | 11111011 |
| 0 | 00000000 或 10000000 | 00000000 或 11111111 | 00000000 |
- 正数:三种码一模一样。
- 负数:补码 = 原码数值位取反 + 1(注意符号位不变)。
- 计算机只存补码,所以加减法都可以用一套加法电路,又快又省电。
常见错误
- 把原码当作计算机实际存储:错了!计算机内部统一用补码,原码只是方便人理解。
- 误认为负数补码就是反码加1,但忘了符号位:符号位始终是1,取反加1只针对数值位。比如 -5 的补码是 11111011,符号位1没变。
- 计算补码时直接对原码整体加1:比如拿着 10000101 直接加1得到 10000110,这是错的。应该先对数值位(后7位)取反,再加1。
- 以为0的补码有两种:不,补码下0只有一种表示:全0。
- 忽略位数:8位和16位的结果不同。比如 -5 在8位下是11111011,在16位下是11111111 11111011。
完整代码演示
下面把上面三个函数整合起来,一次性展示多个数字的三种码:
def show_codes(num, bits=8):
"""显示数字num的原码、反码、补码(bits位)"""
# 原码
if num >= 0:
original = format(num, '0{}b'.format(bits))
else:
abs_bin = format(abs(num), '0{}b'.format(bits-1))
original = '1' + abs_bin
# 反码
if num >= 0:
inverse = original
else:
inv_body = ''.join('1' if c == '0' else '0' for c in original[1:])
inverse = '1' + inv_body
# 补码
if num >= 0:
complement = original
else:
comp_val = num & ((1 << bits) - 1)
complement = format(comp_val, '0{}b'.format(bits))
print(f"数字 {num:>3}:原码={original} 反码={inverse} 补码={complement}")
# 测试几个数字
show_codes(5)
show_codes(-5)
show_codes(0)
show_codes(-1)
show_codes(127) # 最大正数
show_codes(-128) # 最小负数(8位范围-128~127)
运行结果:
数字 5:原码=00000101 反码=00000101 补码=00000101
数字 -5:原码=10000101 反码=11111010 补码=11111011
数字 0:原码=00000000 反码=00000000 补码=00000000
数字 -1:原码=10000001 反码=11111110 补码=11111111
数字 127:原码=01111111 反码=01111111 补码=01111111
数字 -128:原码=10000000 反码=11111111 补码=10000000
发现了吗?-128 的补码是 10000000,因为它的原码数值位是 0000000,取反得 1111111,加1得 10000000,进位被截断,结果符号位又变回1了。这正好是8位补码能表示的最小负数。
相关知识点指引
如果你还想了解更多,可以继续学习:
- 二进制基础:了解2的幂、位权、十进制与二进制互转(例如:
101表示1×4 + 0×2 + 1×1 = 5)。 - 位运算:Python中的
&(与)、|(或)、~(取反)、<<(左移)、>>(右移)。补码的计算离不开这些运算。 - Python整数无限大:Python的整数其实可以无限大,但当我们用
&截断成固定位数时,就模拟了定长整数的补码。 - 负数在计算机中的实际存储:比如用
bin(-5)会输出-0b101,这不是补码,但你可以用-5 & 0xFF得到补码对应的整数251。 - 溢出问题:如果两个正数相加结果超过了127(8位),补码会变成负数,这就是溢出,需要程序员小心判断。
下次你看到二进制 11111011,别再以为是“大数”了——它可能代表 -5 哦!
例题精讲
在8位二进制系统中,十进制数+5的原码、反码、补码分别是多少?
在计算机中,使用补码表示负数可以简化加法运算,因为减法可以转化为加法。
已知 x = -6,假设使用8位二进制,则 x 的补码表示为 ___。下列关于原码的描述,哪一个是错误的?
负数的反码与它的补码相差1(不考虑进位溢出)。