二进制那些事
理清字符集和字符编码关系中介绍到计算机内部由集成电路决定了计算机的信息只能用二进制数处理。本期将介绍二进制那些事。
移位运算
移位运算指的是将二进制数值的各数位进行左右移位的运算。左移空出来的低位要进行补0操作,右移空出来的高位要进行怎样的操作,我们会在后面说明。
我们发现,左移两位相当于对39乘以4,右移两位相当于除4,也就是说计算机用移位算法来表示数据的乘除运算。
补数
刚才之所有没有介绍相关右移的内容,是因为用来填充右移后空出来的高位的数值,有 0 和 1 两种形式。要想区分什么时候补0什么时候补1,只要掌握了用二进制数表示负数的方法即可。
二进制数中表示负数时,一般会把最高位作为符号来使用,也就是说,最高位是符号位。正数的符号位用0表示,负数的符号位用1表示。举个栗子,1的二进制数是0000 0001 ,那么,-1的二进制数是多少呢?难道是1000 0001,1000 0001+0000 0001 结果不是0,说明这个结果是错的。为此,在表示负数时就需要使用补数。补数就是用正数表示负数,通过将二进制数的各位数值全部取反,然后再将结果加1得到补数。-1的补数是1111 1111。同理,1111 1110表示的负数是多少呢?这时我们可以利用负负得正这个性质。假设1111 1110是负xx,那么1111 1110的补数是正xx。1111 1110的补数是0000 0010,因此1111 1110表示-2。
逻辑右移和算术右移
在介绍完补数后,让我们返回到右移这个话题,右移之后在最高位有补0和补1两种情况。当二进制数的值表示图形模式而非数值时,移位后在最高位补0,这是逻辑右移。将二进制数值作为带符号的数值进行运算时,移位后要在最高位填充前符号位的值( 0 或 1 ),这是算术右移。
现在我们来看一个右移的例子。将-8(1111 1000)右移两位。这时,逻辑右移的情况下结果会是 0011 1110,也就是十进制数62,显然不是-2,而在算术右移的情况下,结果会变成1111 1110 ,用补数表示就是-2,和真实结果相同。需要注意的是只有在右移时才区分逻辑移位和算术移位。
二进制数表示小数
通过上述介绍,我们对整数的二进制表示方式做了说明。由于计算机内部所有信息都是以二进制数的形式来处理,因此在这一点上,整数和小数并无差别。不过,使用二进制数表示整数和小数的方法却有很大的不同。
由于二进制数表示的小数的数值范围是有限的,并不能表示所有的十进制小数。例如:小数点后3位用二进制数表示时的数值范围为0.000~0.111,但是只能表示有限的十进制小数,如下图所示。
image.png
为了加深大家印象,举一个更加实际的栗子:将0.1累加100次最终结果是10.000002,不是10。
public class TestBinary {
public static void main(String[] args) {
float sum=0.0f;
for (int i = 0; i < 100; i++) {
sum += 0.1;
}
System.out.println(sum);
}
}
浮点数
现在,我们应该知道仅仅依靠纸面上的二进制数是表示不了全部小数。那么,计算机实际上是以什么样的表现形式来处理小数的呢?
目前,计算机提供了单精度浮点数和双精度浮点数来表示小数形式。单精度浮点数用32位表示全体小数,而双精度浮点数用64位表示。它们都是由符号、尾数和指数组成。
接下来,让我们一起看一下如果将0.1用单精度浮点数来表示,累加100次的结果是否是10,可以结果又一次出乎意料,结果是10.000002。
public static void main(String[] args) {
float sum=0.0f;
float step = 0.1f;
for (int i = 0; i < 100; i++) {
sum += step;
}
System.out.println(sum);
}
}
如何避免计算机计算出错
我们知道,无论是用纸面二进制还是浮点数表示小数,都存在计算出错的可能性,那么我们该如何避免这种问题呢?
首先是回避策略,即无视这些问题。有时候一些微小的偏差并不会造成什么问题。其次,把小数转换成整数来计算。还是以0.1累加100次为例,将0.1扩大10倍后累加100次,最后把结果除以10就可以了。
public class TestBinary {
public static void main(String[] args) {
int sum=0;
int step = 1;
for (int i = 0; i < 100; i++) {
sum += step;
}
System.out.println(sum/10);
}
}