位运算的那些事(一)搞懂机器码

最近在开发过程中查看Android源码,多处看到一些类似result = specSize | MEASURED_STATE_TOO_SMALL;的写法,乍一看很熟悉,实际阅读起来很痛苦,这是我们大学里学过的位运算,单看代码似乎我们不可能一瞬间知道结果是多少,所以千万要和我们常见的result = a || b区分开来。以此为引子我们就了解一下有关位运算的那些事。

位运算主要针对二进制,它包括了:“与(&)”、“或(|)”、“非(~)”、“异或(^)”。从表面上看似乎有点像逻辑运算符,但逻辑运算符是针对两个关系运算符来进行逻辑运算,而位运算符主要针对两个二进制数的位进行逻辑运算。

理解位运算,必须先了解二进制在计算机中转换过程,这也是本篇所讲的重点内容。这些明白了,针对一些复杂的简单的运算法则也就很清晰了。

机器码和真值

在谈二进制之前我们先了解两个简单的概念:“机器码”和“真值”。

机器码

一个数在计算机中的二进制表示形式,叫做这个数的机器码。机器码是带符号的,在计算机用一个数的最高位存放符号,正数为0,负数为1。比如,十进制中的数+1,计算机字长为8位,转换成二进制就是00000001,如果是-1 ,就是10000001。那么,这里的00000001和10000001就是机器码。

真值

在机器码中第一位是符号位,但是我们抛去这个规则,就得到不知正确的数值了,还是上边的例子-1,用机器码来表示为10000001,但如果表示成十进制的真值则等于129,这差别就大了。或者你可以总结真值转换成二进制和机器码的区别就是有没有符号之说,符号你自己定义。

原码, 反码, 补码

上边有了机器码和真值的概念,从这里开始我们就揭开计算机是如何将“-1”这类真值转换成机器码参与计算或存储起来的。首先我们要明白二进制数在内存中是以补码的形式存放的,这里又引申出原码反码补码的概念,也是二进制位运算的基础,后边会说。

原码

前边我们已经说到机器码是带符号的,所以我们的真值转换成机器码的时候要根据其符号正或者负分别将其转成首位的0和1。

例:
[+1] = [0000 0001]原
[-1] = [1000 0001]原

注:这里按照单个字节来表示,java上int类型是四个字节,就不用过于纠结了。

因为第一位是符号位, 所以8位二进制数的取值范围就是:[11111111, 01111111],即[127, -127]。

反码

同样,反码也是区分符号的,正数的反码是他本身, 负数的反码是符号位不变其余按位取反

例:
[+1] = [0000 0001]原 = [0000 0001]反
[-1] = [1000 0001]原 = [1111 1110]反

补码

和上边一样,补码也是区分符号的,正数的补码是他本身,负数的补码是在反码的基础上加1

例:
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补

由上边三个概念我们得到两个结论:

  1. 正数的原码、反码、补码都一样,负数根据原、反、补规则做变换
  2. 计算机将一个真值最终存储或参与计算是需要经过这几步:真值->原码->反码->补码

计算机中为什么要以补码形式参与运算或存储

我们都知道,CPU是由运算器、寄存器、总线构成。单说运算器说的简单点就是有一个个小开关构成的集成器,每一个开关的状态代表0和1,每一次运算都是有由时钟脉冲将这些状态带出来或存储在寄存器中…(不好意思,扯远了)。对于一个有符号的真值转换成机器码做运算,人的大脑很轻松的就能判断处理,但是如果让计算机集成器中一个个小开关去保存这些状态,那么每个集成器上还要预留一个开关做为正负,并且其余开关进行转换的过程中还要兼顾符号开关的状态,这样是不是很繁琐。

聪明的人们在想能不能将真值中的符号位直接参与到运算呢?这样就可以不用在硬件中专门预留一个这样的符号开关标识了。我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了。

例如计算十进制的表达式: 1-1=0,以下循序渐进给出计算机为什么最终采用补码形式计算的原因。

原码计算

上边表达式用原码计算:

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

如果用原码表示, 让符号位也参与计算,显然对于减法来说,结果是不正确的.这也就是为何计算机内部不使用原码表示一个数的原因。

反码计算

为了解决原码做减法的问题, 聪明的人们又想到了反码计算:

1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在"0"这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0.

补码计算

补码的出现, 彻底解决了0的符号以及两个编码的问题:

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原

这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了,而且可以用[1000 0000]表示-128:

(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补

-1-127的结果应该是-128,在用补码运算的结果中,[1000 0000]补就是-128。但是注意因为实际上是使用以前的-0的补码来表示-128, 所以-128并没有原码和反码表示。(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原,这是不正确的)。

使用补码, 不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数(多开辟一个存储空间)。这就是为什么8位二进制使用原码或反码表示的范围为[-127, +127],而使用补码表示的范围为[-128, 127]。

总结

本篇系统了阐述了一个真值转化到计算机所最终使用的机器码的一个过程。一个真值最终被计算机所使用会经过真值->原码->反码->补码这些步骤,但如果计算机计算完之后呢,当然要逆过来走一遍,由补码->反码->原码->真值这个过程最终得到一个被人脑所认知的正确结果。

开篇提到了二进制的位运算,但是不摸清原码、反码、补码这些基础很难去得到位运算的正确结果的。下一篇我会以本篇的基础来讲一下二进制位运算相关运算符的运算规则。

参考

  • cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html
  • https://www.zhihu.com/question/20159860
发布了47 篇原创文章 · 获赞 38 · 访问量 5万+

猜你喜欢

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