现在来看计算机是怎样存储整数的。
首先,通过c语言来看看c中的各种整数类型的大小。
其实通过sizeof函数我们已经了解到了,比如,sizeof(char) = 1表示使用一个字节来存储char类型,而1byte = 8bit
所以,28 = 256 ,所以我们可以表示256个数字,如果从0开始(就是只表示正整数)则可以表示 0~28-1 = 255 的数字;
如果我们考虑到表示负数呢?那么我们拿出一个位来表示符号位,于是取值范围变成-27 ~27-1即 -128~127
当然,如果只是需要表示正整数,那么我们这样定义:
当然,int,short,long些整数定义,和char也是同样的道理。
不同的机器可能有不同的位来表示这些定义,但是一旦确定位数,那么这些数字的取值范围也一样确定。
以典型32位机器为例,我们看下表:
注意: c中可以定义无符号数,但是java中都是有符号数。
无符号整数表示
假设一个数有w位来表示,我们可以看成一个向量,表示为{xw-1 ,xw-2 ,...,X0}
我们用一个函数 B2Uw来表示二进制数到无符号数(十进制)的转换,长度为w,于是有公式:
注意中间的不是等号,而是表示左手边被右手边定义为。
这样,我们把向量通过函数映射到数上面: B2Uw:{0,1}w --> {0,...,2w-1}
注意 B2Uw是一个双射(就是既是满射,又是单射,一对一,而且没有漏掉,呵呵),这样我们就可以找到一个U2Bw实现逆函数。
当向量中的每一位为1时,表示UMaxw=2w-1
当向量中的每一位为0时,表示UMinw=0
整数的二进制补码编码(有符号)
当我们要表示有符号数时,我们可以使用一个位(最高位)来表示符号位(negative weight)负权,这就是大多数计算机使用二进制补码表示法。我们使用B2Tw(T=two's-complement)来表示,则有公式:
同样B2Tw也是双射。
他能表示的最大值为 : [01,...1] ,TMaxw=2w-1-1
他能表示的最小值为: [10,...0], TMinw= -2w-1
强制转换
在c语言中,我们经常看到这样的代码:
也就是从有符号数到无符号数的转换。所以,我们接下来要推导出无符号数和而进制补码表示的有符号数之间的转化。
由于 B2Uw 和 B2Tw都是双射,于是我们可以定义它们的逆函数,也就是 U2Bw和T2Bw,于是我们有:
同理:
这里,我们要注意的是,这些转换对于数据的位表示来说并没有改变,改变的只是解释的方法不同而已。
下面,我们将推导出准确的公式来。首先根据最早的两个公司,也就是无符号数,二进制补码表示的公式,用两个公式相减得到:
由于在二进制补码中,w-1位决定了正负,并且如果为正则xw-1 = 0,如果为负,则 Xw-1 = 1
于是推出公式:
也就是说当x大于等于0的时候,无符号数和二进制补码表示的数都一样。
下面我们来看看 U2Tw的堆导,可以使用相同的方法。
一个无符号数转换到一个而进制补码表示的有符号数后, w-1是关键位,如果w-1是1,那么这个数将大于 2w-1,这样的话,
二进制补码表示不了这个数,根据公式就会 减去 2w,成为负数部分。所以:
总结
1、无符号数和二进制补码表示的数之间的转换,其实对于位表示来说,是不变的,只是解释的改变。
2、对于范围[0,2w-1)区间的数,T2Uw(x) = x = U2Tw(x)
3、对于2描述的范围外的值,要么加上或者减去2w来完成转换。比如T2Uw(-1) = -1 + 2w = UMaxw
再比如,我们有 T2Uw(TMinw) = -2w-1 + 2w = TMaxw + 1
c语言中隐式转换:
如果一个表达式中既有无符号数,又有有符号数,那么,C 会把有符号数强制转换成无符号数,并假设两个数都非负来运算。
我们看一下: -1 < 0U 这个表达式是否为真呢 ?
这里有个有趣的现象,就是 例如在32位机上,要表达TMin,我们可能认为是 -2147483648
但是,这样做是有问题的,因为编译器处理这个数的时候,例如 -X,她会先取X,然后取反,而 2147483648太大了,所以,我们只能把
TMin_32写为 -2147483647 - 1
扩展一个数字的位表示
在不同长度的整数之间运算,我们可能用到扩展一个数字的位表示。
两种扩展方法:
1、对于无符号数,0扩展,就是左边高位补零
2、对于有符号数,我们要在扩张后保证符号,所以需要符号位扩展。
对于第一种,左边高位补零,则对于整个数来说,没有变化。
但是对于第二种,我们如何保证扩展以后是否还是原来的数呢 ?
我们要证明扩展k位以后,得到的数还是原来的值,那么我们只需证明,扩展1位以后不变,那么扩展k位也不变。
所以我们要证明:
这个证明很容易,展开后即可。大家自己想想。
提示: -2w + 2w-1 = -2w-1
根据提示,可知,我们扩展的符号位,其实等于加上一个 -2w 和 转换一个 -2w-1 为 +2w-1
注意,c语言中的位移对于无符号数是执行零扩展,对于有符号数,则是符号扩展。
截断数字
对于从长整数转换到短整数,我们可能会截断表示位。一般丢弃高位。
例如一个 长度为 w位的数字截断到k位,对于无符号数,这就相当于 x mod 2k
让我们来看看这个推导:
这个推导运用了属性:
于是我们得出结论:
符号截断可能产生溢出。
对于无符号数,推荐当这个数本身没有数字意义的时候才能使用。
看下面的例子:
乍一看,代码没有什么问题,可是如果length == 0时,问题就出来了。
整数运算
整数是可以加减乘除的,但是,计算机所表示的整数的加减乘除和我们在纸上运算的可能有些差别。
因为计算机的表示是有限的,所以,在这些运算中,就有溢出的考虑。
我们首先来看无符号数的加法运算:
假设x ,y都是无符号整数。
首先无符号数的表示范围: 0~2w-1,所以,如果 假设x + y <= 2w-1,那么我们的加法不会溢出。
如果x + y > 2w-1,那么此时我们需要w+1位来表示,这里计算机就自动截去最高位。这样相当于整个数减去2w
还有一点,就是x + y后的数的位不能大于w+1,这样的话就要再减去2w+n了……
根据分析,我们可以得出:
在c语言中,如果发生溢出,是没有相应的异常提示的,如何判断溢出呢?
通过公式我们可以看出,当且仅当 x < s 或者 y < s的时候,发生溢出。大家可以考虑为什么。
根据无符号数加法运算,我们可以看到,我们的加法所得的结果不可能大于 2w,这是一个模数加法,构成一个数学结构“阿贝尔群”。当我们要研究减法的时候,我们可以通过这个特性把减去变成加一个“负数”。这里的负并非是我们说的负数,因为这无符号数,是没有负数的。
下面我们看一下稍微复杂一点的二进制补码的加法。
首先,从位级上考虑,二进制补码的加法和无符号数完全一样。
于是我们有二进制补码的加法定义:
根据公式,我们可以求得:
由于 x ,y的取值范围是 [-2w-1,2w-1-1],所以,x + y的取值范围是 [-2w,2w-2]
我们可以从可能的取值区间来分析上式。注意公式中的 (x+y) mod 2w 是当做无符号数处理(使用T2U)的(位模式相同)
1、 当-2w <= x + y <-2w-1 时,根据T2U公式和模加公式,(x+y) mod 2w = x+ y + 2w,于是得出 0 <= (x+y) mod 2w <2w-1+2w=2w-1。根据U2T公式,我们可以得出 此范围的U2Tw(x) = x,所以,我们得到:
这种情况属于负溢出,两个负数相加超出了二进制补码表示所能表示的范围。
2、当-2w-1<= x + y<0时,根据T2U公式和模加公式,(x+y)mod2w = x + y + 2w,又可以得出:
2w-1 < =(x+y)mod2w <2w。根据U2T公式,我们可以得出在此范围的U2Tw(x) = x - 2w,于是我们可以得出:
对于这种情况,是没有溢出的。
3、当0<= x + y < 2w-1 时,根据模加公式,(x+y)mod2w = x + y,所以 0 <= (x+y)mod2w <2w-1。根据U2T公式,我们可知
此范围的U2Tw(x) = x,于是我们得出:
4、当2w-1 <= x + y <2w时,根据模加公式,(x+y)mod2w = x + y ,所以 2w-1 <= (x+y)mod2w <2w。根据U2T公式我们可知,
此范围内的U2Tw(x) = x - 2w,于是我们得出:
这种情况我们称之为正溢出。
综合以上四种情况,对于 -2w-1 <= x, y<=2w-1 -1的 x,和 y的二进制补码加法运算,我们可以给出公式:
关于而进制补码的加法逆元,实际上就是求二进制补码的非
因为对于有符号数来说 , x + (-x) = 0
考虑 x 的范围 ,[-2w-1,2w-1),当 x = -2w-1的时候,-x = 2w-1,注意到 2w-1是不能表示为一个w位的有符号数的。
所以,当x = -2w-1时:
第三种情况,负溢出。由此可知,当x=-2w-1的时候,他的逆元就是自身。
综合:
对于二进制补码的非,我们一般都通过取反 加1获得,这又是为何呢?
我们根据B2T公式推导一下便知:
这下大家都明白了。
整数的乘法
首先来看无符号数的乘法。我们假设两个无符号数 x, y 的是在 [0,2w-1]中的,但是他们的乘积 x.y的范围将是 [0,(2w-1)2]
这个范围可能需要2w位来表示,C中的无符号数乘法的定义是产生2w位的整数乘积的低w位,也就是抛弃了高于w的高位。
根据截断公式:
我们可知 :
再来看看二进制补码的乘法。首先,对于位级别的运算,无符号数和二进制补码都是一样的。所以我们可以推出公式:
由于乘法指令相当的慢,所以,有些时候我们可能使用其他方法来代替乘法。看下面的例子:
设 x 为位模式 [xw-1,xw-2,...0]表示的无符号数,当我们把x左移k位时,右边补零。得到 x`=[xw-1,xw-2,...0,...0]
根据公式可以看到:
把无符号数x左移k位,相当于乘以2k
于是,我们在处理乘法的时候,可能遇到诸如:a是一个无符号数, a * 3 = a<<1 + a
这对于有符号数,二进制补码的表现是一样的。因为左移并未涉及到符号位。
那让我们看看 除以2的幂会怎么样呢 ?
整数除法总是舍入到0。我们分两种情况分析:
1、对于x>=0,y>0,结果定义为 ,这里对于任何的实数a,定义为一个唯一的整数 a·,有 a`<=a<a`+1
2、对于x<0 和 y > 0 ,结果需要定义为 ,这里对于任何的实数a,定义为一个唯一的整数a`,有 a`-1 < a< =a`
所以对于无符号数和有符号数中的正整数来说,结果都是一样的。舍入方式一样,得到的结果是 正整数或者无符号数的算术右移,等价于
把它除以2k 。但是对于负数的情况,由于舍入的方向不同,产生了微妙的变化。例如 -5/2。-5表示为[1011],算术右移一位后得到[1101]这是-3的二进制补码表示。但是-5/2 应该等于-2。这该如何是好呢 ?
解决方法是,我们在位移之前,通过偏置(biasing)这个数,修正这种舍入。我们通过属性:
我们的偏置取 2k-1
于是 -5 + 21 -1 = -4, [1010], -4/2 = -2
在c语言中,我们用表达式来 除以 2k的运算:
至此,整数的表示和运算都说完了。大家多找联系做做就能熟悉了。