介绍
首先,在学习高级语言时,我们都有一个疑问,高级语言为什么还要保留位运算这种“低级”操作?我们都知道存储单元是CPU访问存储器的最基本单位,每个存储单元由8个二进制位组成,每个二进制位能存一个0或1。这就是为什么说存在计算机里面的数据都是0和1组成的原因。位运算就是直接去操作存储单元里面的二进制位,没有中间商赚差价,显然这种底层操作的效率是普通的运算符操作方式远远无法比拟的。在讲位运算之前,咱们要先了解一些其它相关的知识。
进制
对于程序员来说,进制是一个非常重要的概念。进制是进位计数制的简称,是为了定义带进位的计数方法(有不带进位的计数方法,比如原始的结绳计数法,常用的“正”字计数法)。对于任何一种进制,例如"X"进制,就表示每一位置上的数运算时都是逢"X"进一位。 十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,以此类推,"X"进制就是逢"X"进位。我们日常使用最多的进制就是十进制,这也是直观上最容易理解的方式。但是计算机内部采用的却是二进制来存储数据,那为什么计算机要采用二进制呢?
1、技术上容易实现。用双稳态电路表示二进制数字0和1是很容易的事情。
2、可靠性高。二进制中只使用0和1两个数字,传输和处理时不易出错,因而可以保障计算机具有很高的可靠性。
3、运算规则简单。与十进制数相比,二进制数的运算规则要简单得多,这不仅可以使运算器的结构得到简化,而且有利于提高运算速度。
4、与逻辑量相吻合。二进制数0和1正好与逻辑量“真”和“假”相对应,因此用二进制数表示二值逻辑显得十分自然。
5、二进制数与十进制数之间的转换相当容易。人们使用计算机时可以仍然使用自己所习惯的十进制数,而计算机将其自动转换成二进制数存储和处理,输出处理结果时又将二进制数自动转换成十进制数,这给工作带来极大的方便。
--参考资料来源:百度百科-二进制数据
进制之间的转换其实都有一套固定的公式来操作,但是做为一个经常用脑过度的程序员就没必要浪费脑细胞去套公式换算了,下面咱直接用java代码演示进制之间的转换:
//十进制转其它进制
System.out.println(Integer.toBinaryString(10));// 转二进制
System.out.println(Integer.toOctalString(10));// 转八进制
System.out.println(Integer.toHexString(10));// 转十六进制
// 其它进制转十进制
System.out.println(Integer.parseInt("10001",2)); //二进制转十进制
System.out.println(Integer.parseInt("21",8)); //八进制转十进制
System.out.println(Integer.parseInt("11",16)); //十六进制转十进制
原码、反码、 补码
我们知道,当我们在进行十进制的运算时,感觉是如此的自然而然,给你两位数的加减法,基本上是秒解。但是,计算机和真实生活中不同,一个数在计算机中只能以二进制(0或者1)的方式表示。而且计算机的运算器是不能做减法的,至于原因,据说是因为减法器的硬件成本太大,所以废弃了,而且减法也是可以通过加法来实现的。例如,1-1=0也可以写成1+(-1)=0,前提是计算机也要有负数的概念。于是就规定数值的最高位为符号位,正数为0,负数为1。比如,一个byte类型的十进制数,刚好是占一个字节,计算机字长为8位。假如这个数字是5,转换成二进制就是00000101。如果这个数是 -5 ,就是 10000101 。
原码
何为原码?原码即“原来的编码”,哈哈,这是我理解的意思,继续推测“原来的编码”在使用中遇到了什么不好解决的问题,所以后面又推出了反码和补码。不管是什么码,它都是机器存储一个具体数字的编码方式。
原码就是符号位加上该数的绝对值,即用第一位表示符号,其余位表示值。也是我们最容易理解的一个编码,例如一个8位的二进制数用原码表示:
//数字"3"的二进制原码表示
00000011
//数字"-3"的二进制原码表示
10000011
//第一位为符号位,所以8位的二进制数原码的取值范为
[11111111,01111111]即十进制表示为[-127,127]
原码虽然很好理解,但是我们知道,计算机只会做加法,下面分别来演示一下1+1和1-1用原码怎么计算
//二进制原码计算1+1
00000001+00000001 = 00000010(二进制) =2(十进制)
//二进制原码计算1-1即1+(-1)
00000001+10000001 = 10000010(二进制) =-2(十进制) //结果显然不对
如果用二进制原码来计算减法,结果是不正确定的,所以现在计算机内部都不是用一个原码来表示一个数。为了解决原码做减法的问题,这时就发明了反码。
反码
正数的反码和原码是一样的,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反,例如一个8位的二进制数用反码表示并计算减法:
//数字"3"的二进制反码表示
00000011
//数字"-3"的二进制反码表示
11111100
//二进制反码计算3-3即3+(-3)
00000011+11111100 = 11111111(反码)=10000000(原码)=-0(十进制)
我们发现用反码计算减法能够得到正确的值,但是,还有一点美中不足的是"10000000"和"00000000"都可以表示0,-0也是0,这对追求完美的人类来说是无法容忍的事情,怎么能允许两个编码代表一个数?这是暴殄天物的行为。于是又发明了补码。
补码
正数的补码和原码一样,负数的补码是在反码的基础上加1,等等?感觉哪里不对,为什么补码等于反码加1?这是什么套路?其实这样描述只是方便大家对补码的计算而已,但实质并没有这么简单。实际上补码定义公式为:
//补码定义公式
-x = 2^w - x
//w表示x是一个多少位的数,假如-x=-5是一个8位的数,用二进制补码表示
-5 = 2^8 - 5 = 100000000 - 00000101 = 11111011(补码)
//反码的基础上加1
-5 = 10000101(原码) = 11111010(反码) = 11111011(补码)
我们发现,不管是采用公式还是用反码加1的算法,结果都是一样的。那这个公式是怎么推导出来的呢?我们知道一个负数可以用0减去这负数对应的正数得到,例如:
//一个负数等于0减去这个数对应的正常
-3 = 0 - 3
//表达式换算成4位的二进制为
-3 = 1011(原码) = 0000 - 0011
//但是0000不够减,当不够减时,就只能向前借一位,就变成了
-3 = 1011(原码) = 10000 - 0011
//借位后就可以减了
-3 = 1011(原码) = 10000 - 0011 = 1101(补码)
//这样我们可以推导出公式,其中4代表位数w,3代表操作x
-3 = 2^4 - 3 = 10000 - 0011 = 1101(补码)
补码公式是推导出来了,但是我们还有一个疑问,就是当0000不够减时,为什么可以向前借一位。为了方便大家的理解,我画一张能表示4位二进制原码的范围图:
这张图把4位二进制原码的范围的所有正负数用一个圆圈表示,最小的数是-7,最大的数是7。比-7(1111)更小的是-8(10000),但是4位的二制原码最多只能保存4位,则高位的1会被舍弃,所以10000 = 0000。所以0000在做减法时,向前借一位就是0000->10000。哈哈,这不是官方解释,这只是我个人理解,如有错误之处,请大家指正。注意,这张图并不是计算机存储二进制数据的真实情况,计算机存储二进制数据是采用补码的方式存储的,而我画的是二进制原码,所以大家不要被误导了。
补码理解起来虽难困难,但用起来是真的香,现在我来举几个简单的例子:
//采用4位二进制补码计算7-3,注意计算机只会算加法
7-3 = 7 + (-3)= 0111 + 1011(原码) = 0111 + 1101(补码) = 10100(舍弃一位) = 0100 = 4
//采用8位二进制补码计算5-9
5-9 = 5 + (-9)= 00000101 + 10001001(原码) = 00000101 + 11110111(补码) = 11111100(补码) = -4
//采用8位二进制补码计算1-1
1-1 = 1 + (-1)= 00000001 + 10000001(原码) = 00000001 + 11111111(补码) = 00000000 = 0
//计算-127-1
-127 - 1 = -127 + (-1) = 11111111(原码)+10000001(原码)=10000001(补码)+11111111(补码)=10000000(补码) = -128
看到没?采用补码计算都不用考虑符号位,直接拿两个操作数相加得出结果,并且解决反码00000000和10000000都代表0的尴尬,在补码里面00000000表示0,而10000000表示-128,这就是为什么byte类型的取值范围是-128~127的原因。好吧,我承认,这算是天才的设计吧。对于计算机来说,采用补码的方式来设计显然更容易且节省成本。所以现在几乎所有计算机的存储器都是采用补码方式存储数据的。
位运算
java中常用的位运算符如下表:
操作符 | 描述 |
---|---|
& | 如果两操作数相对应位都是1,则结果为1,否则为0 |
| | 如果两操作数相对应位都是 0,则结果为 0,否则为1 |
^ | 如果两操作数相对应位值相同,则结果为0,否则为1 |
〜 | 按位取反运算符,翻转操作数的每一位,即0变成1,1变成0 |
<< | 左移运算符,将运算符左边的操作数向左移动运算符右边指定的位数(在低位补0) |
>> | "有符号"右移运算符,将运算符左边的操作数向右移动运算符右边指定的位数。使用符号扩展机制,也就是说,如果值为正,则在高位补0,如果值为负,则在高位补1 |
>>> | "无符号"右移运算符,将运算符左边的对象向右移动运算符右边指定的位数。采用0扩展机制,也就是说,无论值的正负,都在高位补0 |
灵活使用位运算,不仅可以大大提高程序的执行效率,还可以理解计算机底层的运算逻辑。由于工作中业务的局限性,平时工作中用的比较少,但是在阅读某些大神的源码时可以经常看到位运算操作,为了成为技术大神,接下来咱们就好好学习一下吧。
&(位与)
运算规则:如果两操作数相对应位都是1,则结果为1,否则为0
例如:0&0=0、0&1=0、1&0=0、1&1=1
计算:13 & 27 = 9
00001101 ->13
&
00011011 ->27
=
00001001 ->9
13 & 27 = 00001101 & 00011011 = 00001001 = 9
应用:判断奇偶数
n&1=0 ->偶数
n&1=1 ->奇数
|(位或)
运算规则:如果两操作数相对应位都是 0,则结果为 0,否则为1
例如:0|0=0、0|1=1、1|0=1、1|1=1
计算:13 | 27 = 31
00001101 ->13
|
00011011 ->27
=
00011111 ->31
13 | 27 = 00001101 | 00011011 = 00011111 = 31
应用:判断用户权限,假如一个用户拥有角色1和角色2两个身份,操作权限用二进制字符串表示,每一位代表一个权限
角色1权限->10010010
角色2权限->00101011
用户拥有的权限->10111011
用户权限 = 角色1权限 | 角色2权限 = 10010010 | 00101011 = 10111011
^(位异或)
运算规则:如果两操作数相对应位值相同,则结果为0,否则为1
例如:0^0=0、0^1=1、1^0=1、1^1=0
计算:13 ^ 27 = 31
00001101 ->13
^
00011011 ->27
=
00010110 ->22
13 ^ 27 = 00001101 ^ 00011011 = 00010110 = 22
应用:交换两个值,不用临时变量
a = a^b
b = a^b
a = a^b
〜(位取反)
运算规则:按位取反运算符,翻转操作数的每一位,即0变成1,1变成0
例如:〜0=1、〜1=0
计算:~13
~
00001101 ->13
=
11110010 ->-14 //注意:计算机存储器都是采用补码的方式存储数据
应用:求相反数
~a+1
<<(左移运算符)
运算规则:左移运算符,将运算符左边的操作数向左移动运算符右边指定的位数(在低位补0)
例如:3 << 2 = 12
3 << 2 = 00000011 << 2 = 00001100 = 12
应用:求a*2^x可以用a<<x来代替,效率更高
3*2^3 = 3<<3 = 00000011 << 3 = 00011000 = 24
>>(有符号右移运算符)
运算规则:"有符号"右移运算符,将运算符左边的操作数向右移动运算符右边指定的位数。使用符号扩展机制,也就是说,如果值为正,则在高位补0,如果值为负,则在高位补1
例如:11 >> 2 = 2
11 >> 2 = 00001011 >> 2 = 00000010 = 2
例如:-11 >> 2 = -3
-11 >> 2 = 1111 0101 >> 2 = 11111101 = -3
应用:求a/2^x可以用a>>x来代替,效率更高
8/2^2 = 8>>2 = 00001000 >> 2 = 00000010 = 2
>>>(无符号右移运算符)
运算规则:"无符号"右移运算符,将运算符左边的对象向右移动运算符右边指定的位数。采用0扩展机制,也就是说,无论值的正负,都在高位补0
例如:11 >>> 2 = 2 (正数和有符号右移运算符"<<"效果一样)
11 >>> 2 = 00001011 >>> 2 = 00000010 = 2
例如:-11 >>> 2 = -3
-11 >>> 2 = 1111 0101 >>> 2 = 00111101 = 61
无符号在移动无关数值的东西时还是非常有用的,例如,用二进制位0和1表示串灯的开关,1代表开,0代表关,利用无符号右移可以实现从左至右依次关灯的效果。
好了,有关位运算的知识就写到这里,如果不足之处请多包涵,喜欢的朋友帮忙点一下关注。原创不易,谢谢!