本文并没有系统性的介绍计算机中信息的表示方式,只介绍了一些用户编程时需要注意的的小问题。
1、为什么有符号整数负数可表示范围比正数多一个?
有符号整数是用补码表示的。具体的表示形式如下,其中,向量的每个元素表示了整数的每一位。
负数的最高位是1,非负数最高位是0。
假设有符号整数共w位,那么最小的负数最高位为1,其他为0,即
−
2
w
-2^w
−2w。 最大的正数最高位为0,其他位为1,即
2
w
−
1
2^w-1
2w−1。
因此有符号整数可表示的范围为
[
−
2
w
,
2
w
−
1
]
[-2^w, 2^w-1]
[−2w,2w−1]。负数和非负数各占了取值范围的一半,而非负数中有一个数是0,这也就导致了负数和正数不对称。
2、C语言中有符号整数如何转换成无符号整数?
如果将一个有符号的整数强制类型转换成一个无符号的整数时,结果保持每个位的值不变,但改变解释这些位的方式。有符号整数是用补码方式来解释,而无符号整数则是最高位跟其他位作相同的运算。
比如,一个整数 -12345,用补码表示为 1100111111000111,而强制转换为无符号整数后,其表示依然为 1100111111000111,但解释方式变了,最后得到的结果是 53191。
3、扩展整数的位
示例代码:
short sx = -12345;
unsigned short usx = sx; //53191
int x = sx; //-12345
unsigned ux = usx; //53191
首先,将有符号short类型的数sx转换为无符号short,则是通过位保持不变,将补码解释改为无符号的方式,得到结果 53191。
其次,将无符号short转换为更宽的类型有符号的int,则是通过执行符号扩展,在表示中添加最高有效位的值的方式,这里最高有效位为1,那么将多扩展的16位高位都置为1,得到结果 -12345。
然后,将无符号short类型转换为无符号int类型,则是通过零扩展的方式,在高16位添加0,得到结果 53191。
4、截断整数的位
示例代码:
int x = 53191;
short sx = (short) x; //-12345
int y = sx; //-12345
首先,将有符号int类型转换为有符号short类型,将32位截断为16位,也就是将高16位的0全部去掉,剩余的最高有效位为1,得到结果 -12345。
然后,我们又将sx强制转换为有符号int类型,采用符号扩展的方式,高16位全部置1,得到结果 -12345。
然而,实际上除了C语言外,很少有语言支持无符号整数。java只支持有符号整数,并且用补码表示。java中右移运算符“>>”被定义为执行算术右移,特殊运算符“>>>”被指定为执行逻辑右移。算术右移指在左端补k个最高有效位的值,逻辑右移指在左端补k个0。
5、整数运算中的溢出行为
①无符号数加法
任意两个处于 0 到
(
2
w
−
1
)
(2^w -1)
(2w−1)之间的无符号数 x 和 y,如果相加,可能会由于“字长膨胀”而导致溢出。溢出时,第w位(最低位为第0位,最高位为第w-1位)会被截断,那么最后显示的结果=原结果
−
2
w
-2^w
−2w。
②补码加法
与无符号数加法一样,当补码加法由于字长膨胀而导致溢出时,最高位会被截断,因此也就有可能出现两个正整数相加得到负数的情况。
③无符号乘法
任意两个处于 0 到 ( 2 w − 1 ) (2^w -1) (2w−1) 之间的无符号整数 x 和 y相乘,可能会需要2w位来表示。溢出时,高w位会被截断,因此得到的结果是原结果模 2 w 2^w 2w。
④补码乘法
补码乘法在出现溢出时,会先将该值模 2 w 2^w 2w,再把无符号数转为补码。补码乘法和无符号乘法的位级表示相同,只解释不同。
⑤乘以常数
由于大多数机器上执行整数乘法指令非常慢,所以编译器在优化乘法操作时,通常会尝试使用移位和加法运算的组合来代替乘以成熟因子的乘法。比如:
- x乘以2的k次方——>x左移k位。
- x乘以14———>利用 14 = 2 3 + 2 2 + 2 1 14 = 2^3 + 2^2 + 2^1 14=23+22+21,编译器将乘法重写为 (x<<3)+(x<<2)+(x<<1)。
⑥除以2的幂
除法是比乘法更慢的操作,除以2的幂通常使用右移操作替代。无符号数采用逻辑右移,补码数采用算术右移。
整数除法总是舍入到零。并作以下规定,
- 当x>=0, y>0,x/y得到的结果是该值向下取整。
- 当x<0, y>0,x/y得到的结果是该值向上取整。
1)除以2的幂的无符号除法,由于采用的逻辑右移,所以比较简单。得到的结果满足上述规则。
2)但是除以2的幂的补码除法中,如果x>=0,则其算术右移的效果与逻辑右移一致。但是如果x<0,做除法时,会导致得到的结果向下取整。比如:-12340的二进制表示为1100111111001100,算术右移4位后,得到的二进制表示为1111110011111100,转换成十进制为-772,而实际上-12340/16=-771.25。
所以,我们需要做一些调整,来修正这个不合适的行为。在移位之前“偏置”这个值,即,x右移k位,执行表达式**(x+(1<<k)-1)>>k**,就可以得到期待的值。原理是:对于整数x和y (y>0),(x/y)向上取整=[(x+y-1)/y]向下取整。
6、0.1+0.2不等于0.3
如果想要把一个十进制的小数,比如0.1,转换成二进制,会出现无限循环的情况。
将一个十进制小数转换成二进制,通常的做法是,将十进制小数乘2,得到的积取整数部分,再用余下的小数部分再乘2,再将积的整数部分取出……直到积中的小数部分为0。比如,十进制的0.625,
0.625 * 2 = 1.25 整数部分为 1
0.25 * 2 = 0.5 整数部分为 0
0.5 * 2 = 1 整数部分为1
对应的二进制是0.101。
但是十进制的0.625是一种特殊的情况,其可以转换成有限位的二进制数。然而对于大部分小数来说,转换时会出现无限循环的情况。比如,十进制的0.1对应的二进制数为 0.000110011001100……。
因此,计算机无法精确地用二进制方式表示0.1。
IEEE 754 提出使用有限位的浮点数近似表示小数。C语言中单精度的浮点数 float 共32位,符号位s占1位,阶码e占8位,小数字段m占23位。双精度的浮点数 double 共64位,符号位占1位,阶码占11位,小数字段占52位。表示方式为 n = ( − 1 ) s ∗ m ∗ 2 e n=(-1)^s * m * 2^e n=(−1)s∗m∗2e。
十进制小数0.1,对应的双精度浮点数的二进制表示为0.00011001100110011001100110011001100110011001100110011001,
十进制小数0.2,对应的双精度浮点数的二进制表示为0.00110011001100110011001100110011001100110011001100110011,
0.1+0.2所得结果的双精度浮点数的二进制表示为0.01001100110011001100110011001100110011001100110011001100,对应的十进制数为0.30000000000000004。
如果有高精度运算需求,不要使用float或double进行运算,尤其是涉及资金方面的。那么,如何解决这样的需求呢?
java中提供了BigDecimal来进行精确运算。
7、浮点数强制转换
已知,单精度的浮点数float共32位,符号位占1位,阶码占8位,小数字段占23位。双精度的浮点数double共64位,符号位占1位,阶码占11位,小数字段占52位。
- 从int转换为float,数字不会溢出,但是会被舍入,因为float的小数字段只有23位的精度。
- 从int或float转换为double,由于double的小数字段占52位,精度很高,所以能够保留精确的数值。
- 从double转换为float,不仅精度可能缩小,会被舍入,而且还会出现溢出成+∞或-∞。
- 从float或者double转换为int,值会向0舍入。
参考资料:《深入理解计算机系统》