对于正数:
反码、补码都与原码一样。
对于负数:
反码:原码中除去符号位,其他的数值位按位取反,即0变1,1变0
补码:反码+1
下面给出几个示例:
40:
原码:00101000
反码:00101000
补码:00101000
-216:
原码:1000000011011000
反码:1111111100100111
补码:1111111100101000
-107:
原码:11101011
反码:10010100
补码:10010101
可以看到,对于正数,其原码、反码、补码相同。对于负数,原码中最高位用来表示符号,反码就是除了最高位外,其余位取反,补码就是反码+1
为什么要设计补码
上面介绍了原码、反码和补码三者的概念,那么,计算机中为什么要设计补码这一概念呢?因为直接用原码涉及到减法操作,这就增加了计算机底层电路涉及的复杂性。而用补码操作时,当减去一个数时,可以看做加上一个负数,然后转变位加上这个负数的补码。即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了.
而使用补码的好处还有,在计算中可以直接带上符号位进行计算,比如计算
40-13:
其中 40 的补码为:0010 1000
-13的补码为:1111 0011
因此实际的运算过程就可以直接带符号位进行相加,同时,如果最高位(符号位)有进位,则舍弃。
0010 1000 + 1111 0011 = 1 0001 1011
这里最高符号位的进位可以舍弃,因为8位2进制,能表示的数在-128~127之间。而1 0001 1011 = 283,将283转化到这个范围中即为:
283 % 256 = 27。
其实也相当于 0001 1011. 所以符号位的最高进位可以舍去。
这种思想其实就类似于时钟理解法,如下图所所示:
图中所示4位二进制表示的数范围在-8~7 之间,因为最高位要用来表示符号位。从图中可以看出,表一共可以表示16个数(0~15),-1就相当于表从0的位置向左移动一格,也相当于表从0的位置向右移动15格。所以-1补码就是15(-1+16=15),-2的补码就是14(-2+16=14),依次类推。
查阅实际的进制转化结果可以进行证明:
-1的补码为:11111111
15的补码为:00001111
-2的补码为:11111110
14的补码为:00001110
-7的补码为:11111001
9的补码为:00001001
可以看到除了符号位外,其余位的结果是相等的。即验证了刚刚的思想。
所以对于负数来说都可以在某个周期内找到一个对应的正数来表示,而这个正数的计算公式为:
n = m+c
其中 c 表示周期
n 表示周期内的一个负数
m 表示n在周期c内对应的正数
因此对于4-2的计算过程可以如下所示:
4 + (-2) = 0100(补) + 1110(补) = 1 0010
舍弃掉符号位的进位结果,即得到0010=2。
另一种计算方法是:
由上述介绍可知,4位二进制表示的周期位16,则-2的补码为16+(-2)=14
所以14+4=18 % 16 = 2
对应的二进制加法过程为:
1110 + 0100 = 1 0010
同样舍弃掉最高位的进位后,得到的结果也为0010=2
因此在计算机中,利用补码的方式进行运算,就可以解决直接用原码来计算得到的结果是错误的问题,也解决了减法的问题。而根据所给的负数去得到它对应的补码的过程,就是找到一个正数来代替其负数的过程。。
比如对于-2 的原码为 1 0010,按照求反码的规则对其按位取反的结果为:
1 1101
不考虑符号位,结果为13。
反码的结果+1 = 1 1110 结果为14
跟我们一开始计算16+(-2)=14的结果相同,就找到了替换-2的正数14。所以其实对原码求反码后再加1的过程,就是在对一个负数加上一个范围(4位二进制能表示的范围就是16个数)。因此,对于负数,求补码其实就是为了求出一个在某一范围内可以代替它的正数,而求补码的过程就是求该正数的过程。
最后就是带符号进行运算,得到结果。
通过上述的例子我们可以看到求补码就是找到一个正数来替代一个负数,完成原先的减法操作。那么如何来找这个正数呢?为什么找到这个正数就可以替换该负数进行减法运算呢?直观上,正数可以用上面的公式来找到,接下来介绍一种更严谨的方式,来获取对应的正数。
原码、反码、补码再深入
上述通过逻辑和实例演示了求补码的目的、思路和过程。接下来,介绍其背后蕴含的数学原理。
同样是上面的钟表图,想象其位一个1位的16进制数,如果当前时间是4点,希望将时间设置成2点,需要怎么做呢?
第一种是往回拨2个小时,4 - 2 = 2
第二种是往前拨14个小时, (4+14)mod 16=2
第三种是往前拨14+16=30个小时,(4+30)mod 16 = 2
上述中的mod表示取模操作。所以通过上面的过程不难看出,钟表的回拨(减法)的结果可以用往前拨(加法)来替代。
因此,还是上面的问题,如何找到一个正数来替代一个负数呢?通过上面的过程以及开头的描述,不难发现一些端倪。但是数学是严谨的,不能靠感觉。
首先需要介绍一个数学中相关的概念:同余
数学上,两个整数除以同一个整数,若得相同余数,则二整数同余
记作 a≡b (mod m)
比如:
26≡2(mod 12)
所以26, 2 关于模12同余
负数取模
x mod y = x-y ×「x/y」
1
其中「/」表示对结果取下界。上面公式的意思就是:x mod y等于 x- (y×x与y的商的下界)。
比如:
-3 mod 2
= -3 - 2×「-3/2」
= -3 - 2 × 「-1.5」
= -3 - 2 × (-2)
= -3 + 4
= 1
所以
(-2) mod 16 = 16 -2 =14
(-4) mod 16 = 16 -4 = 12
(-7) mod 16 = 16-7 = 9
有了同余和负数的模的概念后,就可以开始证明了:
还是回到上面的时钟例子上,
(-2) mod 16 = 14
14 mod 16 = 14
因此由同余的概念可知,-2和14是同余的。
同时,有同余数的两个定理:
反身性:
反身性:a≡a (mod m)
显而易见,一个数肯定跟它自己是同余的。
线性运算定理:
如果a ≡ b (mod m),c ≡ d (mod m) 那么:
(1)a ± c ≡ b ± d (mod m)
(2)a * c ≡ b * d (mod m)
1
2
3
所以:
4 ≡ 4 (mod 16)
(-2) ≡ 14 (mod 16)
4 -2 ≡ 4 + 14 (mod 16)
因此,通过上面的过程,我们首先找到了一个正数,于待减的负数是同余数,然后又利用公式证明了一个恒等式,即对于(4-2) mod 16 恒等于 (4+14) mod 16。也就是证明了,我们可以用该正数来代替该负数,得到的结果是相同的。
接下来,回到二进制的问题上,看一下4-2=2的问题:
4 -2 = 4 + (-2) = [0000 0100](原) + [1000 0010](原)
= [0000 1011](反) + [1111 1101](反)
先到这一步,-2的反码表示为1111 1101。这里如果认为其为原码,则1111 1101 = -125,这里将符号位除去,即认为是126.
现有:
(-2) mod 127 = 125
125 mod 127 = 125
即
(-2) ≡ 125 (mod 127)
又 4 ≡ 4 (mod 127),同样由上面的线性定理可知:
4-2≡ 4 + 125 (mod 127)
可以看出,4-2 与 4+125的余数结果是相同的。而这个余数,正是我们所期望的计算结果:4-2=2
从上面的过程,也就能大致端倪出为什么求补码前要先求反码。
其实求反码,实际上就是在求这个数对于一个模的同余数。而这个模代表的就是最大值。就跟钟表的思想一样,转了一圈(最大值16)后总能找到在可表示范围内的一个正确的数值。
而4+125显然已经超过了最大值127,相当于转过了一轮,而因为符号位是参与计算的,正好和溢出的最高位形成正确的运算结果。
既然上述过程,通过0反码已经可以将减法变成加法,那么为什么还要使用补码呢?为什么在反码的基础上需要加1才能得到正确的结果呢?
4+(-2) = [0000 0100](原) + [1000 0010](原)
=[0000 1011](补) + [1111 1110](补)
如果把[1111 1110]当成原码,去除符号位,得到的结果为
[0111 1110] = 126
其实,在反码的基础上+1,只是相当于增加了模的值:
(-2) mod 128 = 126
126 mod 128 = 126
4-2 ≡ 4+126 (mod 128)
此时,表盘相当于128个刻度转一轮。所以用补码表示的运算结果最小值和最大值应该是[-128,128],但是由于0的特殊性,没有办法表示128,所以补码的取值范围是[-128, 127]。
这里反码+1,也是因为-128的补码为1000 0000,所以整个刻度可以划分为128份,便对128取模。而这里的模同样可以取256,也就是最开头模的划定方式,因为256是128的周期倍。
int类型转byte类型
有了上面的知识,理解下面这段代码就很容易了:
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[len - i - 1] = (byte)((value >> 8 * i) & 0xff);
}
return b;
}
这里的参数value 表示待转换的int类型变量, len表示字节数组的长度。
因为字节数组每一位都代表了8位二进制数,所以对value右移8位,第二次右移16位,依次类推,来保存字节数组中每一位的数值。
比如,对于-216,设置len=4的结果为:
-216的补码:11111111111111111111111100101000