负数怎么表示——原码、反码、补码
计算机只认识 0 和 1,那么负数是怎么表示的?
例如,十进制的 5 可以写成二进制 00000101,但 -5 应该怎么表示?
最直接的想法是拿出一位来表示正负号,但这样又会引出新的问题:负数该怎么参与加法?为什么 8 位整数的范围是 -128 到 127,而不是 -127 到 128?
要理解这些问题,就需要了解计算机表示有符号整数的三种方式:原码、反码和补码。
下面以 8 位二进制为例,通过图形和交互演示,逐步理解它们的区别。
一、8 位二进制如何表示正负数?
8 位二进制共有 2^8 = 256 种不同的位模式。
如果按照无符号整数解释,可以表示 0 到 255;如果按照补码解释,可以表示 -128 到 127。
注意:这里展示的是原码。补码虽然也可以通过最高位识别正负,但最高位在数值计算中具有负权重,不能简单理解为独立符号位。
二、原码:最直观的表示方式
原码(Sign-Magnitude)采用最高位表示符号,其余位表示绝对值。
它的问题在于有两个零:
两个不同的二进制位模式表示同一个数值 0,增加了数值比较和运算的复杂度。
另外,原码不能简单地将全部二进制位直接相加来完成有符号加法。
例如,按 8 位原码直接计算 5 + (-3),会得到 10001000,按原码解释是 -8,而正确答案应该是 2。
因此,原码运算需要额外处理操作数符号及绝对值的加减。
三、反码:把二进制位取反
反码(One's Complement)的规则是:
- 正数的反码与原码相同。
- 负数的反码由对应正数的二进制位逐位取反得到。
以 -5 为例:
每一位都取反:0 变成 1,1 变成 0。
反码依然存在两种零表示:
+0 = 00000000 -0 = 11111111
因此,反码同样无法充分利用所有位模式来表示不同的整数。
四、补码:取反再加 1
补码(Two's Complement)解决了原码和反码中正零、负零重复表示的问题。
对于负数,一个常用的补码计算方法是:
先写出绝对值的固定位宽二进制,再逐位取反,最后加 1。
图解:-5 的补码计算
11111010 (取反)
11111011 (加 1,得到 -5 的补码)
因此,-5 的 8 位补码为 11111011。
三种表示方式对比
符号位 + 绝对值
正数位模式取反
取反再加 1
五、为什么补码可以直接做加减法?
补码的核心原理是模运算。
8 位二进制有 256 种位模式,固定宽度加法相当于对 256 取模。
例如,计算 5 - 3:
00000101
11111101
00000010
+ 11111101
------------
1 00000010
↑
丢弃超出 8 位的进位
结果:00000010 = 2
也可以使用十进制模运算理解:
5 + (256 - 3) = 258 258 mod 256 = 2
这就是补码能够用统一的固定位宽加法机制处理减法的原因。
六、4 位补码环形演示
如果只有 4 位二进制,就有 16 种位模式。
按照补码解释,这 16 种位模式分别对应 -8 到 7。
下面的交互图可以观察数值变化时,补码如何在固定位宽的位模式中循环。
当前补码:1101
对应无符号值:13
从环形图可以发现:
- 0 到 7 使用
0000到0111。 - -8 到 -1 使用
1000到1111。 - 在 4 位模 16 加法中,7 加 1 得到位模式
1000,解释为 -8。 - -1 的位模式
1111加 1 后,保留低 4 位得到 0。
七、交互式补码计算器
输入 -128 到 127 范围内的整数,即可查看对应的 8 位原码、反码和补码。
特别注意:8 位原码和反码的表示范围为 -127 到 127,不能表示 -128。而 8 位补码可以表示 -128,对应位模式为 10000000。
八、补码的最高位为什么表示负数?
在 8 位补码中,可以将每一位理解为具有不同的数值权重。
与无符号整数不同,补码最高位的权重是 -128,而不是 +128。
| 位位置 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 位权重 | -128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| -1 的位 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
= -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= -1
因此,补码中的最高位并不只是一个独立的正负标志,它也参与整个数值的计算。
九、原码、反码、补码完整对比
| 数值 | 原码 | 反码 | 补码 |
|---|---|---|---|
| +5 | 00000101 | 00000101 | 00000101 |
| -5 | 10000101 | 11111010 | 11111011 |
| +0 | 00000000 | 00000000 | 00000000 |
| -0 | 10000000 | 11111111 | 不存在独立表示 |
| +127 | 01111111 | 01111111 | 01111111 |
| -127 | 11111111 | 10000000 | 10000001 |
| -128 | 无法表示 | 无法表示 | 10000000 |
十、补码溢出图解
8 位补码的表示范围为 -128 到 127。如果真实数学结果超出这个范围,就会发生有符号溢出。
数学结果为 128,超出 8 位补码的最大值 127。保留低 8 位后得到 10000000,按照 8 位补码解释为 -128。
需要注意,这只是固定 8 位、按模 256 截断的运算结果。
- Python 的普通整数不会因为超过 8 位就自动环绕。
- Java 的有符号整数存在固定宽度的溢出行为,但
byte参与算术运算时通常先提升为int。 - C/C++ 中的有符号整数算术溢出通常属于未定义行为,不能直接依赖这种回绕结果。
十一、Python 实现补码转换
Python 可以通过位运算模拟指定位宽的补码。
实例
"""将整数转换为指定位宽的补码字符串。"""
if bits < 1:
raise ValueError("位宽必须大于等于 1")
minimum = -(1 << (bits - 1))
maximum = (1 << (bits - 1)) - 1
if not minimum <= n <= maximum:
raise ValueError("整数超出补码表示范围")
mask = (1 << bits) - 1
return format(n & mask, f"0{bits}b")
def from_twos_complement(binary):
"""将补码字符串转换为十进制整数。"""
if not binary or any(c not in "01" for c in binary):
raise ValueError("请输入有效的二进制字符串")
unsigned = int(binary, 2)
bits = len(binary)
if binary[0] == "1":
return unsigned - (1 << bits)
return unsigned
for n in [5, -5, -20, 0, 127, -128]:
binary = to_twos_complement(n)
restored = from_twos_complement(binary)
print(f"{n:4d} -> {binary} -> {restored}")
执行结果:
5 -> 00000101 -> 5 -5 -> 11111011 -> -5 -20 -> 11101100 -> -20 0 -> 00000000 -> 0 127 -> 01111111 -> 127 -128 -> 10000000 -> -128
十二、常见问题
为什么补码只有一种零表示?
因为在固定 n 位补码中,0 的位模式只有全零这一种。对零取反加 1,按固定宽度截断后仍然得到全零。
为什么 -128 取反加 1 还是 -128?
在 8 位补码中,-128 的位模式为 10000000。逐位取反加 1 后,保留低 8 位仍是 10000000。
原因是 +128 超出 8 位补码的表示范围,因此最小负数没有可表示的正数相反数。
补码和二进制是同一个概念吗?
不是。二进制是使用 0 和 1 表示数据的计数和编码基础;补码则是一种解释固定位宽二进制位模式的有符号整数编码方式。
总结
原码、反码和补码都是有符号整数的编码方式。
原码比较直观,但存在正零、负零和符号运算复杂的问题;反码通过逐位取反表示负数,但仍然存在两个零;补码则只保留一种零表示,并允许利用模运算统一处理固定宽度的加减法。
理解补码时,最值得记住的是:
- 负数的补码可以通过对应正数的二进制取反加 1 得到。
- 8 位补码的范围是 -128 到 127。
- 补码最高位的位权重为负数。
- 固定 n 位加法可以理解为模
2^n运算。 - 不同编程语言对有符号整数溢出的处理规则不一定相同。
