二进制好像懂了,又有地方很模糊,认识的不够清晰。
问题:求int的最大值
int的最大值为 $2^{31}-1$ 如果直接写:
int maxint=(1<<31)-1;
好像是不对的,因为既然减1就是最大值了,那么 $2^{31}$ 肯定溢出int最大值了。
于是就拆了一下,变为:
int maxint=(1<<30)-1+(1<<30);
全程没有溢出,解决问题,得到答案 $2147483647$,惊喜!这还是一个梅森素数
但是,还有更大的惊喜,方法1:int maxint=(1<<31)-1;
也能得到正确答案:$2147483647$
这是巧合还是某种隐藏的机制在起作用?
补码的加减法
于是,我从补码的运算机制开始研究。
补码:正数的补码 = 原码,负数的补码 = 原码按位求反加1。
为什么这么设计负数的补码?
答:为了保证相反数相加等于0。
补码的加法运算
两个机器数相加的补码可以先通过分别对两个机器数求补码,然后再相加得到,在采用补码形式表示时,进行加法运算可以把符号位和数值位一起进行运算(若符号位有进位,导致了益出,则直接舍弃),结果为两数之和的补码形式。
示例1:求两个十进制数的和 $35+18$。
首先,规定字长是8位,也就是只能用8位二进制表示。
$35$ 的原码:$00100011$
$18$ 的原码:$00010010$
因为 $35$ 和 $18$ 都是正数,所以补码和原码完全一致。
$35$ 的补码:$00100011$
$18$ 的补码:$00010010$
因为补码是可以连同符号位一起运算,所以运算法则等同于无符号二进制运算:
再强调一遍:最高的符号位是参与运算的。
00100011---35二进制表示
00010010---18二进制表示
00110101-----转换成10进制是53。结果正确!
如果有负数参与,也就是减法了。
同样还是当作加法来做。
示例2:$35-18$ 可以变成 $35+(-18)$
同示例1一样,只能用8位表示。
35的原码:00100011。
-18的原码:10010010。
因为35是正数,补码与原码完全一致,但是-18是负数,补码需要转换。
35的补码:00100011。
-18的补码:由原码除符号位外取反,再在最低位+1,得到结果是11101110。这时都是补码,运算规则等同于无符号二进制加法。
00100011
11101110
100010001---因为前面规定了字长是8位,这里出现了9位,
所以最高位舍弃,舍弃后,结果为00010001,转换成十进制是:17。
结果正确!(超出字长部分直接舍弃)
为什么 (1<<31)-1
明明(1<<31)
溢出了,却得到正确答案?
答:根据上面的解读,模拟一下运算过程就知道了。由于 $2^{31}$ 太长,用8位字长来模拟,也就是 $2^7$
首先看:$2^7 = 10000000$,$-1 = 11111111$,则:
10000000
+ 11111111
---------------
101111111 (最左边1,因为超过8位,抛弃)
01111111(正好是int的最大值)
看到了吗?再二进制补码的运算机制之下,虽然中间过程 $2^{31}$ 超出int,但最终仍能保证得到正确的最大值。
一开始我会想,靠这种有点投机的手段靠谱吗?后来看了点linxu的内核代码,里面好多地方都在运用这种机制带来的巧合取加快运算,后来想想二进制的补码运算法则还会变吗?放心大胆的用吧。
unsigned long long类型和模 $2^{64}$
最近做题的时候,经常遇到范围是2^63,取模2^64的这种题目。遇到这种限制条件时就要想到用unsigned long long类型。
可以简洁地声明为typedef unsigned long long ULL;
这样,如果 ULL 类型的整数溢出了 ,就相当于取模 $2^{64} $了。 因为 ULL 的范围是 $[0,2^{64}-1]$。
有一道例题: 字符串哈希 用到了模 $2^{64}$
不是常识吗