编程与类型系统
上QQ阅读APP看书,第一时间看更新

2.3.1 整数类型和溢出

无符号二进制编码使用每个位来表示值的一部分。例如,一个4位无符号整数可以表示0~15之间的任意值。一般来说,一个N位无符号整数可以表示0(所有位都是0)~ 2N–1(所有位都是1)之间的值。图2.2显示了4位无符号整数的一些取值。使用下面的公式,可以将N个二进制位(bN–1bN–2b1b0)转换为一个十进制数字:bN–1×2N–1 + bN–2×2N–2 + … + b1×21 + b0×20

图2.2 4位无符号整数编码。最小取值为0,即全部4个位均为0。最大取值为15,即全部位均为1(1×23 + 1×22 + 1×21 + 1×20

这种编码非常直观,但只能表示正数。如果我们还想表示负数,就需要一种不同的编码,通常是补码。在补码编码中,我们保留一位作为符号位。正数的表示与前面一样,但负数编码则是从2N减去它们的绝对值,其中N是位数。

图2.3显示了4位带符号整数的一些取值。

图2.3 4位带符号整数编码。–8被编码为24 – 8(二进制为1000),–3被编码为24 – 3(二进制为1101)。对于负数,第一位总是1,对于正数,第一位总是0

在这种编码中,所有负数的第一位都是1,所有正数和0的第一位都是0。一个4位带符号整数可表示–8~7之间的值。用来表示值的位数越多,能够表示的值的范围越大。

上溢和下溢

如果算术运算的结果不能用给定位数表示,会发生什么?如果我们使用4位无符号编码来计算10 + 10,但4个位能够表示的最大值为15,此时会发生什么?

这种情形叫作算术上溢。还有一种相反的情形,即得到的数字太小,无法用给定位数表示,这种情形叫作算术下溢。不同的语言采用不同的方式来处理这两种情形,如图2.4所示。

图2.4 处理算术上溢的不同方式。里程表从999 999环绕到0;仪表盘停留在最大值位置;计算器输出Error并停止计算

处理算术上溢和下溢的3种主要方式是环绕、饱和与报错。

硬件通常采用环绕方式,简单地丢弃不合适的位。对于4位无符号整数1111,如果我们向其加1,结果将变成10000,但是因为只需要使用4个位,所以1被丢弃,得到的结果为0000,即环绕回0。这是处理溢出最高效的方式,但也是最危险的方式,可能导致意外的结果。例如,我有15美元,再加1美元,结果我只有0美元。

饱和是处理溢出的另外一种方式。如果运算结果超出了可以表示的最大值,就停止在最大值。这与物理世界非常对应:如果恒温器最高只能达到某个温度,那么尝试继续升高温度不会有效果。另外,使用饱和时,算术运算不再始终具有结合性。如果最大值是7,那么7 + (2 – 2) = 7 + 0 = 7,但是(7 + 2) – 2 = 7 – 2 = 5。

第三种方式是报错,即在发生上溢时抛出错误。这是最安全的方法,但缺点是需要检查每个算术运算,而且每当执行算术运算时,代码都需要处理异常情况。

检测上溢和下溢

不同的语言可能使用上述不同方式来处理算术上溢和下溢。如果场景中要求采用的处理方式与语言的默认方式不同,则需要检查某个操作可能导致上溢还是下溢,然后单独处理该场景。这需要在允许值的范围内完成处理。

例如,为了确保将值ab相加后,结果不会上溢或者下溢出[MIN, MAX]范围,我们需要确保不会发生a + b < MIN(两个负数相加时)或者a + b > MAX

如果b是正数,则不可能出现a + b < MIN,因为我们在让a变得更大,而不是更小。在这种情况中,我们只需要检查上溢。我们可以把a + b > MAX改写为a > MAX – b(在两边均减去b)。因为我们在减去一个正数,所以是在减小值,此时不会发生上溢(MAX – b[MIN, MAX]范围内)。因此,如果b > 0,并且a > MAX – b,就会发生上溢。

如果b是负数,那么不可能出现a + b > MAX,因为我们在让a变得更小,而不是更大。在这种情况中,我们只需要检查下溢。我们可以把a + b < MIN改写为a < MIN – b(在两边均减去b)。因为我们在减去一个负数,所以是在增大值,此时不会发生下溢(MIN – b[MIN, MAX]范围内)。因此,如果b < 0,并且a < MIN – b,就会发生下溢,如程序清单2.8所示。

程序清单2.8 检查加法溢出

对于减法,可以使用类似的逻辑。

对于乘法,我们通过在两侧均除以b来检查上溢和下溢。在这里,我们需要考虑两个数字的符号,因为将两个负数相乘会得到一个正数,而将一个正数和一个负数相乘会得到一个负数。

满足以下条件时,将发生上溢:

b > 0a > 0,并且a > MAX / b

b < 0a < 0,并且 < MAX / b

满足以下条件时,将发生下溢:

b > 0a < 0,并且a < MIN / b

b < 0a > 0,并且a > MIN / b

对于整数除法,a / b的值始终是–a~a之间的一个整数。只有当[-a, a]不完全在[MIN, MAX]之间时,我们才需要检查上溢和下溢。回到4位带符号整数的例子,MIN为–8,MAX为7,所以只有一种情况会发生上溢:–8 / –1(因为[–8, 8]不完全在[–8, 7]范围内)。事实上,对于带符号整数,唯一会出现上溢的场景是当a为可表示的最小值,b为–1的时候。无符号整数除法不会出现上溢。

表2.1和表2.2总结了在需要特殊处理时,检查上溢和下溢的步骤。

表2.1 检查ab在[MIN, MAX]范围内发生整数上溢的情况,其中MIN = -MAX-1

表2.2 检查ab在[MIN, MAX]范围内发生整数下溢的情况,其中MIN = -MAX-1