计算机系统解密:从理解计算机到编写高效代码
上QQ阅读APP看书,第一时间看更新

1.5 用比特表示整数

让我们沿着逻辑链往上走,学习如何用比特来表示数字。数字比逻辑复杂,但比文字简单得多。

1.5.1 表示正数

我们常用十进制数字系统,因为它对应我们的十根手指。十个不同的称为数字的符号(0123456789)可以放进容器。容器从右到左依次堆放。每个容器都有一个与内容无关的名称,位于最右边的容器称为“一”,接下来是“十”,然后是“百”“千”,以此类推。这些名称是10的幂的别名,100是1,101是10,102是100,103是1 000。由于10是这些指数的底数,这个系统被称为十进制系统。数字的值等于每个容器的值与容器内的值的乘积之和。例如,数字5 028是5个1 000、0个100、2个10、8个1的总和,即5×103+0×102+2×101+8×100,如图1-5所示。

图1-5 数字5 028的十进制记数法

我们可以按照类似的方法用比特来表示数字。由于用的是比特而不是数字,因此只有两个符号0和1,但这不是问题。在十进制中,每当空间不够用时,就会增加一个容器。比如我们可以把9装在一个容器中,但装10需要两个容器。这也适用于二进制容器,任何大于1的数,都需要再来一个新的容器。最右边的容器仍然是1,但它的左边是什么容器?就是2!在十进制中,有10个符号,因此容器的值是右边那个容器的10倍。由于在二进制中只有两个符号,因此容器的值是右边那个容器值的两倍。这就是它的全部内容!容器的值都是2的幂,这意味着二进制容器是底数为2的系统,而不是底数为10的系统。

表1-1列出了一些2的幂,我们可以用它作为参考,来理解数字5 028的二进制表示法。

表1-1 2的幂

表1-1中最右边的每一个数字代表一个二进制容器的值。图1-6展示了数字5 028是如何用二进制表示的,其过程与前面的十进制记数法的表示过程基本相同。

图1-6 数字5 028的二进制形式

转成二进制的结果是:

1×212 + 0×211 + 0×210 + 1×29 + 1×28 + 1×27 + 0×26 + 1×25 + 0×24 + 0×23 + 1×22 + 0×21 + 0×20 = 5 028

正如你所看到的,二进制数5 028有1个4 096(212)、0个2 048(211)、0个1 024(210)、1个512(29)、1个256(28),以此类推,组成1001110100100。执行与十进制数相同的计算方法,可以写成1×212 + 0×211 + 0×210 + 1×29 + 1×28 + 1×27 + 0×26 + 1×25 + 0×24 + 0×23 + 1×22 + 0×21 + 0×20。将表1-1中的十进制数代入,得到4 096 + 512 + 256 + 128 + 32 + 4,等于5 028。

5 028是十进制的四位数。在二进制中,它是一个13位的数。

位数的多少决定了可以用十进制表示的值的范围。例如,在0~99的范围内,可以用两位数来表示。同样,比特数的多少也决定了可以用二进制表示的值的范围。例如,2比特可以表示0~3范围内的4个值。表1-2总结了可以用不同的比特数来表示的值的数量和范围。

表1-2 二进制数的范围

二进制数中最右边的位称为最低有效位,最左边的位称为最高有效位,因为改变最右边的位的值对数值影响最小,而改变最左边的位的值影响最大。计算机人喜欢用三个字母的缩写,也就是我们常说的TLA(Three-Letter Acronyms),最低有效位最高有效位的缩写通常为LSB(Least Significant Bit)和MSB(Most Significant Bit)。图1-7展示了用16位表示的数字5 028。

图1-7 最低有效位(LSB)和最高有效位(MSB)

你会发现,虽然5 028的二进制表示法需要13位,但图1-7显示的是16位。就像在十进制中在左侧添加前导零一样,我们总是可以使用比最低要求更多的容器。在十进制中,05 028与5 028的值相同。二进制数通常也是这样表示的,因为计算机是围绕着位块建立的。

1.5.2 二进制加法

了解了如何用二进制来表示数字,我们来看如何用二进制数进行简单的算术。在十进制加法中,我们按照从右边(最低有效位)到左边(最高有效位)的顺序把每个数字加起来,如果结果大于9,则进1位。类似地,我们按照从最低有效位至最高有效位的顺序把二进制数的每个位相加,如果结果大于1,则进1位。

在二进制中,加法实际上是比较简单的,因为2位的二进制数只有4个可能的数,而2位的十进制数有100个数字。例如,图1-8展示了如何用二进制数进行加法,把1和5相加,并显示每一列上的进位。

图1-8 二进制加法

数字1用二进制表示是001,数字5是101,因为(1×4)+(0×2)+(1×1)=5。把二进制数字001和101相加,我们从最右列的最低有效位开始。这一列的二进制数字1和1相加得2,而二进制中并无表示2的符号。但是我们知道十进制中的2实际上就是二进制中的10([1×2]+[0×1]=2),所以我们把0作为和,把1进到下一位。因为中间的位都是0,所以只有之前进位的1。然后,把最左列的数字相加:0加1就是二进制中的1。最后的结果就是二进制的110,或者说是十进制的6,也就是1和5相加后的结果。

你可能会注意到,二进制加法的规则可以用前面讨论过的逻辑运算来表示,如图1-9所示。我们将在第2章中看到,逻辑运算实际上就是计算机硬件做二进制加法的方式。

图1-9 使用逻辑运算的二进制加法

当我们把2个比特相加时,计算结果的值是2个比特的XOR运算结果,进位的值是2个比特的AND运算结果。从图1-9中可以看到,二进制中的1和1相加得到的结果是10。这意味着进位值为1,也就是执行表达式(1 AND 1)所得到的结果。同样,表达式(1 XOR 1)会得到0,也就是给相加位本身分配的值。

将2个比特相加是一个很少单独发生的操作。参照图1-8,我们似乎是把每列中的2个比特相加,但实际上是把3个比特相加,因为进位也需参与加法运算。幸运的是,我们无须学习新的规则就可以把3个比特加在一起(因为根据结合律,A+B+C等于(A+B)+C,所以可以用2次2个比特的加法把3个比特加在一起)。

当加法的结果超出位数时,会发生什么?这会导致溢出,只要从最高有效位进位,就会出现溢出。例如,如果将4位二进制数1001(910)与1000(810)相加,结果应该是10001(1710),但最终会是0001(110),因为没有最高有效位的位置。我们将在后面详细介绍计算机的条件码寄存器,它是存放奇数信息的地方。其中一个是溢出位,它保存着最高有效位的进位值。我们可以通过查看这个值来判断是否发生溢出。

你可能知道,可以通过与一个数的负数相加来减去该数。下一节将介绍如何表示负数。向最高有效位借位称为下溢。针对下溢,计算机也有对应的条件码。

1.5.3 表示负数

上一节中我们用二进制表示的所有数字都是正数,但现实世界中的很多问题都涉及正数和负数,我们来看如何用二进制来表示负数。例如,假设我们有4个比特可以使用。正如上一节提到的,4个比特可以表示0~15的16个数字。用4个比特可以表示16个数字并不意味着这些数字必须位于0~15。记住,语言是通过意义和语境来发挥作用的,这意味着我们可以设计出新的语境来解释比特。

原码表示法

符号常用来区分负数和正数。符号有两个值,即正号和负号,所以可以用比特来表示。我们将使用最左边的位(MSB)来表示符号,剩下的3个位可以表示0~7之间的数字。如果符号位为0,则将其视为正数。如果符号位为1,则将其视为负数。这样,我们总共可以表示15个而不是16个不同的正数和负数,因为正0和负0是相等的。表1-3展示了使用此方法表示的–7~+7之间的数字。

表1-3 二进制数的原码表示

这就是所谓的原码表示法,因为既有表示符号的位,也有表示大小(或者说这个值与零点的距离)的位。

原码表示法并不常用,原因有两个:第一,位的建立需要成本,所以我们不想因为用两种不同的方法表示0而增加成本,我们更愿意用这个位组合来表示另一个数字;第二,使用XOR和AND的运算不能用这种表示方法。

假设我们想把+1与–1相加。我们期望得到的结果是0,但使用原码表示法,我们却得不到该结果,如图1-10所示。

图1-10 使用原码表示法的+1与–1的相加

可以看到,0001在二进制中代表正数1,因为它的符号位是0;1001在二进制中代表–1,因为它的符号位是1。用XOR和AND运算把它们加在一起,就得到了1010。而1010表示十进制的–2,显然并非+1和–1之和。

虽然可以通过更复杂的逻辑来使原码表示法在运算中发挥作用,但尽可能地简化事情更有价值。我们需要探索一些其他的数字表示方法,以找到更好的方法。

反码表示法

另一种得到负数的方法是取正数并翻转它所有的位,这就是所谓的反码表示法。我们以类似于原码的方式对位进行划分。在这种情况下,用NOT运算得到一个反码。表1-4用反码表示了–7~7的数字。

表1-4 二进制数的反码表示

可以看到,翻转0111(+7)的每个位,可以得到1000(–7)。

反码表示法仍然存在有两个0的表示问题。它仍然不能让我们容易地进行加法运算。为了解决这个问题,我们使用循环进位,使最低有效位增加1,如果在最高有效位之外还有进位,那我们就可以得到正确的结果。图1-11说明了它的原理。

图1-11 反码表示的二进制数加法

要将用反码表示的+2与–1相加,可以按正常情况对0010和1110进行二进制加法。因为最高有效位(符号位)的数字相加结果是10,我们把0保留,将1作为循环进位。但是我们只有4个位,所以当到达最高有效位时,我们把进位值带回第一个位,得到0001,即+1,也就是+2和–1之和的正确值。可以看到,这样做过于复杂。

虽然这样做是可行的,但它仍然不是一个很好的解决方案,因为这需要额外的硬件来添加循环进位位。

现代计算机中既不使用原码表示法,也不使用反码表示法。没有额外的硬件,使用这些方法进行算术运算是行不通的,而额外的硬件会增加成本。我们来看看能不能想出一个解决这个问题的方案。

补码表示法

如果不添加任何特殊的硬件,只使用XOR和AND运算,会有什么结果?让我们想一想,当与+1相加的时候,什么位型会计算得到0,并称其为–1。如果我们坚持用4个比特表示,+1就是0001。如图1-12所示,1111与它相加,结果是0000,所以我们用这个位型来表示–1。

图1-12 寻找–1

这就是所谓的补码表示法,它是有符号整数最常用的二进制表示法。对正数求反码(即对每个位进行NOT运算),然后加1,舍弃MSB的任何进位,就可以得到这个数字的负数。表示+1的0001的反码是1110,加1就可以得到表示–1的1111。同理,+2是0010,它的反码是1101,再加1就可以得到表示–2的1110。表1-5展示了用补码表示的–8~7的数字。

表1-5 二进制数的补码表示

我们用0来试一下,看看补码表示法是否解决了0的重复表示问题。取0000并翻转每个位,我们得到它的反码1111。将1加到1111,得到[1]0000,但由于这是一个5位的数字,超过了我们可用的位数,所以我们可以忽略掉进位位中的1。这样就只剩下0000,也就是刚开始的0,所以0在补码表示法中只有一个表示形式。

程序员得知道需要多少位来表示他们需要的数字。对于这些内容,程序员们应烂熟于心。你可以参考表1-6,该表给出了不同位数下可以用补码表示的数字范围。

表1-6 补码表示的数字范围

从表1-6可以看出,随着位数的增加,可以表示的数字范围呈指数级增加。重要的是要记住,我们总是需要根据语境来确定我们看到的4位数字1111是15,而不是用补码表示的–1或用原码表示的–7,也不是用反码表示的–0。你必须知道使用的是哪种表示方式。