负数怎么表示 -- 原码、反码、补码
本讲你将理解:为什么计算机里表示负数不像「加个负号」那么简单,以及补码是如何巧妙地解决了所有问题。
生活化类比:旅馆的楼层编号
想象一栋有 256 层的旅馆。
如果用自然数编号,只能用 0 到 255。
但如果我们想把「地下 128 层」到「地上 127 层」都编进去呢?
一个巧妙的做法:把编号 128~255 重新解释为 -128~-1。
编号 128 就成了 -128,编号 255 就成了 -1,编号 0 还是 0。
这就是补码的核心思想——在有限的 bit 范围里,把数值空间的「后半段」重新解读为负数。
最朴素的想法:原码
表示负数最直接的想法:用最高位当符号位。
- 最高位 0 = 正数
- 最高位 1 = 负数
- 剩下的位表示绝对值
以 8 位为例:
看起来很合理,对吧?但原码有两个致命问题。
问题一:+0 和 -0 同时存在
正 0 是 00000000,负 0 是 10000000。
同一个数值有两种表示方法。
这意味着每次做比较操作(判断两个数是否相等),硬件都要额外处理这个特殊情况。
问题二:加法器不能做减法
用原码计算 5 + (-3):
5 + (-3) 应该等于 2,但用原码直接加出来却是 -8。
这意味着 CPU 必须单独设计一套减法电路,而不能复用已有的加法电路。
原码的问题可以概括为:直观但不实用。它适合人类阅读,但不适合电路实现。
时钟的启示:模运算
在继续之前,我们需要理解一个关键概念——模运算。
看一个时钟:钟面上只有 12 个刻度。
从 10 点出发,你想调到 7 点,有两种走法:
方法一(逆时针):10 - 3 = 7(减法)
方法二(顺时针):10 + 9 = 19,19 mod 12 = 7(加法)
在「模 12」的系统中,-3 和 +9 是等价的(因为 -3 + 12 = 9)。
更一般地说:在模 M 的系统中,减一个数等于加 (M - 该数)。
补码就是把这个思想应用到了二进制上。对 8 位二进制来说,模是 2^8 = 256。
-3 的补码 = 256 - 3 = 253 = 二进制 11111101。
于是 5 + (-3) 可以变成 5 + 253 = 258。
但在 8 位系统中,258 超过了 255 的表示上限,最高位的进位「溢出」被丢弃,剩下 258 mod 256 = 2。
加法得到了减法的正确结果。
补码环形图(交互演示)
下面这个环形图直观展示了 4 位补码系统。
4 位可以表示 16 个值(0~15),补码将其重新解读为 -8~+7。
拖动滑块改变数值,观察指针在圆环上的位置。
补码环形图(4 位系统,范围 -8 ~ +7)
二进制表示:0000 | 无符号解读:0
Plotly 补码环形图(scatterpolar 极坐标)
下图用 Plotly.js 的极坐标展示 4 位补码的 16 个刻度。蓝色=正数区域,红色=负数区域,绿色=零点。
环形的含义:
- 顺时针方向 = 数值增加
- 绕一圈(超过最大值)= 回到起点(模运算的「溢出」效果)
- 红色区域 = 负数(补码解读),蓝色区域 = 正数
- 内圈标注的是无符号解读(对应 0~15),外圈标注的是补码解读(-8~+7)
从图中可以看到,补码本质上是把数值空间折叠成了一个环。
正数走到尽头(+7 之后)自然地变成了负数(-8),减法和加法在环上是同一个方向。
补码的计算规则
手工计算一个数的补码,有两个等价的快方法:
方法一:取反+1法(标准方法)
- 写出绝对值对应的二进制
- 每一位取反(0变1,1变0),得到反码
- 反码加 1,得到补码
方法二:从右往左法(速算方法)
- 从右往左找到第一个 1
- 这个 1 左边所有的位取反
- 这个 1 及其右边所有的位保持不变
以 -20 为例(8 位系统),两种方法验证:
验证:-20 的补码 11101100,按无符号解读 = 236。
236 + 20 = 256,正好是一个模。这说明计算正确。
三种表示法的对比
| 数值 | 原码 | 反码 | 补码 |
|---|---|---|---|
| +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 |
注意三个关键差异:
- 补码没有 -0:全 1 不再浪费在 -0 上,而是表示 -1。
- 补码多一个负数:8 位补码范围是 -128 ~ +127,比原码的 -127~+127 多了一个 -128。
- 补码让加法统一:加正数和加负数用同一套电路。
这就是为什么 Java 的
byte类型范围是 -128~127,C 语言的signed char也是 -128~127。它们使用的都是 8 位补码。
交互演示:补码计算器
在下面输入一个 8 位范围内的整数(-128 ~ 127),查看它的原码、反码和补码。
补码计算器 (8 位)
特别注意:输入 -128 时,补码是 10000000。
这个数的绝对值是 128,但 128 在 8 位正数范围(0~127)之外。
所以 -128 没有对应的正数表示——这是补码系统的一个有趣特性。
补码的溢出
8 位补码范围是 -128~127。
如果计算结果超出这个范围,就会发生溢出。
这再次呼应了前面的环形图——走到尽头后,数值会「绕回来」。
在 C/C++ 中,有符号整数的溢出是未定义行为(编译器可以假设它不会发生),这是很多奇怪 bug 的根源。
代码演示:补码的完整实现
实例
def to_twos_complement(n, bits=8):
"""将整数 n 转换为指定位宽的补码字符串"""
if n < -2**(bits-1) or n > 2**(bits-1) - 1:
raise ValueError(f"数值 {n} 超出 {bits} 位补码范围 [-{2**(bits-1)}, {2**(bits-1)-1}]")
# 利用模运算:负数 n 的无符号对应值 = n + 2^bits
# Python 中直接用位掩码
mask = (1 << bits) - 1 # 比如 8 位: 0xFF = 255
unsigned_val = n & mask
return format(unsigned_val, f'0{bits}b')
def from_twos_complement(bin_str):
"""将补码字符串转换回十进制整数"""
bits = len(bin_str)
unsigned_val = int(bin_str, 2)
# 如果最高位是 1(负数),减去 2^bits
if bin_str[0] == '1':
return unsigned_val - (1 << bits)
else:
return unsigned_val
def show_conversion_steps(n, bits=8):
"""展示从 n 到补码的详细步骤"""
abs_bin = format(abs(n), f'0{bits}b')
print(f"数值: {n}")
print(f" Step 1: 绝对值 |{n}| = {abs(n)} → 二进制 = {abs_bin}")
if n >= 0:
print(f" Step 2: 正数的补码 = 原码 = {abs_bin}")
else:
# 取反
inverted = ''.join('1' if b == '0' else '0' for b in abs_bin)
print(f" Step 2: 取反(反码)= {inverted}")
# 加 1
carry = 1
result_list = list(inverted)
for i in range(bits - 1, -1, -1):
if carry == 0:
break
total = int(result_list[i]) + carry
result_list[i] = str(total % 2)
carry = total // 2
result = ''.join(result_list)
print(f" Step 3: 反码 + 1 = {result}")
comp = to_twos_complement(n, bits)
print(f" → 最终补码: {comp}")
# 验证
recovered = from_twos_complement(comp)
print(f" 验证: 补码 {comp} → 解读为 {recovered}")
print()
# 测试
print("=" * 50)
print("补码转换演示 (8位系统)")
print("=" * 50)
print()
for val in [5, -5, 0, -1, 127, -128]:
show_conversion_steps(val)
# 验证补码加法
print("=" * 50)
print("补码加法验证:减法变加法")
print("=" * 50)
def twos_add(a, b, bits=8):
"""补码加法(包含溢出处理)"""
mask = (1 << bits) - 1
result_unsigned = (a + b) & mask
# 解读有符号结果
if result_unsigned >= (1 << (bits - 1)):
signed_result = result_unsigned - (1 << bits)
else:
signed_result = result_unsigned
return signed_result
# 5 - 3 = 2
print(f"5 + (-3) = {twos_add(5, -3)} (期望: 2)")
# 127 + 1 = -128 (溢出)
print(f"127 + 1 = {twos_add(127, 1)} (期望: -128,发生了溢出)")
# -128 - 1 = 127 (溢出)
print(f"-128 + (-1) = {twos_add(-128, -1)} (期望: 127,发生了溢出)")
# 零相加
print(f"0 + 0 = {twos_add(0, 0)} (期望: 0)")
# 用 RUNOOB 字符验证:R=82, U=85 → 82 + (-85) = -3
print(f"\nRUNOOB 测试:R(82) + (-U(85)) = {twos_add(82, -85)} (期望: -3)")
# 展示补码的循环特性
print("\n补码环形(4位简化版,范围 -8~+7):")
print("数值 | 补码 | 无符号")
print("-" * 30)
for n in range(-8, 8):
comp = to_twos_complement(n, 4)
unsig = int(comp, 2)
print(f" {n:3d} | {comp} | {unsig:2d}")
