gnuemacs

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

补码的本质

在很多课堂上,老师都讲过补码的概念。可是不管怎么讲,最后留在头脑中的只有一些例如“补码的符号位可以直接参与运算”之类的方法,还有一条公式:

\[正数补码=本身,负数补码=反码+1 \]

这些方法和公式似乎成了考试解题的金科玉律。然而,今天早上突然想知道,补码到底是什么,为什么有上面那些公式和方法。通过一段时间的研究,下面写一写个人目前对于补码本质的理解。可能会有些错误,如果各位大神们看到了欢迎斧正。

前置知识

这一部分主要讲同余,主要是为一些不了解或者忘了同余概念的读者而写,如果会的话可以直接跳过。

在数论中有一个重要概念是同余。

\[a\equiv b \pmod c \]

这个式子表示\(a\)\(c\)的余数和\(b\)\(c\)的余数相等。我们可以读作\(a\)\(b\)\(c\)同余,也可以说\(a\)\(b\)在模\(c\)意义下相等。比如,我们有

\[16\equiv9\pmod 7 \]

\[15\equiv5\pmod 5 \]

\[2\equiv7\pmod 5 \]

\[-2\equiv3\pmod 5 \]

\[-2\equiv-7\pmod 5 \]

同余具有一个重要的特点——线性性质。也就是说,

\[a\equiv b\pmod c \\ c\equiv d\pmod c \]

可以推出

\[a+c\equiv b+d\pmod c \]

这个性质使得同余有一个强大的能力——在模意义下,化负数为正数,化减法为加法。比如,在模\(3\)意义下计算\(5-7\)。一种方法是直接计算:

\[5-7=-2\equiv1\pmod 3 \]

还有一种方法是化负数为正数,化减法为加法。因为我们知道

\[-7\equiv2\pmod 3 \\ 5\equiv5\pmod 3 \]

由线性性质,两者相加可得

\[5-7\equiv2+5\pmod 3 \]

于是,我们将模\(3\)意义下的计算\(5-7\)变为计算\(2+5\)。负数变为了正数,减法变为了加法。

为什么要使用补码

学过数字电路基础知识的读者都知道,计算机中的运算主要由ALU(算术逻辑单元)进行。这个单元中具有很多的加法器、乘法器等模块。可是,有一个很大的问题——如何计算减法呢?还有如何表示负数呢?

前人考虑了一种办法。把每个数字的最后额外增加一个符号位。这个符号位为1,则代表这个数字是负数。为0则为正数。注意,这个符号位是不能参与运算的,它只是一个代表这个数字正负的标志。一个数字在计算机内的表示,就是这个符号位和这个数字本身的二进制码的结合。然而,这么做具有一个巨大的问题。需要对四种不同的运算,涉及四种不同的电路:

  • \(正数+正数\)
  • \(正数+负数\)
  • \(负数+正数\)
  • \(负数+负数\)

此外,还需要一个复用器来根据符号位判断选用哪一个电路进行运算。这么做显然对于性能、功耗都很不利。后来,人们希望寻找一种方法,把所有的计算(无论加减),都纯粹地使用加法器电路来完成。

引入

下面考虑一个问题。你面前只有一个3位加法器。如何用它计算\(-1-2\)的值?

我们知道,一个\(k\)位的加法器,具有两个\(k\)位的输入和\(k\)位的输出。最高位的进位会被舍弃(我们目前不考虑进位标志之类的)。也就是说,对于\(3\)位加法器,比如\(111_{(2)}+111_{(2)}=1110_{(2)}\),加法器的输出只会保留其后三位\(110\)。这个加法器的结果\(110_{(2)}\),和实际的值\(1110_{(2)}\)实际上相差了一个进位,或者说相差了\(8\)

\[0110_{(2)}=0\times2^3+1\times2^2+1\times2^1+0\times2^0 \\ 1110_{(2)}=1\times2^3+1\times2^2+1\times2^1+0\times2^0 \]

显然它们相差最高位的\(2^3\),也就是\(8\)。这个加法器要么能算出准确结果,要么算出的结果就比实际结果少\(8\)

实际上,从同余的角度讲,这个加法器是在模8意义下进行计算的。因为,少8不会改变算出结果对\(8\)的余数和实际结果对\(8\)余数的相等性。比如给这个加法器输入两个\(7\),加法器将输出\(110_{(2)}=6\)

\[7+7\equiv6\pmod 8 \]

在模8意义下,加法器就是“准确”的。因为去掉了进位的\(6\),和实际的结果\(14\),是对8同余的。

那我们如何用加法器计算\(-1-2\)呢?显然加法器只能输入正数,所以没法直接输入\(-1\)\(-2\)。因为这个\(3\)位加法器是相当于在模8意义下进行计算的,我们联想到前面讲的同余具有化负数为正数的功力,于是我们将\(-1\)\(-2\)人工转化为同余的正数

\[-1\equiv7\pmod 8 \\ -2\equiv6\pmod 8 \]

然后,我们只需要将\(7\)\(6\)输入到加法器里面就行了!牢记,加法器会在模8意义下计算,于是加法器输出结果为

\[7+6\equiv5\pmod 8 \]

所以,加法器将会告诉我们,\(-1-2\)的值,和\(5\),在模8意义下是相等的。然而我们人为知道,\(5\)\(-3\)在模8意义下也相等。于是可以得到

\[-1-2=-3 \]

出现问题

刚才的最后一步会令人很迷惑。加法器只告诉我\(-1-2\)的值,和\(5\),在模8意义下是相等的。那这个运算的结果到底是\(5\)还是\(-3\)呢?虽然\(5\)\(-3\)在模\(8\)意义下相等,可他俩本身不相等呀。

于是,我们需要人为定义一下,比如加法器计算结果是\(5\),这个5我们需要人为规定它表示的是\(5\)还是\(-3\)。实际上人们是这么一半表示正数,一半表示负数规定的(后面会解释这么规定的好处)。

加法器的计算结果 模8意义下可以表示的数 人为规定表示的数
0 0或-8 0
1 1或-7 1
2 2或-6 2
3 3或-5 3
4 4或-4 -4
5 5或-3 -3
6 6或-2 -2
7 7或-1 -1

这么一规定,于是我们就可以得到\(-1-2\)的结果为\(-3\)了!为了更加统一,人们干脆直接将每个数字(也就是上面的“规定表示的数”一列),和一个模意义下的正数(比如“加法器的计算结果”一列)唯一对应起来。并且,那个正数被称为“补码”。需要注意,这里的“补码”,默认是关于8的补码。关于不同大小的补码是不一样的。得到了下面的这个表格。

使用补码计算

数字 补码 补码的二进制
0 0 000
1 1 001
2 2 010
3 3 011
-4 4 100
-3 5 101
-2 6 110
-1 7 111

回到这个问题,你面前只有一个3位加法器。如何用它计算\(-1-2\)的值?首先,我们人为查这张表,找到数字-1、-2对应的补码。然后把这两个补码送到加法器里面去计算。加法器会输出\(5\),然后找补码\(5\)对应的数字,就是结果的数字。在这里就是\(-3\)

这么做的一个好处在于,这种方式中,数字的运算本身已经携带了符号信息。一切都可以通过加法器完成。此外,为什么人们规定补码0、1、2、3对应的是正数0、1、2、3,补码4、5、6、7对应的是负数-4、-3、-2、-1呢?因为这样一半一半地划分,可以直接通过补码的二进制的最高位(从右往左第3位)看出数字的符号。看上面的表格,补码二进制的最高位是1的地方,对应的都是负数。二进制最高位是0的地方对应的都是正数。不过这个符号位和前面人为故意设置的符号位是不一样的。这个符号位本身就是补码的一部分(还记得吗,补码实际上就是前面那个“加法器的输出”),可以直接参与运算。

接下来我们尝试计算\(-3-4\)。我们先根据上面的表,\(-3\)的补码是5,\(-4\)的补码是4,加法器将会输出\(1\)。回去查表,发现补码\(1\)对应的是正数1?难道\(-3-4=1\)

实际上,根据\(1\)和实际的结果\(-7\),在模8意义下是相等的。但是这里,我们已经人为规定了,\(1\)只能表示1,不能表示-7。此外,我们在前面的表格中,也并没有给数字\(-7\)留位置(如果要留的话,数字\(-7\)将会和数字\(1\)的位置冲突)。这种情况本质是因为两个负数相加得到的负数绝对值太大,以至于超过了表示范围,而变成了一个“正数”。这种情况被称为有符号数溢出。在两个比较大的正数相加,也会有这种情况。比如\(2+3\)也没法被正常计算。读者可以自己试一试。

有符号数溢出还有一个二进制情况下的解释。考虑前面的\(-3-4\),被转化为补码相加\(101_{(2)}+100_{(2)}=1001_{(2)}\)。而加法器只有三位,将得到\(001_{(2)}\)。这种情况下,我们发现得到的结果的“符号位”实际上是1,但是1在从右往左第四位,加法器只有3位,而无法保留这个1。而符号位变成第三位0。

一般情况

我们把之前的要表示的数字称为原码。对于更加一般的情况,一个\(n\)位的定长数,也是类似的。

原码 补码
0 0
1 1
... ...
\(2^{n-1}-1\) \(2^{n-1}-1\)
\(-(2^{n-1})\) \(2^{n-1}\)
\(-(2^{n-1}-1)\) \(2^{n-1}+1\)
... ...
\(-1\) \(2^n-1\)

补码的最高位为1对应了负数\(-2^{n-1}\le x \le-1\),补码的最高位为0对应了正数\(0\le x \le 2^{n-1}-1\)

根据刚才的分析,原码和补码之间天然的关系呼之欲出。对于一个原码\(x\),设\(\bar x\)表示\(x\)的将每一位取反后的数字(一般称为反码),我们显然有

\[x+\bar x=(11111111....11111)_2 \]

上面的括号中,有\(n\)\(1\)。之前已经讲过,所有运算本质上都是在模\(2^n\)下的运算!因此,将上面的算式变化为

\[x+\bar x\equiv (11111111....11111)_2 \pmod {2^n} \]

\[(11111111....11111)_2=2^n-1 \]

所以,

\[x+\bar x\equiv 2^{n}-1\equiv-1 \pmod {2^n} \]

于是

\[-x\equiv\bar x+1\pmod {2^{n}} \]

这个恰恰就是教科书上经常出现,但又出现的莫名其妙的公式:

\[负数补码=反码+1 \]

几个例子

例1

设机器数字长8位,补码计算\(45-54\)

第一种方法:教科书方法

\[45_{(补)}=45_{(原)}=00101101_{(2)} \\ -54_{(补)}=54_{(反)}+1=11001010_{(2)} \\ 于是45-54=00101101_{(2)}+11001010_{(2)}=11110111_{(2)} \\ 于是\bar x=-x-1=11110111_{(2)}-1=11110110_{(2)} \\ x=-1001_{(2)}=-9_{(10)} \]

第二种方法:补码的本质

\[-54\equiv202\pmod {256} \\ 于是\\ 45-54\equiv45+202\equiv247\equiv-9 \pmod {256} \\ \]

显然补码\(247\)对应的原码为\(-9\),故答案为\(-9\)

例2

字长8位,判断下列算式是否发生溢出,若不溢出,计算其结果。

\[-85-60 \]

第一种方法:教科书方法

首先计算出\(-85\)的补码为\(10101011_{(2)}\)\(-60\)的补码为\(11000100_{(2)}\)。于是,

\[10101011_{(2)}+11000100_{(2)}=101101111_{(2)} \]

截断后,发现变为\(01101111\),首位消失。所以产生了溢出。

第二种方法:补码的本质

\[-85\equiv171\pmod {256} \\ -60\equiv196\pmod {256} \\ -85-60\equiv171+196\equiv367\equiv111\pmod {256} \]

而补码\(111\)对应的原码就是\(111\),是一个正数。发生了溢出。

posted on 2020-12-22 13:56  gnuemacs  阅读(636)  评论(0编辑  收藏  举报