MxHanks' Blog

奔赴山海,保持热爱

0%

原码、反码和补码

原码、反码和补码是很重要的入门内容,以下是一些学习的笔记。

为什么要有"机器数"

我们平时写的是带正负号的十进制数,但硬件里只有 0/1,所以符号也得用 0/1 来编码。把带符号的真值(如 +5、−5)变成能在机器里存储的一串 0/1,就是机器数。历史上出现过三种主要编码:原码、反码、补码。

下面统一讨论 n 位二进制:最高位是符号位(0 代表正、1 代表负),低 n−1n-1 位是数值位。

一、原码(Sign–Magnitude)

定义最简单直观:

  • 正数:符号位 0 + 真值的绝对值二进制;
  • 负数:符号位 1 + 真值的绝对值二进制。

写成式子就是:

[x]原={0∣x∣,x≥01∣x∣,x<0[x]_\text{原} = \begin{cases} 0|x|, & x \ge 0 \\ 1|x|, & x < 0 \end{cases}

例子(8 位):+5 → 0000 0101;−5 → 1000 0101

原码的优点是直观,看符号位就知道正负、读真值容易。缺点是:

  1. 0 有两种表示:+0 = 0000 0000,−0 = 1000 0000,判断是否为零要额外处理;
  2. 做加减法得先比较符号,再决定是加还是减、谁减谁,符号位不能直接参与运算,电路很麻烦。

于是有了反码,再到补码。

二、反码(Ones’ Complement)

定义:

  • 正数:反码 = 原码(符号位 0,数值位照抄);
  • 负数:符号位保持 1,数值位逐位取反(0↔1)。

所以反码又叫"对 1 取补",负数反码满足:

[x]反=(2n−1)−∣x∣[x]_\text{反} = (2^n - 1) - |x|

例子(8 位):−5 的原码是 1000 0101,数值位取反 → 1111 1010。

反码的运算有两个麻烦:减法虽能转成加法,但最高位的端进位(end-around carry)要绕回最低位再加一次;而且 0 仍然有两种表示(+0 = 0000 0000,−0 = 1111 1111)。

三、补码(Two’s Complement)——重点

定义:

  • 正数:补码 = 原码 = 反码;
  • 负数:在反码基础上 +1;等价地 [x]补=2n−∣x∣[x]_\text{补} = 2^n - |x|。

统一写成对模 2n2^n 取余:

[x]补=(2n+x) mod 2n,−2n−1≤x<2n−1[x]_\text{补} = (2^n + x) \bmod 2^n, \qquad -2^{n-1} \le x < 2^{n-1}

例子(8 位)求 −5 的补码:

  1. ∣−5∣=5=0000 0101|{-5}| = 5 = 0000\,0101
  2. 取反 → 1111 1010(这就是反码)
  3. +1 → 1111 1011

为什么补码能把减法变成加法?

n 位机器数其实是在模 2n2^n 下计数的:加了 2n2^n 溢出丢掉的进位,等价于没加。对负数有:

[x]补=2n−∣x∣[x]_\text{补} = 2^n - |x|

于是 a−ba - b(b>0b>0)可以这样变形:

a−b=(a+(2n−b)) mod 2na - b = \bigl(a + (2^n - b)\bigr) \bmod 2^n

右边第二项 (2n−b)(2^n - b) 正是 [−b]补[-b]_\text{补}。也就是说:减一个数 = 加上它的补码,硬件里只要有一个加法器就够了——这是补码最大的价值。

顺带验证一下"负数取反加一就是补码":把 n 位 bb 逐位取反得到 2n−1−b2^n-1-b,再 +1 得 2n−b2^n-b,成立 ✓

补码的优点

  1. 0 唯一:+0 与 −0 的补码都是 0000 0000;
  2. 符号位直接参与运算,无需判断正负、无需比较大小;
  3. 范围多一个负数:能表示 −2n−1-2^{n-1},而原码/反码最小只到 −(2n−1−1)-(2^{n-1}-1)。

数值范围对比(以 8 位为例)

编码 表示范围 0 的表示
原码 −127 ~ +127 +0、−0 两种
反码 −127 ~ +127 +0、−0 两种
补码 −128 ~ +127 唯一(0000 0000)

8 位补码的最小值 1000 0000 = −128 很特殊:符号位为 1 且数值位全 0,它占用的是原码里"−0"那个闲置位置,表示 −27-2^7。对它做"取反加一"得到的仍是它自己,所以没有对应的正数 +128。

四、三种编码速查表(8 位)

真值 原码 反码 补码
+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 (与 +0 相同)
−127 1111 1111 1000 0000 1000 0001
−128 不可表示 不可表示 1000 0000

快速互转口诀(只对负数生效,正数三种码一样):

  • 原码 ↔ 反码:数值位取反 / 再取反;
  • 反码 ↔ 补码:+1 / −1;
  • 求任意数的相反数:补码按位取反再加 1。

五、由补码读真值

两种方法任选:

  1. 取反加一:符号位为 1 → 数值位取反 +1 得到绝对值,再添上负号。
    • 例:1111 1011 → 取反 0000 0100 → +1 = 0000 0101 = 5 → 真值 −5 ✓
  2. 按权展开(推荐,更快):最高位的权当作 −2n−1-2^{n-1},其余位照常取正权。
    • 例:4 位 1101 = −23+22+0+20=−8+4+1=−3-2^3 + 2^2 + 0 + 2^0 = -8 + 4 + 1 = -3
    • 例:8 位 1000 0000 = −128-128,一眼看出。

六、补码加减法举例(8 位)

例 1:5 − 3

5+(−3)=0000 0101+1111 1101=1 0000 00105 + (-3) = 0000\,0101 + 1111\,1101 = 1\,0000\,0010

丢掉最高位的进位 → 0000 0010 = 2 ✓(在模 282^8 下这个进位本来就该丢,不算错)

例 2:−5 + 6

1111 1011+0000 0110=1 0000 0001 ⟶ 0000 0001=1 ✓1111\,1011 + 0000\,0110 = 1\,0000\,0001 \ \longrightarrow\ 0000\,0001 = 1 \ \checkmark

七、溢出判断(Overflow)

直观理解:两个同号数相加,结果的符号却与加数相反,就溢出了。因为减法本质是加补码,所以被减数与减数异号时也要留意。

电路上最可靠的判据是:进位到符号位的进位 cinc_\text{in} 与符号位产生的进位 coutc_\text{out} 不同,即溢出(等价于双符号位法):

V=cin⊕coutV = c_\text{in} \oplus c_\text{out}

溢出例子(8 位补码):

  • 120+10120 + 10:0111 1000+0000 1010=1000 00100111\,1000 + 0000\,1010 = 1000\,0010(=−126),两个正数加出负数 → 溢出;
  • −120+(−10)-120 + (-10):1000 1000+1111 0110=1 0111 1110→0111 11101000\,1000 + 1111\,0110 = 1\,0111\,1110 \to 0111\,1110(=+126),两个负数加出正数 → 溢出。

反例(未溢出):−120+30=1000 1000+0001 1110=1010 0110-120 + 30 = 1000\,1000 + 0001\,1110 = 1010\,0110(=−90)✓

注意区分:最高位那个被丢掉的进位 1 不是错误(模 2n2^n 计数本来就该丢);只有结果符号不对(超出可表示范围)才是溢出。

八、扩展与截断

  • 符号扩展(sign extension):把补码从窄位宽变宽时,高位一律补符号位。
    • 例:8 位 −5 = 1111 1011 → 16 位 1111 1111 1111 1011,数值不变。
  • 零扩展:仅用于无符号数(或直接把高位置 0),不要把无符号数和补码混用。
  • 截断:直接砍掉高位。若被砍掉的部分等于"用符号位补齐"的那一串(即原数能装进低位),数值不变;否则说明超范围,会出错。
    • 例:16 位 −5 = 1111...1111 1011 截成 8 位 1111 1011,仍是 −5 ✓

九、数字电路里怎么做减法?

有了补码,加减法可以共用同一个加法器。要算 A−BA - B,只需送 AA 和 B‾\overline{B},并让加法器最低位的进位输入为 1:

A−B=A+B‾+1A - B = A + \overline{B} + 1

硬件上用一个控制信号 Sub(做减法时为 1):

  • 每一位的 BB 先经过一个异或门取反:B⊕SubB \oplus Sub;
  • Sub 同时接到加法器最低位的进位输入 Cin;
  • Sub = 0 时做加法,Sub = 1 时做减法。

加法器会额外输出标志位:Carry(最高位进位)、Overflow(cin⊕coutc_\text{in} \oplus c_\text{out})、Zero、Negative。现代 CPU / ALU 的加减法基本都是这个思路——这也是补码在硬件里真正"好用"的原因。

小结

一句话记住三者:

  • 原码:看真值最直观,但有 ±0、减法麻烦;
  • 反码:负数取反,能"减转加",却有循环进位和 ±0;
  • 补码 = 反码 + 1:0 唯一、符号位直接参与运算、范围多一个 −2n−1-2^{n-1}、加减法统一成加法——机器真正使用的就是补码。