补码,或者说2的补码(Two's complement),是计算机中用于表示负整数的常用方式。你一定知道,它的计算规则是取反码再加一;你很可能也知道,这样表示的好处是只需要一个加法器便可以处理正负数混合的运算。任取几个数尝试,很容易验证这样神奇的性质。除此之外,它还有一些很好的性质。例如在扩宽时,只需要用最高位填充空白部分即可。

但这有些先射箭后画靶的嫌疑。我们不禁想问,如此巧妙的机制是如何提出的呢?就像正态分布只要几个平凡的约束条件即可唯一得到那样,补码也是几个合理约束下唯一能够表示负数的方案。接下来我们不妨重新发明一次补码。理解这些不需要高中以上的数学知识,数学符号只是为了简洁表意。下文中出现的数均为整数。

我们有一个 \(k\) 位(二进制位,下同)的加法器,能够对两个 \(k\) 位无符号整数做加法,结果保留最低的 \(k\) 位,即对 \(2^k\) 取模的结果。在这个系统下,“等于”是在模 \(2^k\) 意义下的,若 \(x \equiv y \pmod {2^k}\),则可认为 \(x = y\)。也就是说,只要两个数关于 \(2^k\) 的模数相同,在这个系统下它们就是相等的。这不是我们所熟知的整数域 \(\mathbb Z\) 中的“等于”,但也很好理解。这是一个循环,可以用一天中的小时(0~23)、角度(0~359°)类比。同理也可以得到,对于减法或者负数,则有 \(-x = 2^k - x\)

那么我们如何在这个系统的基础上,建立带符号整数的运算规则呢?如果我们可以将带符号整数映射到原先的无符号整数上也许可以,比如说最简单的思路是将最高位当作符号。一个合法的映射只需要建立一一对应(即双射)的关系,这会产生非常多种方案;但我们希望它方便而自然,具有一些良好的性质:

  1. 自然数是我们最常用的,它们的表示与值应当相等,这样方便我们在无符号表示和带符号值之间转换,即 \(\forall x \ge 0, \, f(x) = x\)
  2. 为了让原有的加法器可以无需任何修改直接进行带符号运算,我们希望无符号表示的和等于带符号和的表示,即 \(f(x + y) = f(x) + f(y)\);减法同理。这被称之为加法同态性。

接下来我们会看到,这样两条性质已经唯一确定了映射关系。负数可以由减法的定义得到:

\[ \begin{aligned} & f(-x) \\ = & f(0-x) \\ = & f(0) - f(x) & \text{(同态性)} \\ = & 0 - x & \text{(自然数直接对应)} \\ = & 2^k - x & \text{(循环性)}\\ \end{aligned} \]

于是我们得到了负数的映射规则 \(f(-x) = 2^k - x\)。现在,非负数 \([0, a]\) 将被映射到 \([0, a]\),负数 \([-b, -1]\) 则被映射为 \([2^k - b, 2^k - 1]\)。为避免二者产生重叠,\(a < 2^k - b\)。这里的取值就相对不那么讲究了,我们不妨令 \(a = 2^{k-1} - 1, \, b = 2^{k-1}\),这样恰好使得最高位是符号位;也正是因为如此,有符号整数的表示范围中,最小值的绝对值比最大值要大了1。于是我们就重新发明了补码的运算规则。

那么我们熟知的“反码加一”呢?注意到 \(f(-x) = 2^k - x = \left[ \left( 2^k - 1 \right) - x \right] + 1\),而我们知道 \(2^k - 1\)的二进制表示即为 \((\underbrace{11 \dots 1}_k)_2\),于是 \(\left[ \left( 2^k - 1 \right) - x \right]\) 等价于 \(x\) 的反码。因此,\(\left[ \left( 2^k - 1 \right) - x \right] + 1\) 就是我们熟知的“反码加一”的形式。

从群论的视角看,这可能只不过是阿贝尔群、循环群、同态等概念的演绎,但我并不了解这方面的理论,因此不好妄言。我只是想用通俗的语言说明,补码并不是刻意的设计,而是若干合理要求下的唯一选择。


See also: