谈谈计算机中的四则运算

经过前面 从零开始造一台自己的加法器 的学习,现在我们已经能造出一台简易的二进制加法器,可以利用它来做四位二进制的加法。那么减法、乘法和除法呢,难道我们也要一一构造减法器、乘法器和除法器吗?

加法和减法

8 位二进制加法器

四位二进制加法器

将两个四位二进制加法器串联起来,我们就能得到一个 8 位的二进制加法器。也就是说,我们想要相加的二进制数,其范围是从 0000 0000 到 1111 1111,即十进制的 0 到 255。两个 8 位二进制数的和最大可为 1 1111 1110,即 510。

减法与借位

当你确信继电器连接到一起真的可以实现二进制加法的时候,你可能会问:『那么如何实现减法呢?』提出这样的问题并不是说你在没事找事,而实际上这表明你是相当有观察力的。加法和减法在某些方面相互补充,但在机制方面这两个运算则是不同的。加法是始终从两个加数的最右列向最左列进行计算的。每一列的进位加到下一列中。在减法中没有进位,而是有借位——一种与加法存在本质区别的麻烦机制。

例如,我们来看一个典型的借位减法的题目:

 253
-176
----
 ???

结果等于多少呢?

要想计算结果,首先从最右列着手。我们看到,6 是大于 3 的,因此从 5 借 1,再用 13 减去 6,得到结果为 7。由于我们已经在 5 上借了 1,因此,现在实际上那一位是 4,而 4 是小于 7 的,因此继续从 2 借 1,14 减 7 结果为 7。由于 2 借了 1,实际上这一位是 1,从 1 中减去 1,结果为 0。因此,最后结果应为 77

 253
-176
----
  77 

如何才能通过一连串逻辑门来实现这个反逻辑呢?
然而,我们并不打算这样做。相反,我们打算利用一个小技巧来让减法不涉及借位。

在日常生活当中,如果你仔细观察的话会发现:
把某物体左转 90 度,和右转 270 度,在不考虑圈数的条件下,最终的效果是相同的;
把分针顺时针拨 20 分钟,和逆时针拨 40 分钟,在不考虑时针的条件下,效果也是相同的;
把数字 87,减去 25,和加上 75,在不考虑百位数的条件下,效果也是相同的;
……

上述几组数字,有这样的关系:
  90 + 270 = 360
  20 + 40 = 60
  25 + 75 = 100
式中的 360、60 和 100,就是『模』。假如提前就知道模的值,我们可以把减法化为加法。

自定义编码

让我们来想一想。通常用来表示正数和负数的方法,其好处就在于能表示所有的正数、负数。我们将 0 想象为这个无限延伸的序列的中点。这个序列中正数沿着一个方向延伸,而负数则按照另一个方向延伸:

...-10000, -9999...-3, -2, -1, 0, 1, 2, 3...9999, 10000...

但是,如果我们并不需要无限大或无限小的数,而且在开始的时候我们就可以预知所使用的数字的范围,那情况就有所不同了。

以支票账户为例,这里人们通常会遇到负数。假设我的账户余额小于 500 美元,并且银行给了我们 500 美元的无息预支额度,意思就是账户余额数值应该是一个在 499 美元到 -500 美元之间的数。假设我们不会一次取出 500 美元,我们也不会支出多于 500 美元的金额,这里我们用到的只是美元,不涉及美分。这些假设表明账户能处理的额度是介于 -500 到 499 之间,总共 1000 个数,而我们只用三位十进制数就可以表示所有需要的数字。我们并不需要用到从500 到 999 之间的正数,因为我们所需要的数的最大值为 499。因此从 500 到 999 的三位数可以用来表示负数。具体情况如下:

用 500 表示 -500
用 501 表示 -499
用 502 表示 -498
......
用 998 表示 -2
用 999 表示 -1
用 000 表示  0
用 001 表示  1
用 002 表示  2 
......
用 497 表示  497
用 498 表示  498
用 499 表示  499

也就是说,以 5, 6, 7, 8 或 9 开头的三位数实际上表示的都是负数,而不是把数字写成这样:

—500, -499, -498...-4, -3, -2, —1, 0, 1, 2, 3, 4...497, 498, 499

用这种表示法,我们可以将它们写成:

500, 501 ,502...996, 997, 998, 999,000, 001, 002, 003, 004...497, 498, 499

注意,这就形成了一个循环排序。最小的负数(500)看起来像是最大正数(499)的延续。而数字 999 (实际上是 -1)是比 0 小 1 的第一个负数。如果我们在 999 上加 1,通常会得到 1000。由于我们处理的是三位数,这个结果实际上就是 000。这种标记方法称为 10 的补数(ten’s complement)为了将三位负数转化为 10 的补数,我们用 999 减去它再加 1。也就是说,对 10 求补数就是对 9 的补数再加 1。例如想要得到 -255 对 10 的补数,用 999 减去 255 得到 744, 然后再加 1, 得到 745。你可能听说过:『减一个数就等于加一个负数。』你可能会回答:『实际上还是减去了这个数。』然而,利用 10 的补数,我们将不会再用到减法。所有的步骤都用加法来进行。

那么回到我们的 8 位二进制数,如果我们这样编码:

二进制数 十进制数
1000 0000 -128
1000 0001 -127
1000 0010 -126
1000 0011 -125
1111 1101 -3
1111 1110 -2
1111 1111 -1
0000 0000 0
0000 0001 1
0000 0010 2
0111 1101 125
0111 1110 126
0111 1111 127

注: 在这里模为 1 0000 0000,即 256。

这个编码为我们提供了一种不用负号就能表示正负数的方法。同样也让我们自由地将正数和负数用加法法则相加。例如,将 127 和 -1 等价的两个二进制数相加。利用上面的表格,可以简单写为:

 127          0111 1111
-  1   <=>  + 1111 1111
----          ---------
 126        1 0111 1110 ,忽略最左边的进位 1,最后结果为 126。
 
或

 127          0111 1111
-128   <=>  + 1000 0000
----          ---------
  -1          1111 1111,结果为 -1。
  

要注意的是,这里涉及了上溢和下溢情况,即结果大于 127 或小于 -128。例如,将 125 与它自身相加

 125          0111 1101
+125   <=>  + 0111 1101
----          ---------
 250          1111 1010 ,结果为 -6,出现上溢。
 

将 -125 与它本身相加也会出现相同的情况。

-125          1000 0011
-125   <=>  + 1000 0011
----          ---------
-250        1 0000 0110 ,结果为 6,出现下溢。

  

现在,二进制数可以有两种不同的使用方法。二进制数可以是有符号的,也可以是无符号的。无符号的 8 位二进制数所表示的范围是 0 ~ 255。有符号的 8 位二进制数表示的范围是 -128 ~ 127。无论是有符号的还是无符号的,数字本身是无法表示的。例如,如果有一个人问:『有一个 8 位二进制数,值为 10110110。它相当于十进制的多少?』你必须先问:『它是有符号数还是无符号数?它可能为 -74 或 182。』
这就是二进制的麻烦之处,它们只是一些 0 和 1,本身并没有任何含义。

乘法和除法

移位 + 加法

扩展

Q1

这时假如有人问你:Java 中值为 -2 的 int 型变量在内存中的后 8 位是多少?

我们仔细审一下这个题目,发现其中其实隐含了两个问题:

  • java 中『基本数据类型』在内存中分别用几位表示
  • 负数在计算机中如何编码

下面我们就来一一解答。

A1

java 中『基本数据类型』在内存中分别用几位表示

类型名称 类型定义 取值范围
boolean 布尔值,作二元判断 true,false
char 16 位 Unicode 字符 Unicode 字符列表
byte 8 位有符号整数 最小值 -128,最大值 127
short 16 位有符号整数 最小值 -32768,最大值 32767
int 32 位有符号整数 最小值 -2147483648(-2³¹),最大值 2147483647(2³¹-1)
long 64 位有符号整数 -2⁶³ ~ 2⁶³-1
float 32 位浮点数 IEEE_754
double 64 位浮点数 IEEE_754

Java 中所有数值类型都有正负号,所以不要去寻找无符号的数值类型。

负数在计算机中如何编码
8 位有符号数可表示数值范围:-2⁷ ~ 2⁷-1,即 10000000 ~ 01111111
32 位有符号数可表示数的范围: -2³¹ ~ 2³¹-1,即 10000000 00000000 00000000 00000000 ~ 01111111 11111111 11111111 11111111。

所以 -2 在内存中表示应为: 11111111 11111111 11111111 11111110。

代码验证

1
2
3
4
5
6
7
8
9
10
11
/**
* 负数在内存中的二进制表示
*
*/
public class NegativeNumberInMemory {
public static void main(String[] args) {
int i = -2;
System.out.println(Integer.toBinaryString(i));
}
}

<img -2-in-memory src=”/img/hufei/2018/01/-2-in-memory.png” width=”100%” height=”50%”>

引用