从上一篇看,二进制似乎不能像十进制那样使用负号来表达负数概念,其实负号只是一个约定,十进制不用负号也能表示负数,只要我们约定最高位表示负权重,其它不变,比如100表示负100 199表示负100加99等于负一,099表示正99,二进制也可以这样做,而且比十进制更适合这样做。
从数学上将,二进制可以像十进制一样使用无穷多位来表示一个数,如101011....1011010,但实际中计算机对位数的存储和处理是有限的,所以对一般数的表达范围也是有限的,一些运算也是出户意料的
以Java中最短的8位byte类型为例,byte类型范围在1000 0000(-128)到0111 1111(127)之间,要是在坐标轴上会是这样

按照数学中的思路,127要是往上加得往右走变得越来越大,同理-128减下去会越来越小,但在计算机中的结果是这样的,似乎位数的限制让结果陷入了循环
0111 1111 127
+ 0000 0001 +1
= 1000 0000 -128
1000 0000 -128
- 0000 0001 -1
= 0111 1111 127
事实确实如此,有限位数的关系更像下面这样的矩形,矩形的实际宽高比不像图上画的那样,-1和0之间、min和max之间没有数,其它数都在-1到min、0到max之间。虽然以byte为例,但换成其它基本类型,结构是一样的。图中标注的是4个顶点的值,这四个点正如它们所在的位置一样特殊,是4个转折点,max往前走一步就是min,1111 1111往前走一步就是0000 0000,同样后者往回退一步就变成了前者

由于这种特殊循环关系,计算机中两个数相加或相减就有可能出现非预期值,本来正数变大、负数变小应该朝着下图虚线的方向,但实际出现非预期转向,走向超过这两个点就会溢出,我们把这两个点称为为溢出点,-1到0也是溢出,但属于预期的

从两个溢出点可以看出,溢出的可能有两种
- 两个正数相加:从图上看如同0到max的某点逆时针往上走
- 两个负数相加:-1到min的某点顺时针往上走
- 一正一负相加不会溢出,它们没有朝虚线的方向走的足够长,比如-1 + 127 ,0-128 都没溢出
根据上面的分析使用下面代码判断两个byte类型数相加是否会溢出
//代码段改自深入理解计算机系统
private boolean addOk(byte x, byte y) {
byte sum = x + y;
//负溢出
boolean negOver = x < 0 && y < 0 && sum >= 0;
//正溢出
boolean posOver = x >= 0 && y >= 0 && sum < 0;
return !negOver && !posOver;
}
那两个数相减呢,x-y可以表示为x+(-y),看上去我们只需要把y取负值传到上面方法中即可
private boolean subOk(byte x, byte y) {
return addOk(x,-y);
}
但我们将图变换成下面这样,你会发现椭圆上到0这个点距离相同的正负数倆倆成对出现,但有两个例外,一个是0本身,别一个就是Min,也就是0=-0,Min=-Min,前一个没啥意义,但后一个是需要注意的,也就是对min取负值结果还是负值,这影响了符号的判断从而使上面的方法对min失效。

比如 1- (-127)=1+127=128, subOk能判断出来是正溢出;但1 - (-128)=1+128实际是溢出的,由于-min=min,subOk判断的结果是1-128,一正一负是不溢出的。
再比如-1 - (-127)=-1+127,subOk能判断出来是溢出的;-1 - (-128)=127实际上是不溢出的,subOk判断的结果是-1-128,负溢出。
也就是y=min时,x为负数不会溢出
private boolean subOk(byte x, byte y) {
if(y=min){
return x<0;
}else{
return addOk(x,-y);
}
}
总结
编程时发生溢出其实挺常见,以上面为例,-128<1,那么在数学中-128-1<0是铁律,但此时在计算机中-128-1=127>0;导致判断结果完全相反,平时要有溢出的意识,尽量避免造成溢出的条件,不要用相减比较两个数的大小。
网友评论