引言:一个看似愚蠢的问题

我们都知道计算机只认识 0 和 1。那么问题来了:计算机怎么表示负数?

你可能会说:“这还不简单,用一个 bit 表示符号不就行了?“——没错,这正是原码的思路。但如果事情真的这么简单,就不会有反码和补码这两个概念了。

事实上,人类为了让计算机高效地处理负数,先后发明了三种编码方案:原码(Sign-Magnitude)→ 反码(Ones’ Complement)→ 补码(Two’s Complement)。它们不是三种并列的选择,而是一部”打补丁”的进化史。今天所有计算机都用补码,而理解它为什么胜出,是理解计算机体系结构的第一课。

本文将带你走完这段进化之路,搞清楚:

  • 三种编码各是什么、怎么换算
  • 它们之间的内在关系
  • 原码和反码各自踩了什么坑
  • 补码如何一举解决所有问题
  • 补码在现代计算机中的实际应用

一、准备工作:模与”时钟哲学”

在正式进入三种编码之前,先建立一个关键直觉——模(Modulo)

想象一个只有 12 个刻度的时钟。现在时针指向 8 点,你想让它指向 3 点,有两种拨法:

  • 逆时针拨 5 格:8 − 5 = 3
  • 顺时针拨 7 格:8 + 7 = 15,转满一圈回到 3

也就是说,在时钟的世界里,“减 5”和”加 7”完全等价,因为 5 + 7 = 12,而 12 恰好是时钟转一圈的周期——这就是”模”。

核心思想:在有模的系统中,减法可以转化为加法。

计算机的寄存器就是一个”时钟”:一个 8 位寄存器只能表示 0 ~ 255,共 256 个数,超过 255 就会发生溢出,自动”转回”0。所以 8 位系统的模就是 2⁸ = 256。

请记住这个思想,它是补码一切魔法的来源。


二、原码:最直觉的方案

2.1 定义

原码的思路简单粗暴:最高位当符号位(0 表示正,1 表示负),其余位表示数值的绝对值。

以 8 位为例:

十进制原码
+50000 0101
−51000 0101
+00000 0000
−01000 0000

2.2 原码的问题

原码对人友好(看一眼就知道是多少),但对计算机极不友好。关键问题出在加减法上。

试着用原码算 5 + (−5):

  0000 0101   (+5)
+ 1000 0101   (−5)
-----------
  1000 1010   (−10) ← 结果完全错误!

如果直接把两个原码丢进加法器,符号位会跟着一起参与运算,得到荒谬的结果。要让原码正确工作,硬件必须:

  1. 先比较两个数的符号;
  2. 符号相同则数值相加,符号不同则比较绝对值大小、用大的减小的;
  3. 再根据规则确定结果的符号位。

这意味着 CPU 里既要造加法器,又要造减法器,还要造一堆判断逻辑——电路复杂、成本高、速度慢

此外,原码还有一个尴尬的特性:0 有两种表示+0−0),浪费了一个编码,还给比较运算带来麻烦。

结论:原码适合人看,不适合机器算。


三、反码:过渡性的修补

3.1 定义

反码的规则:

  • 正数:反码 = 原码(不变)
  • 负数:符号位不变,其余各位按位取反(0 变 1,1 变 0)
十进制原码反码
+50000 01010000 0101
−51000 01011111 1010
+00000 00000000 0000
−01000 00001111 1111

3.2 反码的进步与残留问题

反码的重大意义在于:减法可以被转化成加法了。

计算 5 − 5,即 5 + (−5),用反码试试:

  0000 0101   (+5 的反码)
+ 1111 1010   (−5 的反码)
-----------
  1111 1111   (−0 的反码)

结果是 1111 1111,即 −0。方向对了!只要把符号位当成普通位一起加,再把溢出的进位”绕回来”加到最低位(这叫循环进位 / end-around carry),反码加法就能得到正确结果。

但反码仍有两大顽疾:

  1. 0 依然有两种表示0000 00001111 1111);
  2. 加法需要处理”循环进位”,硬件依然别扭,多了一步修正逻辑。

反码是一次伟大的中间探索——它证明了”取反”可以把减法变加法,但还差最后临门一脚。


四、补码:终极答案

4.1 定义

补码的规则:

  • 正数:补码 = 原码(不变)
  • 负数:补码 = 反码 + 1
十进制原码反码补码
+50000 01010000 01010000 0101
−51000 01011111 10101111 1011
+00000 00000000 00000000 0000
−01000 00001111 11110000 0000 ← 进位溢出,与 +0 统一!

注意最后一行:−0 的补码加上 1 之后变成了 1 0000 0000,第 9 位的进位被丢弃,结果就是 0000 0000——+0 和 −0 统一了!

4.2 为什么”取反加一”是对的?用模来解释

回到时钟的例子:在模为 256 的 8 位系统里,−5 等价于 256 − 5 = 251,即 1111 1011

“按位取反”的数学本质是:255 − x(因为 8 位全 1 就是 255)。 “取反再加一”就是:255 − x + 1 = 256 − x

所以,负数 x 的补码,就是它在模 2⁸ 系统里的等价正数。这不是巧合,而是严格的数学构造:

$$\text{补码}(x) \equiv x \pmod{2^n}$$

4.3 补码的神奇之处

① 减法彻底消失

计算 5 − 5 = 5 + (−5):

  0000 0101   (+5 的补码)
+ 1111 1011   (−5 的补码)
-----------
1 0000 0000   ← 最高位进位溢出,自然丢弃
= 0000 0000   (0) ✓ 完美!

不需要任何修正逻辑,把符号位当普通位一起加,溢出自动丢弃,结果天然正确。再试一个:5 − 7 = 5 + (−7):

  0000 0101   (+5)
+ 1111 1001   (−7 的补码)
-----------
  1111 1110   ← 这是 −2 的补码 ✓

② 0 只有一种表示

0000 0000 就是唯一的 0,没有正负之分。

③ 多赚一个数

8 位原码/反码的范围是 −127 ~ +127,而补码的范围是 −128 ~ +1271000 0000 不再是无用的 −0,而是表示 −128。

④ 硬件极简

CPU 只需要一个加法器就能同时处理加法和减法,无需比较符号、无需循环进位。A − B 在硬件层面就是 A + (B 的补码)。这就是为什么所有现代计算机——从 x86 到 ARM 到 RISC-V——全部使用补码。

4.4 一张表总结换算关系

正数负数
原码 → 反码不变符号位不变,其余取反
反码 → 补码不变加 1
补码 → 原码不变减 1 后取反(等价于:再取反加一)

记忆口诀:“正数三码合一,负数取反加一。”


五、补码在实际编程中的应用

理论讲完,看看补码如何渗透在你每天写的代码里。

5.1 整数溢出与”环绕”

int8_t a = 127;   // 0111 1111
a = a + 1;        // 变成 1000 0000,即 -128!

补码解释了为什么整数溢出会”绕圈”:127 + 1 不是 128,而是跳到最小值 −128。理解补码,就理解了一切整数溢出行为。

5.2 判断溢出:双符号位法

溢出检测的经典规则:两个正数相加得负数,或两个负数相加得正数,即为溢出(一进一出的进位不一致)。这正是 CPU 状态寄存器中 OF(Overflow Flag)的判断依据。

5.3 位运算的语义

~5      # 结果是 -6

为什么?因为 ~ 是按位取反:0000 01011111 1010,而这正是 −6 的补码。于是有恒等式 ~x = -x - 1,取反加一求相反数也就不再神秘:-x = ~x + 1

5.4 补码快速求相反数

对任何数(包括负数)“取反加一”,得到的就是它的相反数:

int neg = ~x + 1;   // 等价于 -x

编译器和硬件都在悄悄用这个公式。

5.5 无符号数与有符号数共用加法器

正是因为补码的模运算本质,同一段二进制 1111 1011,解释成有符号是 −5,解释成无符号是 251——两者的加法电路完全相同。这就是 C 语言中 intunsigned int 可以共用同一套运算硬件的原因,也是类型转换时”数值突变”的根源。

5.6 浮点数的对照

有趣的是,IEEE 754 浮点数没有采用补码,而是用”符号位 + 原码”的方式表示尾数。为什么?因为浮点运算的核心是乘除(对阶后尾数相乘),符号与数值分离反而更方便。这反过来印证了一个道理:编码方案是为运算需求服务的,没有银弹,只有权衡。


六、思考

  1. 为什么补码能多表示一个 −128? 因为原码/反码中 0 占了两个编码(+0、−0),补码把它们统一后,空出的 1000 0000 就用来表示 −128。

  2. 8 位补码 1000 0000 的相反数是多少? 取反加一:0111 1111 + 1 = 1000 0000,还是它自己!−128 没有对应的 +128,这是补码不对称性的代价。abs(-128) 在 8 位下溢出,程序员必须小心。

  3. 已知某数补码为 1111 0110,它的真值是多少? 取反加一:0000 1001 + 1 = 0000 1010 = 10,所以真值是 −10

  4. 为什么计算机不用原码? 一句话:原码让加法和减法无法共用电路,0 还有两种表示。


七、总结

维度原码反码补码
表示方法符号位 + 绝对值负数按位取反负数取反加一
0 的表示两种两种一种
8 位取值范围−127 ~ +127−127 ~ +127−128 ~ +127
减法转加法✅(需循环进位)✅(无需修正)
硬件成本
现代计算机使用

一句话总结全文:

原码是给人看的,补码是给机器用的。补码用”模”的数学思想,把减法化为加法、把正负 0 统一、把电路简化到极致——这就是它能统治整个计算机世界的原因。

下次当你在调试器里看到一个整数神秘地从 2147483647 跳成 −2147483648 时,请微笑着想起这篇文章——那不是 bug 的玄学,那是补码的呼吸。

评论区