原码、反码、补码:一张图讲透计算机里的负数表示

一、动机:为什么需要补码?

先看一个事实:8 位无符号数能表示 0 ~ 255,共 256 个状态。

一旦要表示负数,同样 8 位还是只有 256 个状态,必须"挤"进去:

表达需求 需要的状态数
0 ~ 127 128
-1 ~ -128 128
合计 256

刚好塞满,一个不多一个不少。所以补码的表示范围是 -128 ~ 127 (-128 是"多出来"的那个特殊值,来源见第四节)。

那问题来了:负数存进去之后,计算机要怎么算数?

如果把符号位和数值位分开对待,加减法会爆炸式地分情况:

复制代码
(+5) + (+3)   → 同号,直接相加
(-5) + (-3)   → 同号,取绝对值相加,再加负号
(+5) + (-3)   → 异号,大减小,符号跟大的走

一套加法器要分三类实现,硬件代价极高。

补码的贡献就是:让"加法"变成唯一一种运算,减法变成"加一个相反数"。

结论前置:补码不是"某种编码偏好",它是为了让 CPU 只用一套加法电路 而设计的表示法。

现代 CPU 的 ALU 里根本没有减法器,全是加法器。


二、三种码的定义

设一个数:-3,用 8 位表示。

2.1 原码(Sign-Magnitude)

复制代码
原码 = 符号位 + 绝对值的二进制

-3  →  1 | 000 0011   →  1000 0011
 3  →  0 | 000 0011   →  0000 0011
  • 优点:人类直观,符号位一目了然。
  • 缺点:加减要分情况 ,且存在 +0(0000 0000) 和 -0(1000 0000) 两个"零"。
  • 范围:-127 ~ 127(浪费了 -128 这个位置)。

2.2 反码(Ones' Complement)

复制代码
反码 = 符号位不变,数值位按位取反

-3 原码  1000 0011
-3 反码  1111 1100   ← 数值位 0000011 取反 → 1111100

 3 原码  0000 0011
 3 反码  0000 0011   ← 正数反码 == 原码
  • 正数的反码永远等于原码。
  • 负零:1111 1111 表示 -0,和 0000 0000 的 +0 并存。
  • 范围:-127 ~ 127。

2.3 补码(Two's Complement)

复制代码
补码 = 反码 + 1(只在负数上加 1)

-3 原码  1000 0011
-3 反码  1111 1100
-3 补码  1111 1101   ← 末位 +1

 3 原码  0000 0011
 3 反码  0000 0011
 3 补码  0000 0011   ← 正数三者完全相同

核心口诀(记这一句就够):

正数:原码 = 反码 = 补码

负数:反码 = 原码符号位不变、其余取反;补码 = 反码 + 1


三、转换规则与对照表

以 8 位为例,符号位恒为最高位,0 正 1 负:

数值 原码 反码 补码 说明
+6 0000 0110 0000 0110 0000 0110 正数三码完全一致
-6 1000 0110 1111 1001 1111 1010 数值位取反 → 末位 +1
-3 1000 0011 1111 1100 1111 1101 最常考的 −3 补码
0 0000 0000 0000 0000 0000 0000 补码的零唯一
-128 (无) (无) 1000 0000 补码独有,比原码多一个数

一眼看出规律:正数三码完全一致;负数原码→反码是"除符号位外全翻",反码→补码是"末位 +1"。

规则三条,照记:

  • 正数:原码 = 反码 = 补码,一个字节用满 8 位。
  • 负数 · 反码:符号位不变,其余 7 位按位取反。
  • 负数 · 补码 :反码末位 +1,即 [x]补 = x + 2ⁿ。

负数转换链(以 -6 为例,一图看清三步):

text 复制代码
  原码   1 000 0110    符号位 1 不动,数值 0000110 按位取反
          │
          ▼
  反码   1 111 1001    末位 +1
          │
          ▼
  补码   1 111 1010    ← -6 在计算机里的真实形态

正数(如 +6)三码完全相同:0000 0110,不需要任何转换。


四、实战演算:-3 + 2,三种码各走一遍

-3 + 2 正确答案是 -1。下面把三种码的计算过程都列出来,差异一目了然。

4.1 用原码算(异号相加无能为力)

复制代码
  1000 0011     (-3 原码)
+ 0000 0010     ( 2 原码)
-----------
  1000 0101     (看起来像 -5)

按原码规则解读结果:1000 0101 → 符号位 1,数值 000 0101 = 5 → 得到 -5 。

原码加法只适用于同号;这里是异号,原码没有定义该怎么做,硬做就会得到错误的结果。

4.2 用反码算(对了,但有坑)

复制代码
  1111 1100     (-3 反码)
+ 0000 0010     ( 2 反码)
-----------
  1111 1110     (-1 反码)  ✅

这一次结果正确了。但换个数据就出岔子:

复制代码
  1111 1101     (-2 反码)
+ 0000 0010     ( 2 反码)
-----------
1 1111 1111
-----------
  1111 1111     ( 丢弃进位 → 这是 -0!)  ❌

-2 + 2 明明等于 0,反码体系却给出 -0。

虽然可以靠额外规则把 1111 1111 归一化成 0000 0000,但每个结果都要检查是不是负零,硬件上又是一堆判断。

4.3 用补码算(一路到底,零特判)

复制代码
  1111 1101     (-3 补码)
+ 0000 0010     ( 2 补码)
-----------
1 1111 1111
-----------
  1111 1111     (丢掉最高位进位)

得到 1111 1111,它是不是 -1?把补码还原成原码验证:

复制代码
补码   1111 1111
 ↓ 符号位不变,数值位取反 → 反码
反码   1000 0000
 ↓ 末位 +1 → 原码
原码   1000 0001     → 符号位 1,数值 0000001 = 1  →  -1  ✅

对照一下 -3 自己(-3 补码 = 1111 1101,1111 1101 → 1000 0010 → 1000 0011 ✅),规律完全一致。

4.4 三种码结果对比

编码方式 -3 + 2 运算结果 解读 对不对
原码 1000 0101 -5 ❌ 异号相加无定义
反码 1111 1110 -1 ✅(但 -2 + 2 会得到 -0)
补码 1111 1111 -1 ✅ 无特判

这才是补码的价值:不管加什么,相加 → 丢弃最高位进位 → 结果就是对的。

三种码列竖式对比(-3 + 2):

text 复制代码
【原码】                       【反码】                       【补码】
 1 0 0 0  0 0 1 1  (-3)        1 1 1 1  1 1 0 0  (-3)        1 1 1 1  1 1 0 1  (-3)
+0 0 0 0  0 0 1 0  (+2)      +0 0 0 0  0 0 1 0  (+2)      +0 0 0 0  0 0 1 0  (+2)
 ────────────────              ────────────────              ────────────────
 1 0 0 0  0 1 0 1  → -5 ❌       1 1 1 1  1 1 1 0  → -1 ✅       1 1 1 1  1 1 1 1 1  (进位1丢弃)
                                                                   └──────────┘
                                                                   1 1 1 1  1 1 1 1  = -1 ✅

原码竖式把符号位当普通位相加,异号时直接算错;反码对但 -2+2 会出 -0;

补码多出的进位直接丢弃,结果永远正确。


五、为什么补码"相加就一定对"?

补码的定义可以写成数学形式(这才是它的本质):

复制代码
[x]补 = x + 2ⁿ       (n = 位宽,x 为真值)

既然 [x]补 只是 x 平移了 2ⁿ,那加减法在补码空间里就退化成模 2ⁿ 运算:

复制代码
[x + y]补 = [x]补 + [y]补   (mod 2ⁿ)

而"丢弃最高位进位"正好就是 mod 256。

所以它天然成立 ------ 补码的正确性不是巧合,是定义使然。

-128 是怎么来的?

复制代码
-128 真值 = -128
-128 补码 = -128 + 256 = 128 = 1000 0000

1000 0000 在无符号视角是 128,在补码视角是 -128。

同一个 bit 序列解释出两个数,这就是补码"挤满 256 个状态"的体现,也是它比原码多表示一个 -128 的原因。

进位丢弃 = 模运算的直观证明

复制代码
  1111 1101   (-3)
+ 0000 0010   (+2)
= 1 1111 1111

这个"多出来的 1"就是 2⁸,减去它(等价于丢弃)就完成了 mod 256。

丢弃进位不是"无奈之举",而是补码算法的一部分。


六、零与范围的对比(补码的"额外福利")

对比项 原码 反码 补码
0 的表示 0000 0000 和 1000 0000 两个 0000 0000 和 1111 1111 两个 只有 0000 0000 一个 ✅
8 位范围 -127 ~ 127 -127 ~ 127 -128 ~ 127 ✅
加减法是否需分情况 必须分(同号/异号) 分,且要处理负零 完全不分 ✅
加法器电路 多套 多套 + 负零修正 一套 ✅
是否需要额外校验位 需要 需要 不需要 ✅
溢出判断 麻烦 麻烦 简单(见下节)

补码零的唯一性带来的连锁好处 :判断 x == 0 不需要同时比较两个 bit 模式;

变长整数(C/C++ 的 char* 手写解析、大小端转换)用补码时也不必为负零写特例。

补码溢出判断:只看两个符号位

两个同号数相加,结果符号位与它们不同 → 溢出。

text 复制代码
  0111 1111  (+127)      两个 0,符号位不变 → +127,无溢出
+ 0000 0001  (+1)

1 1000 0000              → 模 256 后 1000 0000 = -128  ❌ 溢出

这就是为什么很多语言都提供「溢出即报错」的安全加法函数的原因。


七、现代写法:写代码时补码怎么出现

以下均用伪代码 表达,与具体语言无关。主流语言的整数类型(定长有符号)一律采用补码,不存在原码/反码参与运算。

7.1 把整数展开成补码位串

复制代码
// 把整数按 n 位补码展开成二进制串
toBinary(x, n):
    if x >= 0:
        return x 的 n 位二进制            // 正数补码即原码
    else:
        return (2^n + x) 的 n 位二进制    // 负数补码 = x + 2^n

toBinary(-3, 8)  → "11111101"
toBinary( 2, 8)  → "00000010"
(-3) + 2         → -1

7.2 位运算符是补码语义的直接体现

复制代码
x = -8                 // 补码 ...11111000
算术右移  x >> 1  → ...11111100 = -4          // 左侧补符号位 1
逻辑右移  x >>> 1 → 0111...1100 = 2147483644  // 左侧补 0

7.3 一个"教科书级"的分支无关绝对值

利用补码「负数右移得到全 1、正数右移得到全 0」的特性,可以不借助判断/分支求出绝对值:

复制代码
// 负数时 (x XOR -1) - (-1) = ¬x + 1 = -x;正数时 = x
abs(x) = (x XOR (x >> 位宽-1)) - (x >> 位宽-1)

同样利用补码,可以判断两数是否同号(符号位相同 → 异或后最高位为 0):

复制代码
sameSign(a, b) = (a XOR b) >= 0     // 同号返回真;异号或含 0 需另判

7.4 补码回原码(按位操作)

复制代码
// 以 8 位为例:负数补码 → 原码 = 取反 +1,再置回符号位
compToOrig(x):
    if x 的最高位 == 1:                    // 负数
        return ( (NOT x + 1) AND 01111111 ) OR 10000000
    return x

7.5 Kernighan 位技巧集(全部基于补码)

复制代码
isPow2(x)       = (x > 0) AND (x AND (x - 1) == 0)   // 是否 2 的幂
clearLowest1(x) = x AND (x - 1)                       // 清除最低位的 1

popCount(x):                                         // 统计 1 的个数
    c = 0
    while x != 0:
        x = x AND (x - 1)        // 每次消掉一个 1
        c = c + 1
    return c

same(a, b)    = (a XOR b) == 0                          // 两数是否完全相同
merge(lo, hi) = (lo AND 0xFFFF) OR ((hi AND 0xFFFF) << 16)   // 合并高低 16 位

7.6 溢出安全的加减

复制代码
// 加法前先检查是否会溢出,溢出则报错
addExact(a, b):
    result = a + b
    if 同号相加却得异号:        // 溢出特征
        error "overflow"
    return result

7.7 不同语言对溢出的处理差异

整数模型 表示方式 溢出行为 坑
定长补码(C / Java / Go / Rust 等) 补码,定长 静默回绕(Rust 默认报错) 溢出不报错,结果错误
C / C++ 补码(标准强制) 有符号溢出属未定义行为 编译器可能优化成任意结果
无限精度(如 Python) 符号-数值,内部处理 永不溢出 负数不是补码,不能用 bin(负数) 验证补码

⚠️ 面试/实战高频陷阱:无限精度语言的负数不是补码存储 。

它直接输出带符号的二进制(如 -0b11),位运算时对负数"按位截取"才得到补码位串,

不要用这类语言去"验证补码"。

7.8 补码在工程里最常见的场景

场景 补码如何参与
网络字节序 大小端转换函数处理的就是补码 + 字节序的组合
TCP 校验和 16 位按大端取补码求和,溢出即回绕
CRC / 哈希(CRC32) 本质是 GF(2) 上的补码多项式运算
HMAC / 签名算法 字节序、补码进位规则直接影响实现兼容
字节码指令 局部变量索引和常量池索引都是补码整数
Base64 解码后的字节 直接按无符号读,切勿误当补码字符
数据库 DECIMAL 转整数 依赖十进制补码定义(不同厂商曾因此差 1)

八、常见误区速查

误区 正解
"反码是符号位也取反" ❌ 符号位始终不变,只翻数值位
"正数的补码要加 1" ❌ 只有负数反码才 +1;正数三码全等
"补码要额外加一个符号位" ❌ 符号位就在最高位里,8 位就是 8 位,不多不少
"负数相加要手动处理进位" ❌ 最高位进位直接丢,这正是补码正确性的来源
"-3 + 2 原码得到的结果是答案" ❌ 原码异号相加无定义,会得到错误值;补码下答案恒为 -1
"补码里 0 有两个" ❌ 补码只有 0000 0000 一个零
"Python 的 bin(-3) 能验证补码" ❌ Python 是符号-数值模型,不是补码
"位运算会把负数当无符号" ❌ 位运算在补码语义下操作,Python 除外

九、总结

  1. 正数:原码 = 反码 = 补码 ;负数:反码 = 原码数值位取反,补码 = 反码 + 1。
  2. 补码 = x + 2ⁿ 的模表示 ,所以 [x+y]补 = [x]补 + [y]补 (mod 2ⁿ) ------ 加法天然正确。
  3. 丢掉最高位进位 不是 bug,而是 mod 2ⁿ 的实现,是补码算法的一部分。
  4. 补码带来三个工程红利:唯一零 、多表示一个数(-128) 、只需一套加法电路。
  5. -3 + 2:原码异号相加无定义、反码对但负零有坑、补码恒为 -1 且零特判。
  6. 写代码时:定长补码溢出通常是静默的,应优先使用「溢出即报错」的安全加法;分支无关绝对值 ((x XOR (x>>位宽-1)) - (x>>位宽-1)) 玩的就是补码;无限精度语言不是补码,别拿来验证。

一句话记忆

原码是"人话",反码是"半成品",补码是"CPU 的语言"。

正数三者相同,负数取反再加一,加法直接丢进位。


本文的换算过程建议动手敲一遍,尤其是 -2 + 2(反码出 -0)和 127 + 1(补码溢出)这两个反例,比正面例子记得牢得多。

相关推荐
X北辰北1 小时前
C++ 进阶第二阶段总结|STL容器全家桶、迭代器失效、STL算法、序列化入门(工程必备)
开发语言·c++·算法
溪语流沙1 小时前
面试加分项!Manacher:O(n)求最长回文子串的对称之美
java·python·leetcode·面试·职场和发展
过河卒_zh15667661 小时前
再更新!人工智能安全治理指引性文件第三版发布!
人工智能·算法·aigc·生成式人工智能·算法备案
Web3&Basketball2 小时前
昇腾950DT迁移实测:3笔成本账与决策树
服务器·算法·决策树
康实训2 小时前
老年人能力评估实训室核心实训环节解析
人工智能·算法·实训室·实训室建设
强哥聊算力7 小时前
《算力江湖·英伟达传奇》第七集:ChatGPT出现前夜,英伟达发生了什么?
服务器·人工智能·算法·ai·chatgpt·gpu算力
西柚小萌新10 小时前
【岗位面试】--成都博维数孪 - 工业 AI 解决方案顾问
人工智能·面试·职场和发展
小 黄 鸡11 小时前
Flutter 面试必会知识点详解(从基础到进阶)
flutter·面试·职场和发展