二进制:基础、正负数表示、存储与运算

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huangzhilin2015/article/details/82594128

 

一、概述

众所周知,计算机是由各种电子元器件组成的,其中半导体可以通过它的开关状态来传递和处理信息,所以我们可以用1和0分别表示开和关两种状态。逻辑电路通常只有接通和断开两种状态,所以我们以二进制来处理会非常方便。所谓二进制表示从0开始,“逢二进一”(N进制则逢N进一)。比如十进制的0、1、2的二进制表示为0、1 、10。

二、进制转换

网上有很多进制转换的方法,我这里就不多做阐述,只介绍一个我自己常用的简便方法(自认为简便o(╯□╰)o)。都知道,二进制表示中,1:表示2^0(即十进制1),10:表示2^1(即十进制2),100:表示2^2(即十进制4)......依次类推,所以111=100+10+1=2^2+2^1+2^0=4+2+1=7。于是,我们得出如下方法:

二进制->十进制: \sum_{i=0}^{n}x{2^n},其中 n表示从低位往高位数的位数(从0开始),x表示对应位数上的值(1或者0),示例:101101=>1 * 2^0 + 0 * 2^1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 = 1+0+4+8+0+32=45

十进制->二进制:相当于二进制转换十进制的反推,比如要将十进制数N转换为二进制:第一步,找到一个最接近且小于等于N的为2的乘方的数m(即m=2^x,x为整数),记录下来,二进制对应x位上即为1,;第二步,对N-m重复步骤一,知道N-m==1或0。示例,将十进制数168转换为二进制:

1、如果等于1或0,则直接输出,反之进行下一步

2、找到最接近且小于等于168的2的乘方的数为128(2^7),所以第7位为1,记录下来(10000000);

3、然后对40(即168-128)重复0、1步骤,找到32(2^5),所以第5位为1,记录下来(10100000);

4、然后对8(即40-32)重复0、1步骤,找到8(2^3),所以第3位为1,记录下来(10101000);

5、然后对0重复1步骤(此时为0,直接输出0,表示第0位为0)

所以最终结果为:128+32+8 -> 10101000

运用熟练后,对不是特别大(这个多大才算大就因人而异了o(╯□╰)o)的十进制数都能做到快速的转换,比如237,可以不用动笔计算快速的得出结论:

237 -> 128+64+32+8+4+1 -> 然后从右往左直接写出结果:11101101

三、原码、反码、补码和存储

1、阐述

首先,前面我们讲了,计算机中只由1和0表示,当然包括我们常见的表示负数的符号"-",规定:一个有符号二进制数,其最高位为符号位,1表示负,0表示正,剩余位才是数值域。所以一个字节(8位),如果无符号可以表示2^8个(其范围为:0~2^8-1)不同的数(在计算机中可能是对应2^8个不同的状态),但是如果是有符号数,则需要牺牲最高位来表示符号位,所以虽然同样可以表示2^8个数,但是表示范围成了:-2^7 ~ 2^7-1。在此,对于原码、反码、补码,我先给出概念,下面再详细阐述,

正数:原码、反码、补码都是其本身

负数:原码=本身;反码=原码符号位不变,其它位取反;补码=反码+1,例:10010001:原=10010001 反=11101110 补:11101111

系统中二进制均以补码形式存在。

2、详解

我们用最高位表示符号位,的确能够在一定程度解决负数在计算机中的存储问题,但是运算时又发现了另一个问题。我们就简单的加减法运算举个例子:1 - 1 = 0,这个是毋庸置疑的,如果换成二进制(假设我们用8位存储),那么,-1的二进制为:10000001,1的二进制为:00000001,所以1 - 1 = -1 + 1 = 10000001 + 00000001 = 10000010 = -2,明显结果不对,所以符号位不能直接参与数的运算,而要处理又会非常麻烦;另外一方面,现在0这个数尴尬的出现了两种表现形式:00000000和10000000(分别为+0和-0),这没有什么意义。为了解决问题,人们机智的提出了补码的概念,接下来我们一步步阐述。

我们的程序员朋友在开发过程中,都会涉及到各式各样的数据类型,比如java中的byte、int、long等等(java中均为有符号数)。而且大家也熟知,byte占1个字节,int占4个字节,long占8个字节,这表示什么含义呢?举个例子,如果我们要把一个int类型的数强转为byte,可能会造成精度缺失,比如:130(int)在系统中的表示为:00000000 00000000 00000000 100000010,现在把130强转为byte,即执行如下操作:(byte)130,由于byte类型只能存储8位,所以结果只剩下低8位:100000010,而最高位表示符号位,这个明显已经是一个负数,现在我们按照进制转换规则求解:注意这个是补码形式,需要转换为对应的原码我们才好看出来是多少,按照原码-补码转换规则执行一次即可:100000010(补)->111111101(不是反码,只是一个中间数)->11111110(原码),对应十进制数为:-126,所以 ,System.out.println((byte) 130)的输出结果为:-126。

有了上面的介绍,我们再来看二进制的加法运算。一个8位二进制数,暂时先不考虑符号位,其能表示的最大的值为:11111111(255),如果现在在上述值的基础上加1,按照逢二进一的原则,则成了100000000,由于最多存储8位,所以结果成了:00000000,如果再加1,就成了00000001,依次类推。所以,在8位存储的背景下,如果我们从0开始一直不停的往上加1,其过程就会是这种形式:0,1,2...255,0,1,2,...255,0,1...,这样一直重复,是不是感觉一直在转圈圈?其实这个就和我们做模运算是一个道理。现在抛开二进制,举个简单的例子,咱们小学数学常见的分数,分母为4的分数:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}\frac{5}{\displaystyle 4}\frac{6}{\displaystyle 4}\frac{7}{\displaystyle 4}......等等可以表示为:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}1\frac{1}{\displaystyle 4}1\frac{2}{\displaystyle 4}1\frac{3}{\displaystyle 4}......,其中的1\frac{3}{\displaystyle 4}则表示:1 + \frac{3}{\displaystyle 4},即以4为周期过了1个周期,第二个周期到了四分之三处,表示余数,所以读作:一又四分之三,后面还会有二又四分之三、三又四分之三等等。

我们前面举的二进制例子和这个是同样的道理,11111111+1表示一个255周期满了,开始下一个周期,即"一个255又0",但是我们只能存储8位,前面那个表示1个255的1,已经到了第9位,被直接丢弃了,所以结果为00000000。接着分数的例子,如果我们模拟丢弃高位的处理逻辑,把前面的系数丢弃,只保留余数,则成了:\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}\frac{1}{\displaystyle 4}\frac{2}{\displaystyle 4}\frac{3}{\displaystyle 4}\frac{4}{\displaystyle 4}.......。明显看出来这是一个“循环”,然后我们再来看这个“循环”,我们要把\frac{3}{\displaystyle 4}通过运算得到\frac{2}{\displaystyle 4},有哪些方案?方案1:\frac{3}{\displaystyle 4} - \frac{1}{\displaystyle 4} ,即向后移动1步;方案2:\frac{3}{\displaystyle 4} +\frac{3}{\displaystyle 4} ,即向前移动3步。之前在其他地方看到过一个例子:时钟。时钟的小时计数都是1~12,。假设我们要把指针从5点转到6点,可以顺时针转1个小时单位,也可以逆时针转11个小时单位。时钟里的12在数学中表示模,而1和11则互为补数。所以,这类结构数据的减法都可以使用加法来代替。

再谈补码:

还是假设使用8位存储,参照上面的例子(无符号数),其能表示的最大的数为255,如果再加1,最高位被丢弃,回到了0,所以其模为2^8=256,这样我们就可以把模的概念运用到二进制的运算中来,具体就是运用补数。再看我们之前遇到的问题,1-1则可以看做是1+(1的补数),而1的补数=256-1=255即二进制形式的:11111111,所以1-1=00000001+11111111=100000000,最高位丢弃,结果为:00000000=0,这样计算就没有问题了。这里我们用11111111表示了-1,你可能已经看出来了,这就是-1(10000001)的补码。所以我们可以说,负数的补码,其实就可以看做是对应正数在当前模(n位,其模为2^n)下的补数(二进制形式)。

接下来我们回到有符号数

注:如果我们对一个正数的原码取反加一,就可以得到这个正数对应负数的补码,比如还是8位存储,十进制正数4,其二进制:00000100,取反:11111011,再加一:11111100,其 为 10000100(-4)的补码。

因此,一个8位二进制正数的补码范围为:00000001~01111111(1~127),负数的补码范围(可通过对应正数的原码取反加一求得)为:10000001~11111111(-127~-1),再加上00000000,所以我们能表示的范围成了10000001~01111111(-127~127)。这里等等,细心的朋友可能发现了,上面表示的补码范围中,10000000呢?明显不可能直接丢弃。其实,我们可以先按照二进制运算规则来算一算:-127-1等于多少,当然从数学的角度明显等于 -128,那么从8位二进制的角度考虑呢?即 -127 -1 = 10000001(-127的补码)+11111111(-1的补码)= 110000000需要舍弃最高位,最终也得到10000000。所以我们可以把10000000表示为-128。 这样还同时解决了+0 和-0的问题,保证了数据的完整性,运算逻辑也没有问题。所以执行:System.out.println((byte)(127+1)),你得到的结果会是:-128,执行:System.out.println((byte) -(127 + 1)),结果也是:-128。就像我们把时钟从12点开始,顺时针转动12个小时单位和逆时针转动12个小时单位得到的结果是一样的。

如果上面的还是不好理解,可以看看下面的例子(仅仅用作辅助理解):

一个8位存储的二进制数,我们用最高位表示符号位,数值域也就只剩下7位,能表示的最大的数就成了127(2^7-1),即模=2^7=128。我们定义从0开始,往前走一步为+1,往后退一步为-1,形成循环0,1,2...127,0,1,2...,周期为128步。然后,我们就可以开心的举例子了:

8 - 6 = 8 + (6的补数) = 8 + (128-6) = 8 + 122 = 130:表示在8的基础上往前走121步,得到1个128又2:结果=2(等价于8往后退2步可以得到6,即8-2=6);

6 - 8 = 6 + (8的补数) = 6 + (128 - 8) = 6 + 120 = 126:表示在6的基础上往前走120步,得到差2步到1个128:结果= -2(等价于6往前走2步可以得到8,即6+2=8),-2怎么表示呢?最高位表示为1即可表示负数:即10000010(原)。

这当然是非常“粗俗”的解释,我们把8-6和6-8的操作对应到计算机的实际运算过程:

1、8 - 6

首先,求得-6的补码,演示两种方式:

方式一(通过反码求补码):-6->10000110(原)->11111001(反)->11111010(补)

方式二(通过正数原码取反加一):6->00000110(原)->全部取反加一 ->11111010

然后和8(00001000)做加运算:00001000(补)+11111010(补)=1 00000010(补),最高位丢弃:00000010=2(正数,原码==补码)

2、6-8

首先,求得-8的补码:8->00001000(原)->全部取反加一->11111000

然后和6(00000110)做加运算:00000110(补)+11111000(补)=11111110(补),转换为对应的原码为:10000010(原) = -2

以上为本人个人理解,希望能帮到广大朋友,如有错误之处,恳请指正,谢谢!

 

猜你喜欢

转载自blog.csdn.net/huangzhilin2015/article/details/82594128