一文搞明白位运算、原码、反码、补码

之前做过类似的文章,讲的不是太好,现在重新记录一遍,方便自己理解
参考文献:一文搞明白位运算、补码、反码、原码

二进制的正负

我们十进制使用加减号+ -表示数字的正负,而计算机是二进制的,怎么表示正负呢?

答:二进制中的第一位来表示符号位,0表示正数,1表示负数

正数的位移运算

Java中有三个位移运算

  • <<左移:符号位不变,高位丢弃,低位补0
  • >>右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1
  • >>>无符号右移:右移符号位也跟着移动,低位丢弃,高位补0

我们直接来看一下Demo:

1
2
3
4
5
6
System.out.println(2 << 1);     // 4
System.out.println(2 >> 1); // 1
System.out.println(2 >>> 1); // 1
System.out.println(-2 << 1); // -4
System.out.println(-2 >> 1); // -1
System.out.println(-2 >>> 1); // 2147483647

一眼看到上面Demo的打印结果,你应该是懵逼的,接下来我来解释一下这个结果到底是如何运算出来的。
上面的Demo中有“2”和“-2”,这是两个十进制数,并且是int类型的(java中占四个字节),位运算是基于二进制bit来的,所以我们需要将十进制转换为二进制之后再进行运算:

  • 2 << 1:十进制“2”转换成二进制为“00000000 00000000 00000000 00000010”,再将二进制左移一位,高位丢弃,低位补0,所以结果为“00000000 00000000 00000000 00000100”,换算成十进制则为“4”
  • 2 >> 1:十进制“2”转换成二进制为“00000000 00000000 00000000 00000010”,再将二进制右移一位,低位丢弃,高位补0,所以结果为“00000000 00000000 00000000 00000001”,换算成十进制则为“1”
    对于这两种情况非常好理解,那什么是无符号右移,以及负数是怎么运算的呢?
    我们先来看-2 << 1-2 >> 1,这两个负数的左移与右移操作其实和正数类似,都是先将十进制数转换成二进制数,再将二进制数进行移动,所以现在的关键是负数如何用二进制数进行表示。

原码、反码、补码

接下来我们主要介绍十进制数用二进制表示的不同方法,所以为了简洁,我们用一个字节,也就是8个bit来表示二进制数。

原码

十进制 原码
2 0000 0100
-2 ‭ 1111 1110‬

原码其实是最容易理解的,只不过需要利用二进制中的第一位来表示符号位,0表示正数,1表示负数,
所以可以看到,一个数字用二进制原码表示的话,取值范围是-1111 1111 ~ +0111 1111,换成十进制就是-127 ~ 127

反码

在数学中我们有加减乘除,而对于计算机来说最好只有加法,这样计算机会更加简单高效,
我们知道在数学中5-3=2,其实可以转换成5+(-3)=2,
这就表示减法可以用加法表示,而乘法是加法的累积,除法是减法的累积,所以在计算机中只要有加法就够了。

一个数字用原码表示是容易理解的,但是需要单独的一个bit来表示符号位。
并且在进行加法时,计算机需要先识别某个二进制原码是正数还是负数,识别出来之后再进行相应的运算。
这样效率不高,能不能让计算机在进行运算时不用去管符号位,也就是说让符号位也参与运算,这就要用到反码。

十进制 原码 反码
2 0000 0010 0000 0010
-2 1000 0010 1111 1101

正数的反码:和原码一样
负数的反码:在原码的基础上符号位保持不变,其他位取反。

那么我们来看一下,用反码直接运算会是什么情况,我们以5-3举例。
5 - 3 等于 5 + (-3)

十进制 原码 反码
5 0000 0101 0000 0101
-3 1000 0011 1111 1100
1
2
3
4
5
6
5-3
= 5+(-3)
= 0000 0101(反码) + 1111 1100(反码)
= 0000 0001(反码) // 这里举例的是8个bit,1 0000 0001 ,只取后8位 = 0000 0001
= 0000 0001(原码)
= 1

这不对呀??? 5-3=1???,为什么差了1?

我们看一个特殊的运算:

1
2
3
4
5
6
1-1
= 1+(-1)
= 0000 0001(反码) + 1111 1110(反码)
= 1111 1111(反码)
= 1000 0000(原码)
= -0

我们来看一个特殊的运算:

1
2
3
4
5
0+0
= 0000 0000(反码) + 0000 0000(反码)
= 0000 0000(反码)
= 0000 0000(原码)
= 0

我们可以看到1000 0000表示-0,0000 0000表示0,虽然-0和0是一样的,但是在用原码和反码表示时是不同的,
我们可以理解为在用一个字节表示数字取值范围时,这些数字中多了一个-0,
所以导致我们在用反码直接运算时符号位可以直接参加运算,但是结果会不对。

补码

为了解决反码的问题就出现了补码。

十进制 原码 反码 补码
2 0000 0010 0000 0010 0000 0010
-2 1000 0010 1111 1101 1111 1110

正数的补码:和原码、反码一样
负数的补码:就是反码 + 1

十进制 原码 反码 补码
5 0000 0101 0000 0101 0000 0101
-3 1000 0011 1111 1100 1111 1101

1)、回到之前的5-3

1
2
3
4
5
6
5-3
= 5+(-3)
= 0000 0101(补码) + 1111 1101(补码)
= 0000 0010(补码) // 这里举例的是8个bit,1 0000 0010 ,只取后8位 = 0000 0001
= 0000 0010(原码)
= 2

5-3=2!!正确。

2)、1-1
再来看特殊的:

1
2
3
4
5
6
1-1
= 1+(-1)
= 0000 0001(补码) + 1111 1111(补码)
= 0000 0000(补码) // 这里举例的是8个bit,1 0000 0000 ,只取后8位 = 0000 0000
= 0000 0000(原码)
= 0

1-1=0!!正确

3)、0+0
再来看一个特殊的运算:

1
2
3
4
5
0+0
= 0000 0000(补码) + 0000 0000(补码)
= 0000 0000(补码)
= 0000 0000(原码)
= 0

0+0=0!!也正确。

4)、-2-2
再来看一个特殊的运算:

1
2
3
4
5
6
7
-2-2
= -2+(-2)
= 1111 1110(补码) + 1111 1110(补码)
= 1111 1100(补码) // 这里举例的是8个bit,1 1111 1100 ,只取后8位 = 1111 1100
= 1111 1011(反码)
= 1000 0100(原码)
= -4

-2+(-2)=-4!!也完全正确。

所以,我们可以看到补码解决了反码的问题。
所以对于数字,我们可以使用补码的形式来进行二进制表示。

妙啊妙啊秒啊秒,突然想到了,为什么Java中的数字都限制了大小,必须限制最大的那个位,目的就是让计算机知道哪个是符号位。

负数位运算

  • <<左移:符号位不变,高位丢弃,低位补0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    10000000 00000000 00000000 00000010 // -2原码
    11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
    11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

    -2 << 1 : 表示-2的补码左移一位得到补码,再该补码得到=》反码=》原码

    11111111 11111111 11111111 11111100 // -2补码左移一位(补码)
    11111111 11111111 11111111 11111011 // -2补码左移一位(反码 => 补码-1)
    10000000 00000000 00000000 00000100 // -2补码左移一位(原码 => 反码取反,符号位除外)
    = -4
  • >>右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    10000000 00000000 00000000 00000010 // -2原码
    11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
    11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

    -2 >> 1 : 表示-2的补码右移一位得到补码,再该补码得到=》反码=》原码

    11111111 11111111 11111111 11111111 // -2补码右移一位(补码): 注意符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1
    11111111 11111111 11111111 11111110 // -2补码右移一位(反码 => 补码-1)
    10000000 00000000 00000000 00000001 // -2补码右移一位(原码 => 反码取反,符号位除外)
    = -1

无符号右移

  • >>>无符号右移:右移符号位也跟着移动,低位丢弃,高位补0
    上面在进行左移和右移时,我有一点没讲到,就是在对补码进行移动时,符号位是固定不动的,
    而无符号右移是指在进行移动时,符号位也会跟着一起移动
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    10000000 00000000 00000000 00000010 // -2原码
    11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
    11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

    -2 >>> 1 : 表示-2的补码无符号右移一位得到补码,再该补码得到=》反码=》原码

    01111111 11111111 11111111 11111111 // -2补码无符号右移一位(补码)
    因为现在的符号位为0,表示正数,正数的原、反、补码都相同

    所以对应十进制就是 -2 >>> 1 = 2^31 -1 = 2147483647

总结

1)、
二进制中的第一位来表示符号位,0表示正数,1表示负数

2)、

  • <<左移:符号位不变,高位丢弃,低位补0
  • >>右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1
  • >>>无符号右移:右移符号位也跟着移动,低位丢弃,高位补0

3)、
正数:原码,反码,补码相同。

负数的原码:-1的原码:10000000 00000000 00000000 00000001
负数的反码:原码,符号位不变,其他位取反
负数的补码:就是反码 + 1

4)、
参与运算的是补码,真实可以转为二进制的是原码

5)、
5-2可以表示为:5+(-2),减可以用加表示,乘可以用多个加表示,除可以用多个加表示,这样加减乘除都可以用加表示了。
反码就是为了实现负数也可以参与加法而产生了,但是会出现运算错误的现象,所以诞生了补码。

原码:真实的二进制表示,第一位表示0表示正,1表示负
反码:为了实现正负数都可以相加所采取的操作
补码:为了解决反码相加之和,数据不一致的情况,使用补码进行位运算就能成功解决

6)、Integer.toBinaryString所打印的是补码

位运算符& | ^ ~

1)、&
二元运算符
and,与,同为1才为1

1
2
3
4
5
6
7
8
9
10
10000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

5 & -2

00000000 00000000 00000000 00000101 // 5补码
11111111 11111111 11111111 11111110 // -2补码
00000000 00000000 00000000 00000100 // 5 & -2补码,反码,原码 => 正数:原码,反码,补码相同。
= 4

2)、|
二元运算符
or,或,一个为1则为1

1
2
3
4
5
6
7
8
9
10
11
12
10000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

4 | -2

00000000 00000000 00000000 00000100 // 4补码
11111111 11111111 11111111 11111110 // -2补码
11111111 11111111 11111111 11111110 // 4 | -2补码
11111111 11111111 11111111 11111101 // 4 | -2反码
10000000 00000000 00000000 00000010 // 4 | -2原码
= -2

3)、^
二元运算符
not equals,异或,两个不同才为1

1
2
3
4
5
6
7
8
9
10
11
12
10000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)

1 ^ -2

00000000 00000000 00000000 00000001 // 1补码
11111111 11111111 11111111 11111110 // -2补码
11111111 11111111 11111111 11111111 // 1 ^ -2补码
11111111 11111111 11111111 11111110 // 1 ^ -2反码
10000000 00000000 00000000 00000001 // 1 ^ -2原码
= -1

4)、~
一元运算符
not,非,所有位取反

1
2
3
4
5
6
7
~1

00000000 00000000 00000000 00000001 // 1补码
11111111 11111111 11111111 11111110 // ~1补码
11111111 11111111 11111111 11111101 // ~1反码(补码-1)
10000000 00000000 00000000 00000010 // ~1原码(反码取反,符号位不变)
= -2

下面是给同事陈鹤的讲解

减法可以用加法表示

5 - 3
= 5 + (-3)

计算机只想用加法:乘就是多个加,除就是多个加

计算机的二进制正数表示

十进制 正负 + -

二进制:首尾01 1负 0正

原码

byte = 8bit
0 = 0000 0000

1 + 2
= 0000 0001原码 + 0000 0010原码
= 0000 0011原码
= 3

5 + (-3)
= 0000 0101原码 + 1000 0011原码
= 1000 1000原码
= -8错误了

反码

正数:原码,反码,补码相同。
负数:
反码:原码的符号位不变,所有位取反

5 + (-3)
= 0000 0101反码 + 1111 1100反码
= 0000 0001反码 // 1 0000 0001 因为byte表示8bit,只能取后8位,结果就是 0000 0001
= 1还是错误了

补码

正数:原码,反码,补码相同。
负数:
反码:原码的符号位不变,所有位取反
补码:反码+1

5 + (-3)
= 0000 0101补码 + 1111 1101补码
= 1 0000 0010假补码 // 1 0000 0010 因为byte表示8bit,只能取后8位,结果就是 0000 0010
= 0000 0010真补码
= 2

long 64 [2^64-1,-2^64]
int 32 [2^31-1,-2^31]
byte 8 [2^7-1,-2^7]

逻辑运算符号

&& ||

布尔类型二进制表示

true:1
false:0

位运算符号

& | ^ ~

&:同为1才为1

|:一个为1则为1

^:不相同才为1
1^2
=
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000010

~:对所有位取反
~1
= 00000000 00000000 00000000 00000001 // 1补码
= 11111111 11111111 11111111 11111110 // ~1补码
= 11111111 11111111 11111111 11111101 // ~1反码
= 10000000 00000000 00000000 00000010 // ~1原码
= -2

注意事项

1)、参与位运算的都是补码,因为补码才可以实现正负数的相加

完活

2020-07-02 17:38:44