目录
引言
我们知道计算机所能处理的数都是二进制的。准确的说,我们的硬件设备只能对由纯01序列组成的、无符号且固定位长的二进制整数进行运算。
为了方便起见,如无特殊说明,以下称二进制数,均专指 由纯01序列组成的、无符号且固定位长的二进制整数
但是现实生活中我们要处理的数字有正数、有负数,有整数、有小数。为了让计算机能够处理小数的运算,人们提出了浮点数的概念,并设计了相关硬件单元(即浮点运算器,FPU,这已经超出了本文的讨论范围,就不在展开了);而为了能让我们的计算机对正负数进行算数运算,人们发明了原码、反码和补码,并设计了包含加法器、乘法器等硬件单元的算术逻辑单元 ALU。
需要解决的问题
前面提到,我们的计算机硬件设备(CPU 中的 ALU)只能对无符号的二进制整数进行运算。现在为了让我们的计算机能够处理正负数,我们需要解决的问题有两个:
- 如何使用二进制数(或者说用二进制的01序列)表示正数和负数。
- 如何用二进制数的运算来表示正负数的算数运算。
也就是说,我们需要一种将正负数表示为二进制数的方案,使我们可以用二进制数的运算来代替正负数的运算。这里我们将这类方案成为数的二进制编码,简称编码。
如果将某一个数 N
的编码记做
(
N
)
编
(N)_编
(N)编 ,那么我们的编码方案应该满足能够使用
(
N
)
编
(N)_编
(N)编 之间的运算或这些运算构成的算法来表示X之间的运算。
即,对于整数X、Y之间的算数运算 ¤(¤为加减乘除等),如果存在由二进制数之间的运算构成的算法 alg ,使得
a
l
g
(
(
X
)
编
,
(
Y
)
编
)
=
(
X
¤
Y
)
编
alg((X)_编,(Y)_编)=(X¤Y)_编
alg((X)编,(Y)编)=(X¤Y)编
那么我们就可以说在这种编码下,可以用二进制数的运算来表示正负数的算数运算。
运算与硬件
在开始介绍原码、反码与补码之前我们需要先讨论一些关于运算与计算机硬件的关系的知识。
- CPU有专门对二进制数进行运算的硬件单元,称为算术逻辑单元ALU。ALU能对二进制数进行的运算,除了加减乘除等算数运算之外,还有与、或、非、异或等位逻辑运算和移位运算。
- 在ALU中,加减乘除这些算数运算由特定的电路完成,如加法器、乘法器、除法器。没有减法器的原因是,利用补码可以将减法与加法统一,关于这一点,后面会详细讨论。
- 虽然算数运算远不止加减乘除这四种,但是ALU仅提供了对这四种运算的支持,其他的算数运算由软件算法基于这四种运算来完成。
- 乘法器和除法器都是由加法器构成的。事实上二进制数的乘法和除法运算都可以拆解为加法和减法运算(对这方面内容感兴趣的,可百度乘法器和除法器)。也就是说只要我们的编码方案能够完成加减运算,那么就一定可以满足加减乘除四则运算。
最佳的编码方案
通过上面的讨论,我们可以明确编码所要面对的问题,以及目前可用的资源。
ALU已经支持的运算包括二进制数的加法、乘法和除法,以及位与、位或、位非等位运算和移位运算。现在的问题就是如何设计一种编码过程,将一个整数表示成二进制数,从而利用二进制数的各种运算来模拟整数的运算,二进制数运算的结果经过编码的反过程(即解码)得到的整数要正好是原来两个整数运算的结果。这个过程可以用下面的图表示(假定 A+B=C):
如上图所示,这涉及到了三个过程:编码、解码和运算。通常编码和解码的过程仅在需要反馈给我们人时发生一次,而运算的过程会出现多次,例如 处理A+B+C,会首先将ABC进行编码,然后执行两次加法过程,最后将结果解码反馈给我们。所以应当尽量使运算的过程简单,即用更少的二进制数运算过程来表示整数的运算过程,而编码解码可以适当得复杂。
所以最佳的编码方案,就是可以直接使用二进制编码的加减运算来表示整数的加减运算,即:
事实上,补码就是完美的做到了这一点,并且补码的编码和解码过程也不复杂。
原码
为了表示正负数,一种最简单直接的方式,就是规定二进制数的最高位为符号位。符号位为0则表示该数为正数,符号位为1表示该数为负数,剩下的为数值位,数值位数值是多少,则所代表数的绝对值就是多少。人们将这个方案成为原码。
例如,在4位字长的机器上:
5的原码:
(
5
)
原
=
0101
(5)_原=0101
(5)原=0101,-5的原码:
(
−
5
)
原
=
1101
(-5)_原=1101
(−5)原=1101
原码的问题
原码是一种最简单、最直接的表示方案,但是直接使用原码会给带来一些问题。
1. 零的表示不唯一
例如在八位字长的机器上,整数零会有两种表示方法:1000 0000 和 0000 0000
。
2. 原码加减法运算繁杂
例如 -3 + 5 =2
,在4位字长的机器上:-3被表示为 1011
,5被表示为0101
,而1011 + 0101 = 10000
,结果显然不正确。并且由于溢出的存在,高位被截断,所以最终结果为0000
,即0。
只要有一个操作数为负数,我们就不能直接用原码的加减来表示两数的加减。
即,当x、y有一个符号为负时,
x
原
±
y
原
≠
(
x
±
y
)
原
x_原±y_原 ≠ (x±y)_原
x原±y原=(x±y)原
但是有一种情况例外,当且仅当x、y均为正时,
x
原
±
y
原
=
(
x
±
y
)
原
x_原±y_原 = (x±y)_原
x原±y原=(x±y)原
事实上,为了能通过原码来处理负数的加减运算,需要判断参与运算的两个数的正负,然后根据正负进行不同的处理,并且需要将符号位与数值位分开处理,这会导致使用原码来表示正负数加减法运算会十分繁杂。
由于以上两个原因,人们发明了反码与补码。在下面的内容你可以看到,补码是如何用唯一的编码表示零,以及如何做到直接用补码的加法运算来表示正负数的加减运算。
反码
反码是当一个数为正数时(即符号位为0),其反码与原码相同;这个数为负数时(即符号位为1),其反码为原码的数值位按位取反,符号位不变。
可以看出,一个负数所对应正数的原码,跟这个负数的反码相加,得到的结果的所有位都是1, 即对于一个n位的负数 -x(x>0),有:
x
原
+
(
−
x
)
反
=
1111...
=
2
n
−
1
x_原+(-x)_反=1111...=2^n-1
x原+(−x)反=1111...=2n−1
例如 -5:
5
原
=
0101
5_原= 0101
5原=0101,
−
5
反
=
1010
-5_反= 1010
−5反=1010,
5
原
+
(
−
5
)
反
=
1111
=
2
4
−
1
5_原+(-5)_反= 1111=2^4-1
5原+(−5)反=1111=24−1
反码是求补码的过渡码,只需要记住怎么求即可。
溢出与借位
在介绍补码前,需要了解一些溢出与借位的知识,因为计算机所能处理的二进制数都是固定位数的。
溢出
溢出是一个非常重要的概念,溢出是指计算机无法存放过大或者过小的数字,从而产生截断,导致存放数数字与实际的数字不符。
例如 java的byte类型,长度为八位,最高位为符号位,最大可存放的数值范围为 -128~+127,如果存入了+128,那么就产生了溢出,高位会被截断,只保留低位的值。例如前面将 二进制的10000存入四位字长,变成了0000。
在字长为n的机器上,一个字所能存储的最大的值为 二进制的11111…共n个1,转成十进制就是 2 n − 1 2^n-1 2n−1,如果再加一,由于溢出会变成0。
补码就是利用了溢出后高位被截断的特点。
借位
与溢出相对的就是借位,当一个小数减大数时就会产生借位。
例如,在四位机器上:
0010
−
0100
=
1110
0010-0100=1110
0010−0100=1110
补码
一个正数的补码,就是它的原码;而一个负数的补码,就是它的反码加一。
例如,在4位机器上:
3
原
=
0011
,
3
反
=
0011
,
3
补
=
0011
3_原=0011,3_反=0011,3_补=0011
3原=0011,3反=0011,3补=0011
−
3
原
=
1011
,
−
3
反
=
1100
,
−
3
补
=
1101
-3_原=1011,-3_反=1100,-3_补=1101
−3原=1011,−3反=1100,−3补=1101
x
>
0
x>0
x>0时,根据
(
−
x
)
反
=
2
n
−
1
−
x
原
(-x)_反=2^n-1-x_原
(−x)反=2n−1−x原,可得:
(
−
x
)
补
=
(
−
x
)
反
+
1
=
2
n
−
x
原
(-x)_补=(-x)_反+1=2^n-x_原
(−x)补=(−x)反+1=2n−x原,即:
(
−
x
)
补
=
2
n
−
x
原
(-x)_补=2^n-x_原
(−x)补=2n−x原.
我们说引入补码,主要是为了克服原码所存在的问题。
使用补码我们可以将零的表示唯一化:
(
+
0
)
补
=
(
+
0
)
反
=
(
+
0
)
原
=
00000000
(+0)_补=(+0)_反=(+0)_原=00000000
(+0)补=(+0)反=(+0)原=00000000
(
−
0
)
补
=
11111111
+
1
=
00000000
(-0)_补=11111111+1=00000000
(−0)补=11111111+1=00000000
下面我们就讨论补码是如何做到直接用补码的加法运算来表示正负数的加减运算。
补码与正负数加法运算
我们可以将任意两个正数或负数的加减运算转化为如下三种形式之一:
- 正数 + 正数
- 正数 + 负数
- 负数 + 负数
大家可以证明一下这句话的正确性,这里就不展开了。
补码处理 正数+正数
对于 正数+正数 的情况,因为正数的补码与原码相同,又因为前面已经证明过当x、y均为正时:
x
原
±
y
原
=
(
x
±
y
)
原
x_原±y_原 = (x±y)_原
x原±y原=(x±y)原 ,所以:
x
补
+
y
补
=
(
x
+
y
)
补
x_补+y_补 = (x + y)_补
x补+y补=(x+y)补
所以两正数的补码之和,等于两正数之和的补码。
补码处理 正数+负数
对于 正数+负数 的情况,假设 x、y 均为正数,x + (-y) = x - y = - (y - x).
前面已经证明过:
(
−
x
)
补
=
2
n
−
x
原
(-x)_补=2^n-x_原
(−x)补=2n−x原
∵
x
补
+
(
−
y
)
补
=
x
补
+
2
n
−
y
原
=
x
原
+
2
n
−
y
原
=
2
n
−
(
y
原
−
x
原
)
\because x_补 + (-y)_补 = x_补 + 2^n - y_原 = x_原 + 2^n- y_原=2^n- ( y_原-x_原 )
∵x补+(−y)补=x补+2n−y原=x原+2n−y原=2n−(y原−x原)
又
∵
(
x
+
(
−
y
)
)
补
=
(
−
(
y
−
x
)
)
补
=
2
n
−
(
y
−
x
)
原
=
2
n
−
(
y
原
−
x
原
)
又\because (x + (-y))_补=(-(y-x))_补=2^n-(y-x )_原=2^n - (y_原-x_原 )
又∵(x+(−y))补=(−(y−x))补=2n−(y−x)原=2n−(y原−x原)
∴
x
补
+
(
−
y
)
补
=
(
x
+
(
−
y
)
)
补
\therefore x_补 + (-y)_补 = (x + (-y))_补
∴x补+(−y)补=(x+(−y))补
所以正数和负数的补码之和,为正数和负数之和的补码。
补码处理 负数 + 负数
对于 负数 + 负数 的情况,假设 x、y 均为正数,(-x) + (-y) = -(x + y) .
∵
(
−
x
)
补
+
(
−
y
)
补
=
2
n
−
x
原
+
2
n
−
y
原
=
2
n
−
(
x
原
+
y
原
)
+
2
n
\because (-x)_补 + (-y)_补 = 2^n - x_原 + 2^n - y_原 = 2^n-(x_原 + y_原)+2^n
∵(−x)补+(−y)补=2n−x原+2n−y原=2n−(x原+y原)+2n
又
∵
(
(
−
x
)
+
(
−
y
)
)
补
=
(
−
(
y
+
x
)
)
补
=
2
n
−
(
y
+
x
)
原
=
2
n
−
(
y
原
+
x
原
)
又\because ((-x) + (-y))_补=(-(y+x))_补=2^n - (y+x )_原=2^n - (y_原+x_原 )
又∵((−x)+(−y))补=(−(y+x))补=2n−(y+x)原=2n−(y原+x原)
前面提到过,对于 2 n − ( x 原 + y 原 ) + 2 n 2^n-(x_原 + y_原)+2^n 2n−(x原+y原)+2n,由于 2 n 2^n 2n溢出,后面的 2 n 2^n 2n就可以省略,因为可以证明 2 n − ( x 原 + y 原 ) ≥ 0 2^n-(x_原 + y_原)≥0 2n−(x原+y原)≥0。所以 2 n − ( x 原 + y 原 ) + 2 n 2^n-(x_原 + y_原)+2^n 2n−(x原+y原)+2n 和 2 n − ( x 原 + y 原 ) 2^n-(x_原 + y_原) 2n−(x原+y原)最终存储在计算机中的值是一样的。
∴
(
−
x
)
补
+
(
−
y
)
补
=
(
(
−
x
)
+
(
−
y
)
)
补
\therefore (-x)_补 + (-y)_补 = ((-x) + (-y))_补
∴(−x)补+(−y)补=((−x)+(−y))补
即,两负数的补码之和,为两负数之和的补码。
结论
基于以上证明,我们可以得出结论,任意两整数的加法运算,我们都可以使用这两个整数的补码相加来表示,即,对于任意两整数(正数、负数或零) x 、 y x、y x、y,有: x 补 + y 补 = ( x + y ) 补 x_补+y_补=(x+y)_补 x补+y补=(x+y)补
使用补码的好处
该部分内容参考了百度百科,部分表述直接摘自百度百科。百度百科链接
- 使用补码,可以将加法和减法统一为加法,cpu就不载需要提供减法器,使用加法器就可以完成加法与减法运算,简化了硬件结构。
- 克服了原码加减法运算繁杂的弊端,因为原码的加减运算需要判断符号、需要考虑数值位的溢出等。使用补码,可以直接用补码的加法运算表示正负数加减法。
- 使零的表示变得唯一。
- 在计算机中,利用电子器件的特点实现补码和真值、原码之间的相互转换,非常容易。
- 补码表示统一了符号位和数值位,使得符号位可以和数值位一起直接参与运算,这也为后面设计乘法器除法器等运算器件提供了极大的方便。