原码、反码、补码、位运算精讲,一篇就够

介绍

首先,在学习高级语言时,我们都有一个疑问,高级语言为什么还要保留位运算这种“低级”操作?我们都知道存储单元是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位二进制范围图

这张图把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代表关,利用无符号右移可以实现从左至右依次关灯的效果。

好了,有关位运算的知识就写到这里,如果不足之处请多包涵,喜欢的朋友帮忙点一下关注。原创不易,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容