Category 限定情报

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

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

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

想象一栋有 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法(标准方法)

写出绝对值对应的二进制

每一位取反(0变1,1变0),得到反码

反码加 1,得到补码

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

从右往左找到第一个 1

这个 1 左边所有的位取反

这个 1 及其右边所有的位保持不变

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

20 的二进制

00010100

取反(反码)

11101011

加 1(补码)

11101100

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

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

三种表示法的对比

数值原码反码补码

+5000001010000010100000101

-5100001011111101011111011

+0000000000000000000000000

-01000000011111111不存在

+127011111110111111101111111

-127111111111000000010000001

-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 位)

输入整数:

计算

+20

-128

特别注意:输入 -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}")

Copyright © 2088 宅鬼游戏社 - 二次元游戏活动情报站 All Rights Reserved.
友情链接
top