0.0、关于整数除法
在了解完整数乘法后,我们知道了整数乘法的本质是乘数个被乘数相加。同理除法的本质是被除数可以减去多少个除数。
在计算机整数数据类型中,可以将乘数逻辑左移来得到乘以2的幂的结果。也可以用将被乘数分别逻辑左移与乘数相对应的多个2的幂再相加,得到乘以任意常数的结果。
与乘法类似,除法可以将被除数算数右移来得到除以2的幂的结果。但是却无法可乘法一样使用移位的方法来得到除以任意常数的结果。
也许是因为除以任意常数在业界还没有定论,也许其太过复杂而篇幅有限无法展开来讲,又也许会在后面的内容再详细讲解。书中讲到除以2的幂后就戛然而止,未对整数再进行更深入的讲解。介于全书内容还很多,我也不再深挖了。今年是蛇年,再深挖恐怕真的要挖到猴年马月去了。
2.3.7、除以2的幂
整数除法的核心在与舍入到零(Rounds toward zero),在日常生活中更加常见的翻译为向零舍入。所谓向零舍入,就是为正数时向下舍入,为负数时向上舍入。“舍入”就是四舍五入的“舍入”,但是向零舍入并不是四舍五入,而是无论小数是几都舍弃小数。比如:3.14向零舍入为3;-3.14向零舍入为-3;对与我们人类来说这似乎是理所应当,但是对于计算机开来说却不是这样。对于计算机来说,向上舍入和向下舍入需要用到不同的方法。
先从相对简单的无符号除法来看,无符号除法没有负数部分所以也就没有向上舍入。我们先来通过几个例子来看看被除数12340逻辑右移k位与除以2^k向上舍入的结果是否相同。如下表
x | k | >>k(二进制) | 十进制 | 12340/2^k |
12340 | 0 | 0011000000110100 | 12345 | 12340.0 |
1 | 0001100000011010 | 6170 | 6170.0 | |
4 | 0000001100000011 | 771 | 771.25 | |
8 | 0000000000110000 | 48 | 48.203125 |
从例子中可以看出x>>k与x/2^k有着相同的结果,但是我们不能以偏概全,需要严谨的数学推导来证明这个关系是完全可行的。
假设x'=x>>k;x''=x & 2^k - 1;
则有x =(2^k)x' + x'';
因为 0 <= x'' < 2^k,为余数,将被舍弃;
所以有x/2^k 向下舍入为 x'。
而在补码除法中需要用到算数移位,这在以前补码的数据类型大小扩展中详细讨论过。在正整数部分与无符号整数相同可直接使用x>>k对x/2^k进行计算,而在负整数部分却有不同,因其任然会啊使商向下舍入。如下表
x | k | >>k(二进制) | 十进制 | 12340/2^k |
-12340 | 0 | 1100111111001100 | -12345 | 12340.0 |
1 | 1110011111100110 | -6170 | 6170.0 | |
4 | 1111110011111100 | -772 | -771.25 | |
8 | 1111111111001111 | -49 | -48.203125 |
可以看出在可以整除,即不需要舍入的时候结果是符合预期的,但是当需要舍入时,其结果依旧是向下摄入。那么从数学的角度来看是否证明这是普遍现象呢?
假设x'=x>>k;x''=x & 2^k - 1;
则有x =(2^k)x' + x'';
因为 0 <= x'' < 2^k,为余数,将被舍弃;
所以有x/2^k 向下舍入为 x'。
可以看得出来与无符号整数的逻辑没有什么区别,区别主要体现在位表示上。
所以在做补码的负数除法计算时不能只使用单纯的算术右移,还需要在移位之前对x进行偏置,其公式为(x+(1<<k)-1)>>k。这个公式所表达的含义是,如果有余数,则结果加1。可能不太好理解,我们来拆开来看,(1<<k)-1表示除数-1,就得到了一个k-1位到0位全是1的向量,这也就代表着,x的k-1位到0位只要不是0,就会发生进位。而x的k-1位到0位只要不是0就意味着有余数需要舍入。而在x的k-1位到0位为0时,意味着没有余数不需要舍入。示例如下
x | k | >>k(二进制) | 十进制 | 12340/2^k |
-12340 | 0 | 1100111111001100 | -12345 | 12340.0 |
1 | 1110011111100110 | -6170 | 6170.0 | |
4 | 1111110011111101 | -771 | -771.25 | |
8 | 1111111111010000 | -48 | -48.203125 |
虽然理论上是如此,但是数学角度证明是必不可少的。
假设 x = qy + r;
则有 (x + y - 1) / y = (qy + r + y -1)/y = q+((r + y -1)/y);
如果 r = 0;
则 r + y - 1 < y;
(r + y -1) / y = 0;
(x + y - 1) / y = q;
如果 r > 0;
则 r + y - 1 >= 0;
(r + y -1)/y = 1(向下舍入);
(x + y - 1) / y = q + 1;
可以看出其最终还是向下舍入,只是在负数结果要向下舍入前先为被除数上一个除数-1,使得最终结果为向上舍入。
用C语言来完整的表达补码的除法为
(x < 0 ? (x + (1 << k) - 1) : x) >> k;
没错前辈们的智慧有震惊了我。
2.3.8、关于整数运算的最后思考
可以看出因为整数运算特性导致了诸多限制,也许某些诡异的程序异常的答案就藏在这写特性之中。过去的我们也许只能一层一层的加判断来规避掉这些程序异常。但我们正在学习这些异常的本质,期望彻底结束这场躲猫猫。揪出那个藏在暗处的虫子,中指一弹,说声走你~加油吧少年你将有光明的未来。