Skip to content

19.3 补充:原码、反码、补码与移码

在 19.2 节我们学习了计算机如何用二进制表示正整数。但这引出了一个巨大的问题:计算机底层只有 0 和 1,它连负号(-)都不认识,那么它到底是如何表示负数的呢?

为了解决如何在有限的硬件中同时搞定“正负”和“加减法”的问题,计算机科学家们先后发明了原码、反码和补码,后来在浮点数领域又引入了移码。今天,几乎所有现代计算机的整数底层都是用补码来存储的。

  • 真值:我们在现实中真正想表示的数字,拥有正负号。例如:+3-5
  • 机器数:这个数字在计算机内存中实际的二进制长相。由于计算机不认识正负号,科学家规定:把内存最高位(最左边那一位)作为符号位,0 代表正数,1 代表负数。

为了方便演示,下面我们假设所有的数字都用 8位(1个字节) 的内存来存储。


2. 原码 (Sign-Magnitude):最符合人类直觉

Section titled “2. 原码 (Sign-Magnitude):最符合人类直觉”

原码的定义:最高位是符号位(0正1负),剩下 7 位直接用它的二进制绝对值表示。

示例:

  • +3 的原码:[0] 0000011
  • -3 的原码:[1] 0000011

优点:简单直白,人类一眼就能看懂。 致命缺点(为什么被淘汰?)

  1. 会有两个零[0] 0000000 叫做 +0[1] 0000000 叫做 -0。在数学上 0 就是 0,分正负 0 纯属浪费硬件,还导致比较大小很麻烦。
  2. 没法直接做加减法:如果让计算机直接把 +3-3 的原码按位相加,会得到 [1] 0000110(即 -6),而不是预期的 0。如果要通过原码做减法,CPU 里面还得专门设计一套极其复杂的减法器电路。

3. 反码 (Ones’ Complement):走向加减法统一的过渡

Section titled “3. 反码 (Ones’ Complement):走向加减法统一的过渡”

为了让计算机只需要加法器就能做减法(把 A - B 变成 A + (-B)),人们发明了反码。

反码的规则

  • 正数的反码:与原码完全一样,不改变。
  • 负数的反码:符号位保持为 1,剩下的所有数据位全盘按位取反(0变1,1变0)。

示例:

  • +3 的原码:00000011 \rightarrow 反码:00000011
  • -3 的原码:10000011 \rightarrow 反码:11111100 (看,后面 7 位原本是 0000011,全反过来了)

尝试做加法 (+3 加上 -3): 用它们的反码相加:00000011 + 11111100 = 11111111。 结果 11111111 是个反码(最高位是1说明是负数)。我们把它转换回原码看看是多少(符号位不变,剩下取反):变为 10000000,也就是 -0

反码的缺点: 虽然反码解决了部分加法问题,但它依然没有解决“世界上有两个零(+0-0)”的奇葩问题,在跨越 0 的算术进位上依然有瑕疵。


4. 补码 (Two’s Complement):现代计算机的唯一真神

Section titled “4. 补码 (Two’s Complement):现代计算机的唯一真神”

为了彻底干掉 -0 并且让一套加法器电路完美搞定所有正负数的加减法,补码诞生了。如今所有的 C++ int 型底层都是补码。

补码的规则

  • 正数的补码:与原码、反码完全一样。(正数永远是本体)
  • 负数的补码:在它反码的基础上,再加 1

口诀:负数的补码 = 符号位不变,其余位取反,最后整体 + 1。

示例:推导 -3 的补码:

  1. 写出原码:10000011
  2. 变反码:11111100
  3. 最末位 +1 变补码:11111101

补码的两大神级优势:

优势一:天下大一统的加法:+3-3 用补码相加: 00000011 (这就是+3)

  • 11111101 (这就是-3)

100000000 (最高位的那个1溢出了,8位容器装不下,直接丢弃掉) 留下的正好是:00000000!完美的绝对的 0!

优势二:多出了一个极小值: 在反码和原码中,8 位内存有两套 0,能表示的范围是 -127 到 +127。 而在补码中,原本的 -0 (10000000) 废除了,被硬性规定用来表示 -128。 这就解释了为什么 8 位带符号整数(int8_t)的范围是神奇的:-128 到 127。总是负数比正数多容纳一个!C++ 里的 int(通常是 32位)最小值也是 -2147483648,而最大值是 2147483647


5. 移码 (Offset Binary / Biased):浮点数的专属配件

Section titled “5. 移码 (Offset Binary / Biased):浮点数的专属配件”

移码通常不用于我们常见的整数 int,它主要用于浮点数(如 float, double)底层的“指数部分”

存在的痛点: 补码虽然加减算术无敌,但是如果你要把两个补码内存直接拿去对比大小,就会非常混乱。 比如:正数 +3 补码是 00000011,负数 -3 补码是 11111101。 如果让计算机按照无符号的本能直接从左到右去比大小,由于负数最高位是 1,它居然会觉得 -3+3 还要大!计算机如果要判断大小,还得专门先剥离符号位,很麻烦。

移码的规则: 把数字在数轴上整体平移,加上一个固定的偏置值(Bias),强行把所有负数都拉上来变成“正数”。

假设在 8 位中,我们加上偏置值 128(即加上二进制 10000000):

  • 原本是真值 -3,移码 = -3 + 128 = 125,二进制存为 01111101
  • 原本是真值 0,移码 = 0 + 128 = 128,二进制存为 10000000
  • 原本是真值 +3,移码 = 3 + 128 = 131,二进制存为 10000011

移码的优势: 仔细观察上述二进制,在这个系统里,只要真值越大,存进去的二进制看起来也绝对越大!这就意味着,计算机完全可以直接调用毫无技术含量的“无符号比较器”来对移码进行大小判断,速度极快。这就是浮点数(IEEE 754标准)底层指数位采用移码的根本原因。

当你看到 C++ 里的 int x = -10; 时:

  1. 它在你的代码里是个十进制字面量。
  2. 编译器编译它时,先算它的长相。
  3. 它在你的内存里,实际静静躺着的是它的补码形式。