好多人工作了几年,都不明白什么是补码,原码,反码.
在开篇我们先介绍一下二进制的一些知识,毕竟在计算机的运算中全部都是使用的二进制。
二进制分为有符号数和无符号数。
- 有符号数,用来在计算机中表示正数和负数,最高位是符号位,0表示正数,1表示负数。
比如1010 0010就表示一个负数。
- 无符号数,最高位不是符号位,而是二进制数字的一部分。比如10100010就表示一个正数162.
Java中所有的数字均为有符号数,最高位是符号位。
比如int为32位,byte为8位,short为16位,long为64位,他们的取值范围分别是:
类型 | 取值范围 | 描述 |
Byte | -128~127 | 占用1个字节,-2的7次方到2的7次方减1 |
Short | -32768~32767 | 占用2个字节,-2的15次方到2的15次方-1 |
Int | -2147483648~2147483647 | 占用4个字节,-2的31次方到2的31次方-1 |
Long | -9223372036854774808~ 9223372036854774807 | 占用8个字节(-2的63次方到2的63次方-1) |
其实,就拿最简单的4位bit(如图)来说,这个数表示的十进制数就是2^4-1.即2^3+2^2+2^1+1=8+4+2+1=15=2^4-1,这个是在不考虑符号位的情况下 计算出的结果。
1 | 1 | 1 | 1 |
由此,可以知道JAVA中32位int的最大值是除了最高位0之外的31位1的数字,即:
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
4个1是2^4-1,那么31个1就是2^31-1,这就是int型的最大值。
那java中int型最小值怎么表示呢?
我们再拿四个bit位(如图)来举例子,即符号位为0,其余三位为1的值0111,换算为十进制就是2^2+2^1+1=7,这是四个bit位的最大值,表示为2^3-1,即7.
0 | 1 | 1 | 1 |
那么四个bit位的最小值是多少?其实就是最大值加1,结果是:
1 | 0 | 0 | 0 |
这个数其实就是四位bit位所能表示的最小值,表示为:-2^3,即-8.
所以四位bit位表示的范围是[-2^3, 2^3-1],即[-8,7]
那这里继续引申一下:
对于n位bit的整数类型?
- 符号位是1,其余n-1位是0,我们表示为-2^(n-1),而不是2^(n-1).,表示为最小值
- 符号位为0,其余n-1位是1,我们表示为2^(n-1)-1,表示为最大值
所以n位bit位的整数,所能表示的范围是[-2^(n-1), 2^(n-1)-1]
一旦数字超过了这个限定,就会发生溢出,超过上限,叫做上溢出。超过下限,叫做下溢出。上溢出之后,又从下限开始:最大的数加1,就变成了最小的数。下溢出之后又从上限开始,最小的数减去1就变成了最大的数,如此循环往复。
public static void main(String[] args) {
byte c = 127;
byte d = 1;
// byte f = c + d;//编译错误,越界
// System.out.println(f);
System.out.println((byte)(c + d));//-128,127+1,即最大值加1,超过最大值范围,上溢出为最小值
byte g = -128;
byte h = -1;
System.out.println((byte)(g+h));//127,-128-1,得到一个byte类型的最大值127,即超过最小值范围,下溢出为最大值
}
这有点像对一个数取模的概念
比如当取模的除数为5时,任意一个数对5取模,得到的都是[0,4]区间内的一个数。而巧的是区间上限减去区间下限再加1,刚好就是取模的除数。比如4-0+1=5.
所以对于二进制的溢出也是一样的,[-2^(n-1), 2^(n-1)-1]的上限减去下限再加1,的值就是取模的除数,这个值是:
2^(n-1)-1-(-2^(n-1))+1=2^n=2^n-1+1
在[-2^(n-1), 2^(n-1)-1]区间内的任意数字加上这个值得到的永远是[-2^(n-1), 2^(n-1)-1]内的值。
再比如今天是周一,加上七天后、14天后、21天后…7*n天之后的那天都是周一一样。因为每周只有周一到周日这7天的区间[1,7],那么根据我们的公式:7-1+1=7,就是取模的除数。任意一天加上这个区间内的任意一个值,得到的都是周一到周日。
接着我们介绍原码、反码、补码的概念:
原码:就是我们看到的二进制的原始表示。最高位为符号位。其余位数表示该数字绝对值的二进制。
我们是否可以直接用负数的原码来进行减法计算呢?
我们来看一个例子3+(-2),假如我们使用的是java语言中的int型表示3和-2.
那么-2的二进制原码表示:
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
最高位1,低两位10,其余位0.
3的二进制原码表示:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
最高位0,低两位11,其余位0.
3和-2的二进制原码相加得到:
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
但这个表示的是几呢?-5的原码。显然3-2等于1。
所以负数的原码不能直接用来做减法。其实CPU的运算器中只有加法器,减法是由加法换算而来的。
假设i-j,其中j为正数。如果i-j加上取模的除数2^n-1+1,那么就会形成溢出可以得到i-j的值:
i-j=(i-j)+(2^n-1+1)=i+(2^n-1-j+1)
我这里为什么一直在强调2^n-1+1这个写法?
2^n-1所表示的二进制有n个1(就跟2^4-1=15表示1111一样)。在不考虑符号位的情况下有n-1个1,那么2^n-1-2的结果:
从结果可以看出,2^n-1-j相当于对正数j的二进制原码除了符号位外按位取反。-j和j的原码,除了符号位外都是相同的,所以2^n-1-j相当于对负数-j的二进制原码除了符号位外按位取反。我们把2^n-1-j所对应的编码称为负数-j的反码。所以:
i-j=(i-j)+(2^n-1+1)=i+(2^n-1-j+1)
就等于i的原码+(-j的反码+1),也就是i的原码+(-j的补码).如果i也是正数,那么结果等于
i-j= i的补码+(-j)的补码
因为正数的原码、反码、补码都相同。
补充知识:二进制的位操作
向左移位:表示为<<,二进制110101(十进制的53)向左移动一位,就是在末尾添加一位0.变为1101010(十进制的106).
左移一位后,53变成了106,结论是:二进制左移一位,其实是将数字翻倍。
向右移位:表示为>>>.二进制110101(十进制53)向右移动一位,就是去除末尾的那一位,变成了10010(十进制26),而26也是53除以2取整的值。因此二进制右移一位,就是数字除以2取整后的值。
问题:左移是<<.为啥右移就是>>>?
先看下53的32位表示:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 |
再看下-53的32位表示
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 |
那么-53右移操作,符号位1我们是否需要右移呢?
抛开这个问题不论,再来看JAVA中定义的两种右移:
逻辑右移:符号>>>,逻辑右移1位,左边补0即可。
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 |
算术右移:符号>>,算术右移1位,符号位保持不变,其他一律右移一位,并且补符号位1,补的1仍然在符号位之后。
按位或
表示为|
参与操作的位中,只要有一个位为1,那么最终结果是1,也就是真
比如:110101和100011每一位对齐进行按位或,结果是110111
按位与
表示为&
参与操作的位中必须全部是1,那么最终结果才是1,否则为0.
比如:110101和100011每一位对齐进行按位与,结果是100001
应用:使用按位与来判断一个数的奇偶性。
偶数的二进制最后一位总是0;奇数的二进制最后一位总是1,那么对于给定数值,可以让其二进制与数字1进行按位与操作,然后取得这个数的最后1位,如果是1说明是奇数,否则说明是偶数。
按位异或
表示为^
参与操作的位相同,结果为0,否则为1.所以要想得到1,必须参与的操作位是不同的,这就是异的含义。比如:110101和100011每一位对齐进行按位异或,结果是010010
根据按位异或的性质,所有的数值跟自身异或操作后,结果是0.而且想得到0,两个数值也必须是相同的。这可以作为判断两个变量是否相等的条件。而且任意一个数字跟0按位异或结果都是这个数本身。比如:x^0=x