大家好,我是贠学文,点击右上方“关注”,每天为您分享java程序员需要掌握的知识点干货。
凌晨四点的北京
在学习java基础的时候,学到int类型的时候,我们都知道int类型是由32位二进制表示的,那么它的最小值,应该为-2^31-1,但是实际上,它的最小值却为-2^31,大家有没有想过,为什么会这样呢?这就与我们今天提到的计算机中二进制的原码、反码和补码有关。
我们都知道,在计算机内部,一切的数据都是以二进制的形式来存储的,而我们在有关二进制的术语中,经常会提到原码、反码、补码这三个概念,那这三种编码分别指的是什么呢,它存在的意义又是什么呢,今天就来详细讲解一下。
原码、反码、补码存在的意义原码:
这个理解起来很简单,就是二进制最原始的编码,由0和1组成,最高位为符号位,其它位为数值位,其中符号位0代码正数,1代表负数,比如用一个4位的二进制码表示 6:0110,-2:1010
补码:
在二进制的运算中,计算加法是非常简单的,但是如果计算减法,计算机的硬件电路设计会非常复杂(至于为什么会复杂,这是硬件的东西,我们不考虑),所以我们就想,是否可以让加法来取代减法运算,从而简化计算机的硬件电路设计呢?
我们都知道,在运算规则中,减去一个数,等于加上这个数的相反数,那么利用这个思路,我们就可以实现用加法来取代减法运算,从而简化计算机的硬件电路设计。但是在实际的操作过程中,发现用这种实现方式计算出来的结果是不正确的,至于为什么不正确,后面会说到。所以为了使结算结果正确,就为每一个二进制数,都设计出了对应的补码,然后在运算时和存储时,都是基于补码来运算和存储,这样计算出来的结果就是正确的。
反码:
在计算二进制原码对应的补码时,衍生出的一种处于中间状态的编码。它的存在,只是用来计算补码的。
原码、反码、补码的计算规则对于正数来讲,反码和补码都等同于原码本身。
对于负数来讲,
反码:符号位不变,数值位的每一位做按位取反
补码:对反码的值+1
举例:
一个4位的二进制数表示-6,那么它的原码:1110,反码:1001,补码:1010
-1:原码:1001,反码:1110,补码:1111
补码的计算规则的由来假如对于两个4位的二进制数,我们要计算5-2,那么我们可以转换为5+ (-2),那我们来计算一下他的结果
0101
1010
——–
1111
我们看到,计算出来的结果竟然是-7,这明显是错误的。至于为什么会出现这样的结果,原因也很简单,我们在把2的二进制转换为-2的二进制的时候,它的数值位没有变,只是把符号位由0变成了1,那我们在数值位做运算的时候,实际上就是做的5+2,然后符号位在做运算的时候,计算的结果负数,那么计算后的符号位与计算后的数值位结合起来,结果自然就是-7。这也就解答了前面说的为什么用加上一个数的相反数这种运算来取代减法时,计算结果是不正确的原因。
那么我们怎样才能计算出正确结果呢?
按照正常的情况下:A – B = C ,其中A和B均为正数且A>B,那么C一定是大于0并且小于A的。但是按照上面的原理来讲,A – B =A + (-B)= -(A + B),这样计算出来的结果,符号位一定是负数,并且数值位的结果一定大于A,这显示是不对的,我们预期的结果是,符号位为正数,并且数值位结果小于A,那怎样才能达到这个效果呢?这时我们就想到了补码的算法。
我们可以参考下时钟的原理,时钟最多只能表示12个数,所以时钟每次过12点以后,就相当于发生了溢出,溢出后,就会再次从1点开始计算。补码的原理:假如对于一个4位的二进制数来讲,它可以表示的正数的范围:0000-0111,即它0到7这8个数,负数的范围:1000-1111,即-0到-7这8个数,这时候我们发现0有两种表示方式,一种0000,一种1000,这种情况显然是不对的,我们后面再说怎么解决这种情况。那把能表示的正数的数量和能表示的负数的数量加到一起,即得到一个4位的二进制数,可以表示16个数。那我们把这16个数想象成一个时钟,我们做5-2,其实就相当于把时钟回拨了两个小时,但是我们如果不回拨两个小时,而是向后拨14个小时,可以达到同样的效果,只不过这时会发生溢出,溢出后,重新从-7开始计算。所以这时14就相当于-2的补码。我们可以把5+(-2)转换为5+14。
此时这个14,只是-2的补码的十进制编码,我们需要把十进制转换位二进制,但是,一个4位二进制数能表示的最大值为7,14显然已经超过了最大值,所以我们在将14转换成二进制时,一定会发生溢出,发生溢出后,我们把最高位舍弃,即得到了二进制的补码。14转换成二进制为 01110,那我们最高位的0舍弃,即1110,所以,-2的二进制补码为-6。
那么我们再来算下5+(-6)的结果
0101
1110
——–
10011
这时候我们发现发生了高位溢出,那我们再把最高位舍弃,即0011,结果为3,这时候的结果就是正确的了。
那么现在的问题来了,我们如何计算任意一个负数对于的补码呢?
首先我们说正数,因为补码主要是为了负数计算不准确的问题,但是对于正数来说,计算永远都是准确的。所以正数可以不用计算补码,直接做运算。我们也可以理解为正数的反码和补码都和原码保持一致。那么下面说的所有关于补码的运算,都是针对与负数来说。
我们先来定义几个变量:
S1 = 二进制能表示的范围的数量
S原码10 = 十进制原码
S原码10_符号 = 十进制原码符号位
S原码10_数值= 十进制原码数值位
S补码10 = 十进制补码
S补码10_符号 = 十进制补码符号位
S补码10_数值 = 十进制补码数值位
S原码2 = 二进制原码
S原码2_符号 = 二进制原码符号位
S原码2_数值= 二进制原码数值位
S补码2 = 二进制补码
S补码2_符号 = 二进制补码符号位
S补码2_数值 = 二进制补码数值位
从时钟的原理我们可以知道,S补码10=S1 –S原码10_数值。那么S1 怎么算呢?由上文我们可以知道,二进制能表示的正数的数量和能表示的负数的数量相同,我们我们只需计算能表示的正数的数量,在乘以2,就是整个二进制能表示的数的数量。那对于正数来说,能表示的最小的正数为0,能表示的最大的正数为2^(n-1)-1,所以,能表示的正数的数量 = 2^(n-1)-1 + 1 = 2^(n-1),所以S1=2^(n-1)* 2 = 2^n。所以,S补码10=S1 –S原码10_数值 = 2^n – S原码10_数值。
现在,S补码10已经计算出来了,我们在来计算S补码2,首先我们来计算一下S补码2_符号:
因为S原码10_数值的最大是为 2^(n-1)-1,所以S补码10的最小值=2^n-2^(n-1)+1=2^(n-1) + 1,它一定大于2^(n-1)-1。即S补码10,一定会大于二进制能表示的最大值,所以转换为二进制时,一定会发生溢出,并且次高位为1,溢出后,我们把最高位舍弃,这时,次高位就成了符号位,因为此时次高位为1,所以S补码2_符号一定为负数。
下面我们在来计算一下S补码2_数值:
由上文,我们知道,S补码10= 2^n – S原码10_数值,所以,S补码2_数值=2^n – S原码2_数值=2^0+2^1+2^2+……+2^n-1 + 1 – S原码2_数值 = (2^0+2^1+2^2+……+2^n-1 – S原码2_数值)+ 1 。
此时,我们可以认为2^0+2^1+2^2+……+2^n-1,相当于二进制的每一位都是1,那么它减去S原码2_数值时,就相当于对S原码2_数值的每一位都取反,即得到S原码2_数值的反码。所以,S补码2_数值 = S原码2_数值的反码 + 1。
所以,这也就证明了前面的结论:
对于负数来讲,
反码:符号位不变,数值位的每一位做按位取反
补码:对反码的值+1
补码还可以解决二进制中0的问题以及int类型为什么可以表示-2^31我们还是拿一个4位的二进制来举例:4位二进制能表示的负数的范围为:1111至1000,即-7到-0,而位二进制能表示的负数的范围为:0000至0111,就0至7,这时我们发现,出现了两个0,一个是1000,一个是0000,这显然是不行的。
通过上面我们知道,负数的反码是数值位不变,然后数值位按位取反,那么负数的反码可以表示的范围也是1000至1111,而补码是在反码的基础上+1,所以补码可以表示的范围为:1001至1111+1,就-1到-8。所以,通过补码,就完美的解决了这个问题。而int类型为什么可以表示-2^31这个问题,也同时就解释清楚了。
往期精彩:
作者介绍:
贠学文,具有多年经验的java开发工程师,业余时间利用头条分享技术知识点与自己对技术的感悟,帮助对自己未来感到迷茫的程序员,在技术上得到提升。结识一些志同道合的朋友,相互促进,共同进步。