位运算的那些事(二)如何位运算

上一篇为了讲位运算不得已将二进制机器码与真值之间的关系系统的解释了一通,本篇我们就根据这些基础将二进制位运算几个常用的运算符运算规则理一理,相信本文之后你就明白了这一个过程。

上篇我也提到位运算符主要针对二进制,它主要包括:“与(&)”、“或(|)”、“非(~)”、“异或(^)”,当然还有移位运算(左移、右移,无符号左移),这在开发过程中也是很常见的,下边我就以这两部分来说。

位运算过程

常见的位运算都是针对两个值之间的运算,只有“非”是针对一个值计算。

再次强调一下,上篇文章我提到的计算机最终参与运算和存储的是真值的二进制补码,所以本篇位运算转化为二进制都要以补码的形式进行运算,然后再还原成真值才能得到正确的结果,这也是上篇文末提到的两个过程。

为了描述简单起见,下边四种位运算都用a = 12, b= -21来进行运算,所以我们先对这两个数字进行补码,用一个字节来表示,方便下边运算使用,如果这里还看不懂,建议返回上一篇。

伪代码:

a = 12 = [0000 1100]原 = [0000 1100]反 = [0000 1100]补

b = -21 = [1001 0101]原 = [1110 1010]反 = [1110 1011]补

与(AND)

与运算符用符号“&”表示,其使用规则如下:
两个操作数所对应的二进制补码相同坐标位值都为1,结果才为1,否则结果为0。例如下面的程序段:

    System.out.println("a和b,与的结果是:"+(a&b));

运行结果:
a和b,与的结果是:8

分析:
“a”的补码:0000 1100
“b”的补码:1110 1011
“与”的补码:0000 1000
转换为真值:[0000 1000]补 = [0000 1000]反 = [0000 1000]原 = 8

或(OR)

或运算符用符号“|”表示,其运算规则如下:
两个操作数所对应的二进制补码相同坐标位值有一个为1,结果就为1,否则结果为0。例如下面的程序段:

    System.out.println("a和b,或的结果是:"+(a|b));

运行结果:
a和b,或的结果是:-17

分析:
“a”的补码:0000 1100
“b”的补码:1110 1011
“或”的补码:1110 1111
转换为真值:[1110 1111]补 = [1110 1110]反 = [1001 0001]原 = -17

非(NOT)

非运算符用符号“~”表示,其使用规则如下:
操作数所对应的二进制补码位值都为0,结果为1,位值为1结果为0。例如下面的程序段:

    System.out.println("a,非的结果是:"+(~a));

运行结果:
a,非的结果是:-13

分析:
“a”的补码:0000 1100
“非”的补码:1111 0011
转换为真值:[1111 0011]补 = [1111 0010]反 = [1000 1101]原 = -13

这里有一个有趣的规律:

  • 所有正整数的非,是其本身加1的负数
  • 所有负整数的非,是其本身加1的正数
  • 0的非,是-1

异或(XOR)

异或运算符用符号“^”表示,其使用规则如下:
两个操作数所对应的二进制补码相同坐标位值相同(无论同为1还是同为0)则为为0,不同则为1。例如下面的程序段:

    System.out.println("a和b,异或的结果是:"+(a^b));

运行结果:
a和b,异或的结果是:-25

分析:
“a”的补码:0000 1100
“b”的补码:1110 1011
“异或”的补码:1110 0111
转换为真值:[1110 0111]补 = [1110 0110]反 = [1001 1001]原 = -25

到这里四种位运算符的运算过程已经分析完了,是否和你之前想象的一样呢?如果一样,恭喜你,你已经理解了位运算相关运算符的运算过程了。下边我们就进一步探讨移位运算的运算过程。

移位运算过程

移位运算中主要涉及到三种运算符:左移(<<)、右移(>>)、无符号右移(>>>)。假设有一个管道是固定长度的(这个长度是根据数据类型来确定的),移位运算就是一组二进制码在此管道中移动,不够的地方做补充,多余的部分省去。下边就分别讲一下这三个区别以及运算过程。

为了方便下边的运算,我们这里还是先定义一个数字a = -8,则:
“a”的原码:1000 1000
“a”的反码:1111 0111
“a”的补码:1111 1000

假设管道长度按照一个字节8位来定(按照移动位数2位,管道长度我猜可能是够用的)。

左移

左移运算符用符号“<<”表示,其使用规则如下:
操作数所对应的二进制补码整体向左移动,原来左边的数据会被溢出,右边会空缺相应的位数,需要进行补0。例如下面的程序段:

    System.out.println("a和b,异或的结果是:"+(a<<2));

运行结果:
a,左移2位的结果是:-32

分析:
“a”的补码:1111 1000
“左移2位”的补码:1110 0000
转换为真值:[1110 0000]补 = [1101 1111]反 = [1010 0000]原 = -32

右移

右移运算符用符号“>>”表示,其使用规则如下:
操作数所对应的二进制补码整体向右移动,原来右边的数据会被溢出,左边会空缺相应的位数,需要进行补0或补1(根据原值的正负,正数补0,负数补1)。例如下面的程序段:

    System.out.println("a和b,异或的结果是:"+(a>>2));

运行结果:
a,右移2位的结果是:-2

分析:
“a”的补码:1111 1000
“右移2位”的补码:1111 1110
转换为真值:[1111 1110]补 = [1111 1101]反 = [1000 0010]原 = -2

无符号右移

无符号右移运算符用符号“>>>”表示,其使用规则如下:
操作数所对应的二进制补码整体向右移动,原来右边的数据会被溢出,左边会空缺相应的位数,忽略正负情况,统一补0。例如下面的程序段:

    System.out.println("a和b,异或的结果是:"+(a>>>2));

运行结果:
a,无符号右移2位的结果是:1073741822

分析:
“a”的补码:1111 1000
“无符号右移2位”的补码:0011 1110
转换为真值:[0011 1110]补 = [0011 1110]反 = [0011 1110]原 = 62

显然这个结果出入很大,其错误原因就是我定义的这个管道长度不够导致的,按照我上边说的管道长度是按照数据类型来确定的,我程序里面都是java来写的,java中一个int占四个字节即32位。所以应该这样算:
a的原码:1000 0000 0000 0000 0000 0000 0000 1000
a的反码:1111 1111 1111 1111 1111 1111 1111 0111
a的补码:1111 1111 1111 1111 1111 1111 1111 1000
无符号右移2位的补码:0011 1111 1111 1111 1111 1111 1111 1110
转换成真值:[0011 … 1110]补 = [0011 … 1110]反 = [0011 … 1110]原 = 1073741822

这样就对了,你可能会问为什么上边的左移右移没问题呢?其实当你看到最终的原码就发现了其实中间都是0,可以省略的。还有疑问吗?好,最多再给你一个问题:为什么没有无符号左移呢?,其实我也想问一句:无符号左移和左移有区别吗?

至此移位运算分析完毕,你有没有发现一个规律:

  • 左移运算,相当于原值乘以2的n次方
  • 右移运算,相当于原值除以2的n次方

总结

本篇文章将位运算和移位运算的运算过程整体分析了一遍,二者其实都可以统一叫做位运算,总之就是以上七中规则,细心看来其实这些也都有规律可循。

当你看完之后是不是又惊喜又懵X,哈哈,虽然我们也知道有这样一个计算过程,但是开发过程中,无论是写代码还是看代码,总不能让我在私下先画一些小杠杠吧。其实位运算在计算机中的用途很广,拿异或来说,可用于数据的加密,并且是高度对称的,也可用于各类纠错码和校验码的编码…,虽然这些有代我们去深入开辟,但是在现阶段针对C++、java等上层语言主要用在哪些地方呢?下篇文章我会根据前两篇的思路叙说一下位运算在常规开发阶段的一个重要用途。

发布了47 篇原创文章 · 获赞 38 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/li0978/article/details/100714958