阅读本文需提前学习《编码---隐匿在计算机软硬件背后的语言》前十二章
上一章中我们通过全加器中的“加和输出”和“进位输出”在电路中完成了加法运算。
我们知道加法中存在进位,而减法中存在的则是借位,例如下面的式子:
16 - 8 = ?
减数16的个位要比被减数的个位大,所以需要从被减数的十位借1个10(1位)过来完成整个运算。
在计算机中其实没有用来实现减法的逻辑门,它是通过加法来实现的。因为任何减法比如X - Y都可以表示为X + (-Y)。因为加法器我们已经完成,那么现在的问题就变成了负数如何在计算机中表示?
你可能会说直接在数字前面加符号即可,但是要知道计算机其实只认识“0”和“1”,它并不认识负号。
其实解决方法也很简单,假设我们的十进制数取值范围是-128-127,那么从0-127我们就用他们对应的8位二进制范围就是00000000-01111111。
我们发现8位二进制数的范围是00000000-11111111,而刚刚我们只是用到了所有最高位是0的二进制数字,以1开头的二进制数字都没有用到,那么我们就用这些以1开头的二进制数字来表示-128--1这个范围内所有的数字。也就是说8位二进制的最高位是一个符号位,1代表负数,0代表正数。
到此我们解决了上面提出的问题,负数如何在计算机中表示。我们回到最开始的那个式子:
16 - 8 = ?
我们可以把式子写成下面的形式:
16 - 8 = 16 + (-8)
就是上面我们所说的任何减法都可以写成X + (-Y)的形式。
16的二进制是00010000,而-8的二进制形式就是上面我们所说的10001000,其中最高位的1代表是负数的意思。
那么两个数相加
0 0 0 1 0 0 0 0
+1 0 0 0 1 0 0 0
—————————
10 0 1 1 0 0 0
换算成十进制就是-24,这显然不是我们想要的答案。
在这里我们要明白,负数在计算机内部用什么方式表示都是无所谓的,只要能保证一一对应的关系就可以。那么肯定是怎么方便怎么来。上面的形式虽然说可以表示一定范围内的每个负数,但是正常的加法规则不适用于正数与负数的加法,因此必须制定两套运算规则,一套用于正数加正数,还有一套用于正数加负数。从电路上说,就是必须为加法运算做两种电路。于是,我们的先人们就发明了“补码”这种东西。
所谓补码,在二进制中,就是说正数的补码还是其本身,而负数的补码是首位不变,其原码(本身)取反变成反码再加1。或者说成是其绝对值的原码取反再加1。
这样做的好处是什么呢?
还看上面的例子,-8的补码应该是10001000取反得到反码11110111再加1得到补码11111000。两个数相加
0 0 0 1 0 0 0 0
+1 1 1 1 1 0 0 0
————————
1 0 0 0 0 1 0 0 0
可以看到,按照正常的加法规则,得到的结果是100001000。注意,这是一个9位的二进制数。我们已经假定这是一台8位机,因此最高的第9位是一个溢出位,会被自动舍去。所以,结果就变成了00001000,转成十进制正好是8,也就是16 + (-8) 的正确答案。这说明了,2的补码表示法可以将加法运算规则,扩展到整个整数集,从而用一套电路就可以实现全部整数的加法。
所以说,在计算机中,所有数字都是以补码的形式存储的。
现在我们已经完美解决了负数在计算机中的表示方式,下面我们只需要通过逻辑门来搭建出可以计算减法的工具即可。
我们把之前全加器的面板做一点改动,如下图:
它在最左侧增加一个开关和一个灯泡,开关用来选择加法还是减法。灯泡表示“上溢/下溢”。这个灯泡表明了正在计算的数字是一个不能用8个灯泡表示的数字。如果在加法中得到的结果大于255(上溢)或者在减法中得到了负数(下溢)这个灯泡就会发光。
这个设备的加法部分我们在上一章已经完成,现在只需要解决减法部分即可,我们已经知道在计算机中,所有数字都是以补码的形式存储的。在二进制中取补码只要把对应的1变成0、0变成1后再加1即可。自然而然就可以想到反向器,它的功能正是我们需要的。我们直接把电路改造为如下图:
标记为“取反”的线路将被输入到每一个异或门中,回想一下异或门的工作方式:
我们可以发现,当“取反”信号是0,则8个异或门输出与输入是相同的。当“取反”信号是1,8个异或门的输入与输出则不同。这个装置叫做求补器,如下所示:
我们想要做的是在一个机器里同时实现加/减法,将一个求补器,一个8位二进制加法器和一个异或门做如下连接:
上图三个"SUB"标识就是加/减法转换开关,当都为0时,B输入经过求补器不变,CI进位输入为0,CO进位输出经过异或门仍然等于其本身。可以看到其做的就是加法运算。当CO为1时(代表最后有进位)最左侧灯泡亮,表示加法计算结果大于255。
当三个“SUB”都为1时,进行的是减法运算既“X + (-Y)”,我们要得到的是-Y(B输入)的补码,B输入经过求补器取反得到输出,CI端进入表示加1,这里就巧妙的得到了B输入的补码(这里就是将-Y的绝对值的原码取反再加1)。之后再将其与A输入相加即可得到结果。还有一点要注意的是当减数(B输入)小于被减数(A输入)时,CO端输出为1代表溢出(既减法最后一步减去100000000,或者叫忽略进位),因为SUB也为1,所以两个信号经过一个异或门输出为0,所以最左侧灯不亮代表结果是正数。相反,当CO端输出为0时,最左侧灯泡亮,代表下溢,既结果是个负数。
最后我们用一个例子来看下关于减法部分的原理。
一、减数小于被减数
3(00000011) - 2(00000010) = ?
等于求
3 + (-2) = ?
第一步:求-2的补码,既求-2的绝对值2的原码取反(11111101)再加1得11111110。在上图中就是输入B经过取反再加CI等于11111110。
第二步:3的补码(00000011)加上-2的补码(11111110)等于100000001。在上图中就是最后进位1等于CO输出(舍去)即可得到00000001。而CO与SUB均为1,经过异或门为0,灯泡不亮。
二、减数大于被减数
2 - 3 = ?
等于求
2 + (-3) = ?
第一步:求-3的补码,既求-3的绝对值3的原码取反(11111100)再加1得11111101。在上图中就是输入B经过取反再加CI等于11111101。
第二步:2的补码(00000010)加上-3的补码(11111101)等于11111111。在上图中就是最后进位0等于CO输出。而SUB为1,两个输入1、0经过异或门得1,最左侧灯泡亮,表示结果为负数。
这里结果11111111明明是255,为什么说是个负数呢?
还记得前面出现的这个图吗?
我们把最高位为1的所有8位二进制数去表示-128--1了,所以11111111就是-1。
至此我们在一个电路中实现了加法与减法。