自从香农的硕士论文《继电器与开关电路的符号分析》问世,布尔代数和开关电路的完美结合开启了数字电路和电子计算机的崭新时代,莱布尼茨的梦想终于在他发明二进制两百多年后实现,采用二进制是冯·诺依曼体系结构计算机的基本原则,因为它非常契合半导体开关器件和数字电路,使计算机的电路结构变得简单且更加可靠。采用补码进行数的存储和运算,则进一步优化了电路和计算。
1.为什么需要补码?
为了简化电路结构,提高运算速度,计算机的算术运算仅通过加法器和移位电路实现。相信大家首先想到的问题是减法怎么实现?答案是用补码,补码是一种规则简单且能够通过加法运算实现减法的码制。通过使用补码,计算机可以统一处理正数和负数,简化了内部的设计和逻辑。
补码的发明者并不明确,可以确定的是,冯·诺依曼(John von Neumann)将其应用到了计算机设计中。
为了能够让初学者理解补码,现有通用教材都会用十进制的加减运算以及时钟指针的顺时针或逆时针拨动来类比说明,但一般都是点到为止,往往又给同学们(特别是喜欢寻根问底的同学)带来新的困惑,网上这种疑问很多,有的同学直到毕业也没有搞清楚。那我们就先从十进制数类比说起。
2.关于十进制类比问题
我们知道,两个正数相减,可以看成是一个正数和一个负数相加,但这只是形式上的加法,在人脑计算的过程中,做的仍然是绝对值的减法。那能不能用真正的加法来实现这个减法呢?通用教材里给出了一个互补的概念,即有
a
a
a、
b
b
b两个数,若满足
a
+
b
=
m
a+b=m
a+b=m,则称
a
a
a与
b
b
b关于
m
m
m互补,若
a
a
a和
b
b
b是
m
m
m进制数,那我们就可以说
a
a
a是
−
b
-b
−b对模
m
m
m的补数(有的教材称之为补码,这里称补数以与二进制补码区别),这样,关于
b
b
b的减法(
b
b
b为减数),就可以用它的补数
a
a
a通过加法实现。例如:十进制数运算
3
−
2
⇒
转化为
3
+
(
−
2
)
补
=
3
+
8
=
(
1
)
1
3-2\xRightarrow{\text{转化为}}3+(-2)_{\text{补}}=3+8=(1)1
3−2转化为3+(−2)补=3+8=(1)1
此处,进位自动舍去(去模),得到了理想的结果。
再看一个例子
2
−
4
⇒
转化为
2
+
(
−
4
)
补
=
2
+
6
=
8
⇒
?
选择
8
o
r
(
−
2
)
补
2-4\xRightarrow{\text{转化为}}2+(-4)_{\text{补}}=2+6=8\xRightarrow{?\text{选择}}8\mathrm{ or (}-2)_{\text{补}}
2−4转化为2+(−4)补=2+6=8?选择8or(−2)补
上式中,如果我们认为8是
−
2
-2
−2的补数,则计算结果正确。但是,由于8和
−
2
-2
−2关于模10同余,怎样确定这个8是负数
−
2
-2
−2的补数而不是8自身,引出以下问题:
①单从计算结果分析,如果没有新的定义,我们确实不能直接判断应该是8还是$-$2,例如有一个新的运算 4 + 4 = 8 4+4=8 4+4=8,和上式放在一起,不管怎么判定,这两个运算总有一个会错,因为计算结果须有唯一性。所以,我们需要一个清晰的定义。
②按照上一步的分析,补数运算结果8只能代表一个确定的数,而我们确定
2
−
4
=
−
2
2-4=-2
2−4=−2,所以这里的8就是负数
−
2
-2
−2的补数,代表的是
−
2
-2
−2,这就是我们寻求定义的方向,本质上是通过无符号数表示有符号数,造成的结果是牺牲了数的范围,所以,非常遗憾,+8超出了这个范围。
2
−
4
⇒
转化为
2
+
(
−
4
)
补
=
2
+
6
=
8
=
(
−
2
)
补
2-4\xRightarrow{\text{转化为}}2+(-4)_{\text{补}}=2+6=8=\mathrm{ (}-2)_{\text{补}}
2−4转化为2+(−4)补=2+6=8=(−2)补
综上,在利用补数通过加法实现减法的过程中,1位十进制数,其10个基数原本表示10个无符号数0~9,而现在则表示10个有符号数
−
5
-5
−5 ~ 4,具体来说就是0 ~ 4还是0 ~ 4,而5 ~ 9代表
−
5
-5
−5 ~
−
1
-1
−1的补数,即一半表示零和正数,一半表示负数。
在这个定义中,明确了有符号数的范围,因为负数的递增顺序和表示它的无符号数(补数)的递增顺序完全一致,且 9 + 1 = ( 1 ) 0 9+1=(1)0 9+1=(1)0,等价于 − 1 + 1 = 0 -1+1=0 −1+1=0,因此运算结果唯一且不会出错。但是,基于有符号数的定义范围,运算结果也会被限定在$-$5~4范围内,所以,前述 4 + 4 = 8 4+4=8 4+4=8中的8依然是$-$2的补码,计算结果不正确是因为溢出错误。
以上就是对通用教材中补码的十进制类比问题的剖析,不具有现实意义,但是对理解下文二进制补码的内容还是有所助益的。
顺便提一下教材中常用的时钟类比的例子,因为时钟本身构成一个闭环,所以指针从不同路径(方向)走到同一位置,两个路径长度的值是关于12互补的,引申出两点:一是如果把拨动指针看作一个运算,无论指针朝哪个方向拨动(加或减),到达同一个位置,就意味着计算结果相同且具有唯一性,即所有减法(逆时针调整)都可以通过补数的加法(顺时针拨动指针)实现;二是指针过12点后开始新的时钟周期,相当于自动舍去进位(模12)。
3.原码、反码和补码
直接用正负号表示的有符号二进制数或对应十进制数,我们称其为真值,例如+5,+101B(为简便起见,下文具体计算例子均以4位(bit)为例,即有符号数的数值位为3bit)。
能为计算机所识别的数的二进制表示形式,称为机器数或机器码。具体来说,将符号数值化,即在最高位用0表示 “+”,1表示 “ − - −”,这样,有符号二进制数的机器数就包括符号位和数值位,例如0101可作为+5的机器数。
⑴从原码到补码
①原码是最基本的机器数,即符号位用0、1表示,数值位不变(相对真值),例如+5(+101B)的原码是0101, − 3 -3 −3( − - − 011B)的原码是1011。
原码不能直接用于计算机的加减运算,因为加法器不能实现原码的减法,即使同符号数的加法,也可能出错,例如用原码计算
(
−
1
)
+
(
−
2
)
(-1)+(-2)
(−1)+(−2):
1001
+
1010
=
(
1
)
0011
⟺
(
−
1
)
+
(
−
2
)
=
3
×
1001+1010=\left( 1 \right) 0011\Longleftrightarrow \left( -1 \right) +\left( -2 \right) =3\textcolor{#FF0000}{\times}
1001+1010=(1)0011⟺(−1)+(−2)=3×
出错的根源是加法器在做计算时,并不判断数的正负,符号位也被当作一位数,至始至终做的是无符号数的加法,这导致原码的数值递增方向和真值产生了背离。所以,我们要根据互补关系找到合适的无符号数来表示有符号数,也就是设计一个补码体制,来解决有符号数的计算问题。
②现设字长为4,对无符号数,包含0(0000)~ 15(1111)共16个数。根据上文(第2节)的经验,我们尝试用其中一半0000 ~ 0111来表示零和正数(0 ~ +7),这正是它们的原码;那么另一半就用来表示负数,即1000~1111表示 − 8 -8 −8 ~ − 1 -1 −1的补码(模16),最小的 − 8 -8 −8用最小的1000表示,随后的 − 7 -7 −7用1001表示,最大的 − 1 -1 −1用最大的1111表示。显然,补码(看作无符号数)与所代表的负数的递增顺序一致,且 1111 + 1 = ( 1 ) 0000 1111+1=(1)0000 1111+1=(1)0000,这表示 − 1 -1 −1和0能够完美衔接,互为相反数的两个数的和自然为零(原码做不到这一点)。如果定义正数的补码就是原码,那么 − 8 -8 −8~ +7所有16个数的补码在计算的效果上是依次递增的,这确保了计算结果的唯一性和正确性。
根据以上分析,设机器的数字长为4,若
m
m
m为
−
8
-8
−8~ +7中的一个数(
−
8
-8
−8的原码是11000,字长为5,特例),
m
m
m表示真值,其绝对值(也是原码数值位的值)为
M
M
M,则
m
m
m的补码为
[
m
]
补
=
(
2
4
+
m
)
2
=
{
M
=
[
m
]
原
0
≤
m
≤
7
(
2
4
−
M
)
2
−
8
≤
m
<
0
[m]_{\text{补}}=(2^4+m)_2=\left\{ \begin{array}{c} M=[m]_{\text{原}}~~~~~~~~~0\le m\le 7\\ (2^4-M)_2~~~~-8\le m<0\\ \end{array} \right.
[m]补=(24+m)2={M=[m]原 0≤m≤7(24−M)2 −8≤m<0
扩展到
n
n
n位字长,数
x
x
x(真值)的绝对值
∣
x
∣
=
X
\left| x \right|=X
∣x∣=X,则
x
x
x的补码定义为
[
x
]
补
=
(
2
n
+
x
)
2
=
{
[
x
]
原
0
≤
x
≤
2
n
−
1
−
1
(
2
n
−
X
)
2
−
2
n
−
1
≤
x
<
0
[x]_{\text{补}}=(2^n+x)_2=\left\{ \begin{array}{c} \mathrm{ [}x]_{\text{原}}~~~~~~~~~~~0\le x\le 2^{n-1}-1\\ (2^n-X)_2~~~~-2^{n-1}\le x<0\\ \end{array} \right.
[x]补=(2n+x)2={[x]原 0≤x≤2n−1−1(2n−X)2 −2n−1≤x<0
简言之,正数的补码与原码相同,字长为
n
n
n的负数的补码与其绝对值(原码数值位的值)关于
2
n
2^n
2n互补。由此易知,求补码的补码,可得原码。
③我们起初定义的补码,并没有考虑符号问题,但最终却得到了一个附加成果:正数的补码最高位为0,负数的补码最高位为1,跟原码的符号位一致。于是,就有了补码的符号位这一概念,做到了所谓的符号位和数值位的统一。
⑵符号位含权
设数
x
x
x的补码
[
x
]
补
=
k
n
−
1
a
n
−
2
a
n
−
2
⋯
a
1
a
0
\left[ x \right] _{\text{补}}=k_{n-1}a_{n-2}a_{n-2}\cdots a_1a_0
[x]补=kn−1an−2an−2⋯a1a0,则其代表的十进制真值为:
x
=
−
2
n
−
1
×
k
n
−
1
+
∑
i
=
0
n
−
2
a
i
×
2
i
x=-2^{n-1}\times k_{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i}
x=−2n−1×kn−1+i=0∑n−2ai×2i
上式表示补码的符号位具有
−
2
n
−
1
-2^{n-1}
−2n−1的权值,它以数学形式说明了补码为什么能保持和真值一致的递增顺序,也体现了正数和负数的统一。一个简单的例子:一个数的补码是1011,则其真值为
−
8
+
3
=
−
5
-8+3=-5
−8+3=−5。
以下分析过程,供选择性阅读。
n n n位二进制数 a n − 1 a n − 2 a n − 2 ⋯ a 1 a 0 B a_{n-1}a_{n-2}a_{n-2}\cdots a_1a_0\mathrm{B} an−1an−2an−2⋯a1a0B,对应十进制数为:
D = ∑ i = 0 n − 1 a i × 2 i D=\sum_{i=0}^{n-1}{a_i\times 2^i} D=i=0∑n−1ai×2i
若数 x x x的补码 [ x ] 补 = k n − 1 a n − 2 a n − 2 ⋯ a 1 a 0 \left[ x \right] _{\text{补}}=k_{n-1}a_{n-2}a_{n-2}\cdots a_1a_0 [x]补=kn−1an−2an−2⋯a1a0,将其看作无符号数展开,得
x 补 = k n − 1 a n − 2 a n − 2 ⋯ a 1 a 0 B = k n − 1 × 2 n − 1 + ∑ i = 0 n − 2 a i × 2 i x_{\text{补}}=k_{n-1}a_{n-2}a_{n-2}\cdots a_1a_0\mathrm{B}=k_{n-1}\times 2^{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i} x补=kn−1an−2an−2⋯a1a0B=kn−1×2n−1+i=0∑n−2ai×2i
由上文知, [ x ] 补 = ( 2 n + x ) 2 \left[ x \right] _{\text{补}}=(2^n+x)_2 [x]补=(2n+x)2,故十进制真值
x = x 补 − 2 n = k n − 1 × 2 n − 1 + ∑ i = 0 n − 2 a i × 2 i − 2 n x=x_{\text{补}}-2^n=k_{n-1}\times 2^{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i}-2^n x=x补−2n=kn−1×2n−1+i=0∑n−2ai×2i−2n
当 x ≥ 0 x\ge 0 x≥0时, k n − 1 = 0 k_{n-1}=0 kn−1=0
x = ∑ i = 0 n − 2 a i × 2 i − 2 n ⇔ 去模 ∑ i = 0 n − 2 a i × 2 i x=\sum_{i=0}^{n-2}{a_i\times 2^i}-2^n\xLeftrightarrow{\text{去模}}\sum_{i=0}^{n-2}{a_i\times 2^i} x=i=0∑n−2ai×2i−2n去模 i=0∑n−2ai×2i
当 x < 0 x<0 x<0时, k n − 1 = 1 k_{n-1}=1 kn−1=1
x = 2 n − 1 + ∑ i = 0 n − 2 a i × 2 i − 2 n = − 2 n − 1 + ∑ i = 0 n − 2 a i × 2 i x=2^{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i}-2^n=-2^{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i} x=2n−1+i=0∑n−2ai×2i−2n=−2n−1+i=0∑n−2ai×2i
综合正、负数两种情况,有
x = − 2 n − 1 × k n − 1 + ∑ i = 0 n − 2 a i × 2 i x=-2^{n-1}\times k_{n-1}+\sum_{i=0}^{n-2}{a_i\times 2^i} x=−2n−1×kn−1+i=0∑n−2ai×2i
⑶补码怎么转换?反码出场
根据补码的定义,求负数的补码需要做减法运算,还有一种方法,是根据二进制减法的特点衍生而来:已知负数的原码,从原码最低位自右向左找到第一个1,则1和右边的0保持不变,1左边的其余各位取反(符号位不变仍为1)。
例如:原码为 1 10 1 1\color{red} {10}\color{green}{1} 1101,则补码为 1 01 1 1\color{red} {01}\color{green}{1} 1011;原码为 1 0 10 1\color{red} {0}\color{green}{10} 1010,则补码为 1 1 10 1\color{red} {1}\color{green}{10} 1110。
以上两种方法,显然不适合机器,于是,科学家们找到了反码,它的生成很容易,转换补码更容易。
①补码被称为是关于“2的补”,反码则是关于“1的补”,意思是用“全1”减去负数的数值位(绝对值),即可得其反码,例如
−
5
-5
−5(
−
101
B
-101\mathrm{B}
−101B)的反码为:
[
−
101
B
]
反
=
1111
−
101
=
1010
[-101\mathrm{B]}_{\text{反}}=1111-101=1010
[−101B]反=1111−101=1010
将求得的反码1010跟原码是1101比较,可以看出:符号位不变,其他各位取反,这就是求负数反码的方法,也是反码名称的由来。
正数的反码同原码。
②因
n
n
n位字长的“全1”等于
2
n
−
1
2^n-1
2n−1,易得
n
n
n位字长的负数
x
x
x的反码和补码的关系:
[
x
]
补
−
1
=
[
x
]
反
[x]_{\text{补}}-1=\left[ x \right] _{\text{反}}
[x]补−1=[x]反,故
[
x
]
补
=
[
x
]
反
+
1
\left[ x \right] _{\text{补}}=\left[ x \right] _{\text{反}}+1
[x]补=[x]反+1
例如:
[
−
5
]
原
=
1101
[-5]_{\text{原}}=1101
[−5]原=1101,
[
−
5
]
反
=
1010
[-5]_{\text{反}}=1010
[−5]反=1010;
则 [ − 5 ] 补 = 1010 + 1 = 1011 [-5]_{\text{补}}=1010+1=1011 [−5]补=1010+1=1011。
③为什么不直接用反码进行运算?
既然反码跟补码非常接近,也具有跟真值相同的递增顺序,为什么不直接使用反码进行数的存储和运算呢?因为反码没有实现零的统一, [ − 0 ] 反 = 1111 [-0]_{\text{反}}=1111 [−0]反=1111, [ + 0 ] 反 = 0000 [+0]_{\text{反}}=0000 [+0]反=0000, − 1 -1 −1到+0产生了跳跃,因此不能得到正确的计算结果。
4.补码运算的溢出问题
同符号数相加,在确定的字长范围内,其绝对值的和超出数值位所能表示的范围,就会产生溢出错误。解决溢出的方法是位扩展,即增加数值位。
判断溢出的方法很多,有关计算机课程有专门介绍,最简单直接的方法就是比较符号位的变化,计算结果的符号位与两个加数的符号位不同,则说明有溢出。