现在位置: 首页 > 计算机组成原理 > 正文

负数怎么表示 -- 原码、反码、补码

本讲你将理解:为什么计算机里表示负数不像「加个负号」那么简单,以及补码是如何巧妙地解决了所有问题。


生活化类比:旅馆的楼层编号

想象一栋有 256 层的旅馆。

如果用自然数编号,只能用 0 到 255。

但如果我们想把「地下 128 层」到「地上 127 层」都编进去呢?

一个巧妙的做法:把编号 128~255 重新解释为 -128~-1。

编号 128 就成了 -128,编号 255 就成了 -1,编号 0 还是 0。

这就是补码的核心思想——在有限的 bit 范围里,把数值空间的「后半段」重新解读为负数。


最朴素的想法:原码

表示负数最直接的想法:用最高位当符号位

  • 最高位 0 = 正数
  • 最高位 1 = 负数
  • 剩下的位表示绝对值

以 8 位为例:

+5 =
0
0
0
0
0
1
0
1
符号位 0=正
-5 =
1
0
0
0
0
1
0
1
符号位 1=负

看起来很合理,对吧?但原码有两个致命问题。

问题一:+0 和 -0 同时存在

正 0 是 00000000,负 0 是 10000000

同一个数值有两种表示方法。

这意味着每次做比较操作(判断两个数是否相等),硬件都要额外处理这个特殊情况。

问题二:加法器不能做减法

用原码计算 5 + (-3)

5 (原码)
00000101
+
-3 (原码)
10000011
=
直接相加
10001000
=
结果
-8

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)

0

二进制表示:0000  |  无符号解读:0

Plotly 补码环形图(scatterpolar 极坐标)

下图用 Plotly.js 的极坐标展示 4 位补码的 16 个刻度。蓝色=正数区域,红色=负数区域,绿色=零点。

正数区域 (0 ~ +7)
负数区域 (-8 ~ -1)
零点
溢出点 (+7→-8)

环形的含义:

  • 顺时针方向 = 数值增加
  • 绕一圈(超过最大值)= 回到起点(模运算的「溢出」效果)
  • 红色区域 = 负数(补码解读),蓝色区域 = 正数
  • 内圈标注的是无符号解读(对应 0~15),外圈标注的是补码解读(-8~+7)

从图中可以看到,补码本质上是把数值空间折叠成了一个环

正数走到尽头(+7 之后)自然地变成了负数(-8),减法和加法在环上是同一个方向。


补码的计算规则

手工计算一个数的补码,有两个等价的快方法:

方法一:取反+1法(标准方法)

  1. 写出绝对值对应的二进制
  2. 每一位取反(0变1,1变0),得到反码
  3. 反码加 1,得到补码

方法二:从右往左法(速算方法)

  1. 从右往左找到第一个 1
  2. 这个 1 左边所有的位取反
  3. 这个 1 及其右边所有的位保持不变

以 -20 为例(8 位系统),两种方法验证:

20 的二进制
00010100
取反(反码)
11101011
加 1(补码)
11101100

验证:-20 的补码 11101100,按无符号解读 = 236。

236 + 20 = 256,正好是一个模。这说明计算正确。


三种表示法的对比

数值原码反码补码
+5000001010000010100000101
-5100001011111101011111011
+0000000000000000000000000
-01000000011111111不存在
+127011111110111111101111111
-127111111111000000010000001
-128无法表示无法表示10000000

注意三个关键差异:

  1. 补码没有 -0:全 1 不再浪费在 -0 上,而是表示 -1。
  2. 补码多一个负数:8 位补码范围是 -128 ~ +127,比原码的 -127~+127 多了一个 -128。
  3. 补码让加法统一:加正数和加负数用同一套电路。

这就是为什么 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。

如果计算结果超出这个范围,就会发生溢出

127 + 1
-128
(正溢出,负数结果)
|
-128 - 1
+127
(负溢出,正数结果)

这再次呼应了前面的环形图——走到尽头后,数值会「绕回来」。

在 C/C++ 中,有符号整数的溢出是未定义行为(编译器可以假设它不会发生),这是很多奇怪 bug 的根源。


代码演示:补码的完整实现

实例

# 补码的完整实现与验证(runoob 演示)

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}")