原码、反码与补码:计算机是如何"算计"正负数的?
一篇讲透计算机有符号数表示法
引言:一个看似愚蠢的问题
我们都知道计算机只认识 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 位为例:
| 十进制 | 原码 |
|---|---|
| +5 | 0000 0101 |
| −5 | 1000 0101 |
| +0 | 0000 0000 |
| −0 | 1000 0000 |
2.2 原码的问题
原码对人友好(看一眼就知道是多少),但对计算机极不友好。关键问题出在加减法上。
试着用原码算 5 + (−5):
0000 0101 (+5)
+ 1000 0101 (−5)
-----------
1000 1010 (−10) ← 结果完全错误!
如果直接把两个原码丢进加法器,符号位会跟着一起参与运算,得到荒谬的结果。要让原码正确工作,硬件必须:
- 先比较两个数的符号;
- 符号相同则数值相加,符号不同则比较绝对值大小、用大的减小的;
- 再根据规则确定结果的符号位。
这意味着 CPU 里既要造加法器,又要造减法器,还要造一堆判断逻辑——电路复杂、成本高、速度慢。
此外,原码还有一个尴尬的特性:0 有两种表示(+0 和 −0),浪费了一个编码,还给比较运算带来麻烦。
结论:原码适合人看,不适合机器算。
三、反码:过渡性的修补
3.1 定义
反码的规则:
- 正数:反码 = 原码(不变)
- 负数:符号位不变,其余各位按位取反(0 变 1,1 变 0)
| 十进制 | 原码 | 反码 |
|---|---|---|
| +5 | 0000 0101 | 0000 0101 |
| −5 | 1000 0101 | 1111 1010 |
| +0 | 0000 0000 | 0000 0000 |
| −0 | 1000 0000 | 1111 1111 |
3.2 反码的进步与残留问题
反码的重大意义在于:减法可以被转化成加法了。
计算 5 − 5,即 5 + (−5),用反码试试:
0000 0101 (+5 的反码)
+ 1111 1010 (−5 的反码)
-----------
1111 1111 (−0 的反码)
结果是 1111 1111,即 −0。方向对了!只要把符号位当成普通位一起加,再把溢出的进位”绕回来”加到最低位(这叫循环进位 / end-around carry),反码加法就能得到正确结果。
但反码仍有两大顽疾:
- 0 依然有两种表示(
0000 0000和1111 1111); - 加法需要处理”循环进位”,硬件依然别扭,多了一步修正逻辑。
反码是一次伟大的中间探索——它证明了”取反”可以把减法变加法,但还差最后临门一脚。
四、补码:终极答案
4.1 定义
补码的规则:
- 正数:补码 = 原码(不变)
- 负数:补码 = 反码 + 1
| 十进制 | 原码 | 反码 | 补码 |
|---|---|---|---|
| +5 | 0000 0101 | 0000 0101 | 0000 0101 |
| −5 | 1000 0101 | 1111 1010 | 1111 1011 |
| +0 | 0000 0000 | 0000 0000 | 0000 0000 |
| −0 | 1000 0000 | 1111 1111 | 0000 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 ~ +127。1000 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 0101 → 1111 1010,而这正是 −6 的补码。于是有恒等式 ~x = -x - 1,取反加一求相反数也就不再神秘:-x = ~x + 1。
5.4 补码快速求相反数
对任何数(包括负数)“取反加一”,得到的就是它的相反数:
int neg = ~x + 1; // 等价于 -x
编译器和硬件都在悄悄用这个公式。
5.5 无符号数与有符号数共用加法器
正是因为补码的模运算本质,同一段二进制 1111 1011,解释成有符号是 −5,解释成无符号是 251——两者的加法电路完全相同。这就是 C 语言中 int 和 unsigned int 可以共用同一套运算硬件的原因,也是类型转换时”数值突变”的根源。
5.6 浮点数的对照
有趣的是,IEEE 754 浮点数没有采用补码,而是用”符号位 + 原码”的方式表示尾数。为什么?因为浮点运算的核心是乘除(对阶后尾数相乘),符号与数值分离反而更方便。这反过来印证了一个道理:编码方案是为运算需求服务的,没有银弹,只有权衡。
六、思考
-
为什么补码能多表示一个 −128? 因为原码/反码中 0 占了两个编码(+0、−0),补码把它们统一后,空出的
1000 0000就用来表示 −128。 -
8 位补码
1000 0000的相反数是多少? 取反加一:0111 1111+ 1 =1000 0000,还是它自己!−128 没有对应的 +128,这是补码不对称性的代价。abs(-128)在 8 位下溢出,程序员必须小心。 -
已知某数补码为
1111 0110,它的真值是多少? 取反加一:0000 1001+ 1 =0000 1010= 10,所以真值是 −10。 -
为什么计算机不用原码? 一句话:原码让加法和减法无法共用电路,0 还有两种表示。
七、总结
| 维度 | 原码 | 反码 | 补码 |
|---|---|---|---|
| 表示方法 | 符号位 + 绝对值 | 负数按位取反 | 负数取反加一 |
| 0 的表示 | 两种 | 两种 | 一种 |
| 8 位取值范围 | −127 ~ +127 | −127 ~ +127 | −128 ~ +127 |
| 减法转加法 | ❌ | ✅(需循环进位) | ✅(无需修正) |
| 硬件成本 | 高 | 中 | 低 |
| 现代计算机使用 | ❌ | ❌ | ✅ |
一句话总结全文:
原码是给人看的,补码是给机器用的。补码用”模”的数学思想,把减法化为加法、把正负 0 统一、把电路简化到极致——这就是它能统治整个计算机世界的原因。
下次当你在调试器里看到一个整数神秘地从 2147483647 跳成 −2147483648 时,请微笑着想起这篇文章——那不是 bug 的玄学,那是补码的呼吸。