参考文章
参考文章1
https://blog.csdn.net/zl10086111/article/details/80907428
作者:张子秋
出处:http://www.cnblogs.com/zhangziqiu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
参考文章2
https://blog.csdn.net/afsvsv/article/details/94553228
参考文章3
https://www.cnblogs.com/Jamesjiang/p/8947252.html
一、预备知识
在学习原码, 反码和补码之前, 需要先了解一些概念.
1、机器数
一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.
比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。
那么,这里的 00000011 和 10000011 就是机器数。
2、真值
因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3,而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
例:
0000 0001的真值 = +000 0001 = +1
1000 0001的真值 = –000 0001 = –1
3、原理
我们学习之前还得认识二进制,十六进制。会二进制与十进制的相互转化运算。
进制转换可以参考我的博客:https://www.cnblogs.com/Zzbj/p/10905744.html
由计算机的硬件决定,任何存储于计算机中的数据,其本质都是以二进制码存储。
根据冯~诺依曼提出的经典计算机体系结构框架。一台计算机由运算器,控制器,存储器,输入和输出设备组成。其中运算器,只有加法运算器,没有减法运算器。
所以,计算机中的没法直接做减法的,它的减法是通过加法来实现的。
你也许会说,现实世界中所有的减法也可以当成加法的,减去一个数,可以看作加上这个数的相反数。当然没错,但是前提是要先有负数的概念。这就为什么不得不引入一个该死的符号位。
-
而且从硬件的角度上看,只有正数加负数才算减法。
-
正数与正数相加,负数与负数相加,其实都可以通过加法器直接相加。
原码,反码,补码的产生过程,就是为了解决,计算机做减法和引入符号位(正号和负号)的问题。
二、原码, 反码, 补码的基础概念和计算方法
对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.
1、原码
原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。
例如:
带符号的8位二进制:
[+1]原 = 0000 0001 = +1
[-1]原 = 1000 0001 = -1
若以带符号位的四位二进值数为例
-
1010 : 最高位为‘1’,表示这是一个负数,其他三位为‘010’,
-
即(0*2^2)+(1*2^1)+(0*2^0)=2(‘^’表示幂运算符)
-
所以1010表示十进制数(-2)。
下图给出部份正负数数的二进制原码表示法
首先大概了解一下二级制加法运算
二进制运算规则:逢2进1 0+0=0,0+1=1,1+0=1,1+1=10 也就是当两个数相加的二进制位仅一位为1时,相加的结果为1; 如果两个二进制位全是0,相加的结果仍为0; 而如果两个相加的二进制位均为1,则结果为10(相当于十进制中的2) 例如: # 这里暂时不考虑符号 1010 + 0110 = 10 + 6 = 16 1010 + 0110 ---------- 10000 逢2进1(从右到左): 0+0=0 1+1=10 # 进位1,剩余0 0+1+1(这个1是进位) = 10 # 进位1,剩余0 1+0+1(这个1是进位) = 10 # 进位1,剩余0 1 # 最后进位得到的1 再比如 111 + 111 = 7 + 7 = 14 111 + 111 ---------- 1110 逢2进1(从右到左): 1+1=10 # 进位1,剩余0 1+1+1(这个1是进位)=10+1=11 # 进位1,剩余1,这种情况的运算规则是:先算原本的1+1=10,再算10+1(进位)=11 1+1+1(这个1是进位)=10+1=11 # 进位1,剩余1 1 # 最后进位得到的1
OK,原码表示法很简单有没有,虽然出现了+0和-0,但是直观易懂。 于是,我们高兴的开始运算。 0001+0010=0011 (1+2=3)OK 0000+1000=1000 (+0+(-0)=-0) 额,问题不大 0001+1001=1010 (1+(-1)=-2) 噢,1+(-1)=-2,这仿佛是在逗我呢。 于是我们可以看到其实正数之间的加法通常是不会出错的,因为它就是一个很简单的二进制加法。 而正数与负数相加,或负数与负数相加,就要引起莫名其妙的结果,这都是该死的符号位引起的。0分为+0和-0也是因他而起。 所以原码,虽然直观易懂,易于正值转换。但用来实现加减法的话,运算规则总归是太复杂。于是反码来了。
2、反码
我们知道,原码最大的问题就在于一个数加上他的相反数不等于零。
例如:
0000 0001 + 1000 0001 = 1000 0010 (1+(-1)=-2)
0000 0010 + 1000 0010 = 1000 0100 (2+(-2)=-4)
于是反码的设计思想就是冲着解决这一点,既然一个负数是一个正数的相反数,那我们干脆用一个正数按位取反来表示负数试试。
反码:
正数的反码还是等于原码
负数的反码就是他的原码除符号位外,按位取反。
例如:
带符号的8位二进制:
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
那么我们再试着用反码的方式解决一下原码的问题
把原码都转成反码进行计算,得到反码,把结果得到的反码再换算回原码即可
反码转换回原码:
正数:反码就是原码
负数:符号位不变,其他位按位取反
# 正数和负数相加 1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0 互为相反数相加等于0,解决,虽然是得到的结果是-0 1 - 2 = 1 + (-2) = [0000 0001]原 + [1000 0010]原 = [0000 0001]反 + [1111 1101]反 = [1111 1110]反 = [1000 0001]原 = -1 结果也是正确的 # 再试着做一下两个负数相加 (-1) + (-2) = [1000 0001]原 + [1000 0010]原 = [1111 1110]反 + [1111 1101]反 = [1 1111 1011]反 = [1 0000 0100]原 = -4 噢,好像又出现了新问题 (-1)+(-2)=(-4)?
看来相反数问题是解决了,但是却让两个负数相加的出错了。
但是实际两个正数相加和两个负数相加,其实都是一个加法问题,只是有无符号位罢了。而正数+负数才是真正的减法问题.
也就是说只要正数+负数不会出错,那么就没问题了。负数加负数出错没关系的,负数的本质就是正数加上一个符号位而已。
在原码表示法中两个负数相加,其实在不溢出的情况下结果就只有符号位出错而已(1001+1010=0011)
实际反码表示法其实已经解决了减法的问题,他不仅不会像原码那样出现两个相反数相加不为零的情况,而且对于任意的一个正数加负数的计算结果是正确的。
所以反码与原码比较,最大的优点,就在于解决了减法的问题。
但我们还有一个负数相加的问题,此时就需要用补码了。
3、补码
补码:
- 正数的补码等于他的原码
- 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
补码运算规则是:
- X+Y = [X]补 + [Y]补 = [X+Y]补 = 再转换为原码即可
- X-Y = [X]补 + [-Y]补 = [X-Y]补 = 再转换为原码即可
- -X-Y = [-X]补 + [-Y]补 = [-X-Y]补 = 再转换为原码即可
例如:
正数和负数相加: 1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [1 0000 0000]补 = [0 0000 0000]原 = 0 两个负数相加: (-1) + (-2) = [1000 0001]原 + [1000 0010]原 = [1111 1111]补 + [1111 1110]补 = [1 1111 1101]补 = [1 1111 1100]反 = [1 0000 0011]原 = -3 # 由此可见,补码即解决了符号问题,也解决了负数加负数的情况
4、移码
移码(又叫增码或偏置码)通常用于表示浮点数的阶码,其表示形式与补码相似,只是其符号位用“1”表示正数,用“0”表示负数,数值部分与补码相同。
X=-101011 , [X]原= 1010_1011 ,[X]反=1101_0100,[X]补=1101_0101,[X]移=0101_0101
三、溢出
1、溢出的常用处理方法
用变形补码进行双符号位运算(正数符为00,负数符号以11)
若运算结果的符号位为:
00 正数无溢出
01 正溢出(但是也是正数)
10 负溢出(但是也是负数)
11 负数无溢出
2、溢出解析
1.说到溢出,还是要先提一下自然丢弃。
请看下面这个例子:
-2 - 6
-2 --> 1110[补]
-6 --> 1010[补]
相加 --------
结果 (1)1000
结果的位数比原先的多出了一位,此处最左边的1,是会被自然丢弃的(就是不要了)。再看结果,对1000[补]=0000[原](也就是0)。这和我们想要的-8有天壤之别。为什么会出现这个情况呢?
原因就是这里出现了溢出!
首先来看溢出的定义:
对一个N位二进制补码,其可以表达的范围是 - 2N-1+1 ~ 2N+1 - 1之间。如果超出这个范围就称为溢出了。
拿上面的-2-6来说,我们刚刚在计算时,转换为二进制补码是4位的。它的取值范围是-7~+7之间。而我们想要的结果是-8,比范围的最小值还要小,这个叫做负溢出。同理如果想要的结果比最大值还要大,那么就叫做正溢出,如取值范围是-7~+7之间,想要的结果是+8,那么就是正溢出。
说完了溢出的定义,我们来说说溢出的判定,就是怎么在计算开始时知道自己算的结果是不是溢出了?
采用双符号形式,我们再计算一次 -2-6
11 110[补]
11 010[补]
(1)11 000[补]
得到结果是 11 000[补](最高位超出位宽,自然舍弃),采用双符号运算得到的最后结果应该是单符号的,
就是:-2 = 11 110 前面双符号11代表负数, -6 = 11 010 前面双符号11代表负数,但是得到的结果11 000 只要前面1是符号位,代表负数,真值是1 000,
因此最后结果:11 000[补] = 11 000[原] = -8
再比如:
-5-3(按8位算),11 111 1011[补] + 11 111 1101[补] = (1)11 111 1000[补]
双符号位的话,计算结果以符号位高位为结果符号,即 11,发生了负溢出。
11 111 1000[补] = 1 0000 1000[原] = -8
四、反码与补码的原理
计算机中的数值是以补码形式存储的(只不过正数的补码跟原码一样。
强调原码,反码,补码的引入是为了解决做减法的问题。
在原码,反码表示法中,我们把减法化为加法的思维是减去一个数,等于加上一个数的相反数,结果发现引入了符号位,却因为符号位造成了各种意向不到的问题。
反码+1 ,它只是补码的另外一种求法,不是补码的定义。
1、补码的原理
为了便于理解可以用时钟计算(12小时制的)
9要拨到5,可以减4,也可以加8 ,所以此时 -4和+8是等价的 。
那么 ,我们可以认为 8是4的补码
那么这两个数有什么联系呢? 没错他们和在一起就绕了时钟一圈,用数学表达的话就是 两数的相加的绝对值 为 12
类比到补码:首先的明白一圈是多少 ?假设机器一圈为128(2的7次方)
于是用128减真值 也就是所谓的 数值位取反,末位加一 的操作了。
再回到时钟, 9-4=9+[4]补=(9+8)%12 因为他会多走一圈,所以我们再在这里还要 %12,而计算机补码是有周期的,不用这一步。
也就是,当前9点,我们希望拨到5点,做法如下
1. 往回拨4个小时: 9 - 4 = 5
2. 往前拨8个小时: (9 + 8) mod 12 = 5
3. 往前拨8+12=20个小时: (9+20) mod 12 =5
2,3方法中的mod是指取模操作, 17 mod 12 =5 即用17除以12后的余数是5.
所以钟表往回拨(减法)的结果可以用往前拨(加法)替代!
现在的焦点就落在了如何用一个正数, 来替代一个负数. 上面的例子我们能感觉出来一些端倪, 发现一些规律. 但是数学是严谨的. 不能靠感觉.
首先介绍一个数学中相关的概念: 同余
两个整数a,b,若它们除以整数m所得的余数相等,则称a,b对于模m同余
记作 a ≡ b (mod m)
读作 a 与 b 关于模 m 同余。
举例说明:
4 mod 12 = 4
16 mod 12 = 4
28 mod 12 = 4
所以4, 16, 28关于模 12 同余.
负数取模
-1 mod 4 = (-1 + 4*n) mod 4,n取正整数,一直到括号里的数不为负数。
例如:
(-2) mod 12 = 12-2=10
(-4) mod 12 = 12-4 = 8
(-5) mod 12 = 12 - 5 = 7
再回到时钟的问题上:
回拨2小时 = 前拨10小时
回拨4小时 = 前拨8小时
回拨5小时= 前拨7小时
注意, 这里发现的规律!
结合上面学到的同余的概念.实际上:
(-2) mod 12 = 10
10 mod 12 = 10
-2与10是同余的.
(-4) mod 12 = 8
8 mod 12 = 8
-4与8是同余的.
距离成功越来越近了. 要实现用正数替代负数, 只需要运用同余数的两个定理:
反身性:
a ≡ a (mod m)
这个定理是很显而易见的.
线性运算定理:
如果a ≡ b (mod m),c ≡ d (mod m) 那么:
(1)a ± c ≡ b ± d (mod m)
(2)a * c ≡ b * d (mod m)
如果想看这个定理的证明, 请看:http://baike.baidu.com/view/79282.htm
所以:
7 ≡ 7 (mod 12)
(-2) ≡ 10 (mod 12)
7 -2 ≡ 7 + 10 (mod 12)
现在我们为一个负数, 找到了它的正数同余数. 但是并不是7-2 = 7+10, 而是 7 -2 ≡ 7 + 10 (mod 12) , 即计算结果的余数相等.
接下来回到二进制的问题上, 看一下: 2-1=1的问题.
2-1=2+(-1) = [0000 0010]原 + [1000 0001]原= [0000 0010]反 + [1111 1110]反
先到这一步, -1的反码表示是1111 1110. 如果这里将[1111 1110]认为是原码, 则[1111 1110]原 = -126, 这里将符号位除去, 即认为是126.
发现有如下规律:
(-1) mod 127 = 126
126 mod 127 = 126
即:
(-1) ≡ 126 (mod 127)
2-1 ≡ 2+126 (mod 127)
2-1 与 2+126的余数结果是相同的! 而这个余数, 正式我们的期望的计算结果: 2-1=1
所以说一个数的反码, 实际上是这个数对于一个膜的同余数. 而这个膜并不是我们的二进制, 而是所能表示的最大值! 这就和钟表一样, 转了一圈后总能找到在可表示范围内的一个正确的数值!
而2+126很显然相当于钟表转过了一轮, 而因为符号位是参与计算的, 正好和溢出的最高位形成正确的运算结果.
既然反码可以将减法变成加法, 那么现在计算机使用的补码呢? 为什么在反码的基础上加1, 还能得到正确的结果?
2-1=2+(-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]补 + [1111 1111]补
如果把[1111 1111]当成原码, 去除符号位, 则:
[0111 1111]原 = 127
其实, 在反码的基础上+1, 只是相当于增加了膜的值:
(-1) mod 128 = 127
127 mod 128 = 127
2-1 ≡ 2+127 (mod 128)
此时, 表盘相当于每128个刻度转一轮. 所以用补码表示的运算结果最小值和最大值应该是[-128, 128].
但是由于0的特殊情况, 没有办法表示128, 所以补码的取值范围是[-128, 127]