MxHanks' Blog

奔赴山海,保持热爱

0%

原码、反码和补码

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

为什么要有"机器数"

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

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

一、原码(Sign–Magnitude)

定义最简单直观:

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

写成式子就是:

[x]={0x,x01x,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]=(2n1)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]=2nx[x]_\text{补} = 2^n - |x|

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

[x]=(2n+x)mod2n,2n1x<2n1[x]_\text{补} = (2^n + x) \bmod 2^n, \qquad -2^{n-1} \le x < 2^{n-1}

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

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

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

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

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

于是 aba - bb>0b>0)可以这样变形:

ab=(a+(2nb))mod2na - b = \bigl(a + (2^n - b)\bigr) \bmod 2^n

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

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

补码的优点

  1. 0 唯一:+0 与 −0 的补码都是 0000 0000
  2. 符号位直接参与运算,无需判断正负、无需比较大小;
  3. 范围多一个负数:能表示 2n1-2^{n-1},而原码/反码最小只到 (2n11)-(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. 按权展开(推荐,更快):最高位的权当作 2n1-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)=00000101+11111101=1000000105 + (-3) = 0000\,0101 + 1111\,1101 = 1\,0000\,0010

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

例 2:−5 + 6

11111011+00000110=100000001  00000001=1 1111\,1011 + 0000\,0110 = 1\,0000\,0001 \ \longrightarrow\ 0000\,0001 = 1 \ \checkmark

七、溢出判断(Overflow)

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

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

V=cincoutV = c_\text{in} \oplus c_\text{out}

溢出例子(8 位补码)

  • 120+10120 + 1001111000+00001010=100000100111\,1000 + 0000\,1010 = 1000\,0010(=−126),两个正数加出负数 → 溢出;
  • 120+(10)-120 + (-10)10001000+11110110=101111110011111101000\,1000 + 1111\,0110 = 1\,0111\,1110 \to 0111\,1110(=+126),两个负数加出正数 → 溢出。

反例(未溢出)120+30=10001000+00011110=10100110-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 ✓

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

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

AB=A+B+1A - B = A + \overline{B} + 1

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

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

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

小结

一句话记住三者:

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