原码反码补码的转换方法(含:符号位处理与溢出判定)

把 8 位二进制想成一排开关,补码只是换了个数数的约定。八只开关一共 256 种组合,无符号时读作 0 到 255;一旦约定「左边第一只开关拨到 1 就代表负数」,同一串 0 和 1 马上变成 -128 到 127。原码、反码、补码就是三套读法:原码贴近人的书写习惯,反码是推导中的中间产物,补码是处理器真正采用的方案。

这个差别在做 8 位 ALU 仿真时最容易暴露。当时用门电路搭加减法器,学生按原码思路写负数,加法器结果总是差一,排查了半天才发现减法要靠「取反加一」把负数转成补码再进加法器。同一批实验里,还有人在 127 加 1 之后看到 -128,以为是仿真器出错。三套编码的符号位处理方式不同,转换步骤不同,溢出判定的依据也不同,把这三件事分开记会清楚很多。

准备:固定位宽与符号位的约定

转换前必须先固定位宽和符号位位置,否则同一串 0 和 1 可以有两种合法读法。约定最高位(左边第一位)为符号位,0 表示正、1 表示负;剩下的位是数值位。位宽决定可表示范围:n 位补码覆盖 -2^(n-1) 到 2^(n-1)-1,8 位是 -128 到 127,16 位是 -32768 到 32767。

三套编码在零的写法上分道扬镳,这也是补码被硬件接纳的原因。

表示法 8 位可表示范围 零的写法 主要问题
原码 -127 ~ +127 0000 0000 与 1000 0000 零有两种写法,减法需额外判断符号
反码 -127 ~ +127 0000 0000 与 1111 1111 零仍不统一,运算后可能要循环进位
补码 -128 ~ +127 只有 0000 0000 负数不能直接读出绝对值

准备工作按下面三步做完,后面就不会在中途反复改约定。

  1. 确定位宽,是 8 位还是 16 位。
  2. 约定最高位为符号位,其余为数值位。
  3. 写下该位宽对应的范围上下界。

位宽选错是常见返工点。同一个十进制数 -25,写成 8 位补码是 1110 0111,写成 16 位补码却是 1111 1111 1110 0111,前面多出一串 1。这不是算错,而是负数在高位补齐时补 1 而不是补 0,理解了这一点,跨位宽转换就不会出错。

执行:三种编码的转换步骤

正数的三套编码写法完全一致,负数按「原码 → 符号位不动、数值位取反 → 末位加一」就能得到补码。整套流程固定成五步,照做即可。

  1. 写出该数绝对值的二进制。
  2. 补齐到指定位宽,前导位补零。
  3. 把最高位改为 1,得到原码。
  4. 符号位不动,其余位逐位取反,得到反码。
  5. 反码末位加一,得到补码。

以 -25 为例:绝对值 25 的二进制是 11001,补齐 8 位得 0001 1001;最高位改 1,原码为 1001 1001;符号位保持 1、其余取反,反码为 1110 0110;末位加一,补码为 1110 0111。整个过程不需要理解为什么这样做,先按步骤走通,再回头看补码解决了什么问题。

正数:三码相同

正数只要写出二进制并补齐位宽就够了。+25 的原码、反码、补码都是 0001 1001,最高位天然是 0。这一点在写代码时同样成立:非负整数的补码表示就是它本身的二进制,不需要任何转换函数。

负数:取反之后还要加一

负数的关键在最后那次加一。只取反不加一得到的是反码,反码参与运算时结果可能差一,必须在某处做循环进位修正,硬件反而更复杂。补码把修正动作提前到编码阶段,运算过程中就只剩加法。下面这段代码把三套编码放在一起对照,方便直接验证手工计算的结果。

WIDTH = 8

def to_mag(value):          # 原码:符号位 + 绝对值
    if value < 0:
        return (1 << (WIDTH - 1)) | (-value)
    return value

def to_ones(value):         # 反码:负数按位取反(符号位保持不变)
    if value < 0:
        return ((1 << WIDTH) - 1) ^ (-value)
    return value

def to_twos(value):         # 补码:负数取模 2^WIDTH
    return value & ((1 << WIDTH) - 1)

def from_twos(bits):        # 补码还原十进制
    if bits >> (WIDTH - 1):
        return bits - (1 << WIDTH)
    return bits

for v in (5, -5, 25, -25):
    print(v, format(to_mag(v), "08b"), format(to_ones(v), "08b"), format(to_twos(v), "08b"))

print(from_twos(0b11100111))   # -25

跑一遍就能确认 -25 的三码分别是 10011001、11100110、11100111。注意 to_twos 用的是取模而不是位运算拼接,因为负数在补码里的语义就是「模 256 的余数」,这也解释了为什么 -128 能写出来而原码写不出来。

从补码还原十进制

补码还原有两条等价路径:一条是「取反、加一、再加负号」,另一条是直接算无符号值减去 2ⁿ。以 1110 0111 为例,取反得 0001 1000,加一得 0001 1001,也就是 25,加负号得 -25;用第二条路径,1110 0111 的无符号值是 231,231 减 256 同样是 -25。第二条路径在心算时更快,唯一需要留意的是最高位为 0 时不要减,直接读原值即可。

三种编码在几个典型数值上的对照如下,可以把这几行背下来当检查清单。

十进制 原码 反码 补码
+5 0000 0101 0000 0101 0000 0101
-5 1000 0101 1111 1010 1111 1011
+25 0001 1001 0001 1001 0001 1001
-25 1001 1001 1110 0110 1110 0111
0 0000 0000 0000 0000 0000 0000
-128 无法表示 无法表示 1000 0000

-128 这一行是三套编码差异的直接证据:原码和反码受「零吃掉两个编码」的影响,只能表示到 -127,补码把省下来的那个编码给了 -128。

验证:拿边界值回代检查

转换结果用 0、-1、-128、127 四个边界值回代,能查出绝大多数写错的位置。这四个值的补码形态固定且好记,写错时会立刻与预期不符。

  1. 检查 0 的补码是否为 0000 0000。
  2. 检查 -1 的补码是否为全 1。
  3. 检查 -128 是否为 1000 0000。
  4. 检查 +127 是否为 0111 1111。
  5. 把结果代回加法,验证 a + (-a) 是否回零。

第五步是最有价值的一步。取某个数及其相反数的补码相加,结果在 8 位下应当被截断成 0000 0000,例如 0000 0011 加 1111 1101 得 1 0000 0000,丢掉第 9 位的进位后正是零。如果这一步得不到零,说明符号位处理错了,而不是运算错了。

验证时还要留意「截断」和「溢出」是两个不同概念。截断发生在位宽固定的加法中,多出来的高位直接丢弃;溢出指的是丢弃后的结果已经不能代表真实数值,这两件事在补码加法里经常同时出现,但只有溢出的结果不可信。

溢出判定:结果符号不对就说明越界

两个同号数相加,如果结果的符号位与两个加数相反,就是溢出;两个异号数相加不会溢出。这条规则比记公式实用,因为在纸上算的时候只需要看符号位。

把 127 和 1 相加:0111 1111 加 0000 0001 得 1000 0000,符号位从 0 变成 1,正数加正数得到负数,结果 -128 显然是错的,真实值 128 超出了 8 位补码的上界。反方向同理,-128 加 -1 得 0111 1111,负数加负数变成正数,真实值 -129 越过了下界。

硬件把这条规则做成一个异或门:最高位向外的进位与次高位向外的进位不一致时,置起溢出标志。写代码时用符号位比较更直观。

def add_overflow(a, b):
    s = (a + b) & 0xFF
    sign_a, sign_b, sign_s = (a >> 7) & 1, (b >> 7) & 1, (s >> 7) & 1
    overflow = (sign_a == sign_b) and (sign_s != sign_a)
    return from_twos(s), overflow

print(add_overflow(127, 1))     # (-128, True)  上界越界
print(add_overflow(-128, -1))   # (127, True)   下界越界
print(add_overflow(100, -50))   # (50, False)   异号相加不会溢出

这段判定的前提是先把结果截断回原位宽,否则 Python 的整数不会自己回绕,符号位比较也就失去意义。在固定位宽的场合里,有符号溢出看符号位,无符号溢出看进位标志,两者判定依据不同,混用会得出相反结论。

常见问题(FAQ)

Q1:为什么 8 位补码的范围是 -128 到 127?

补码的零只有一种写法,省下的那个编码给了 -128,正负两侧因此不对称。

Q2:16 位补码怎么转换?

步骤相同,只把位宽换成 16,负数高位补齐时补 1 而不是补 0。

Q3:符号位参与运算会不会算错?

不会,补码的设计就是让符号位像普通位一样参与加法,前提是位宽固定且检查溢出。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
命令行手艺人的头像命令行手艺人普通用户

相关推荐

返回顶部