原码,反码,补码、移码

参考文章

参考文章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. 而且从硬件的角度上看,只有正数加负数才算减法。

  2. 正数与正数相加,负数与负数相加,其实都可以通过加法器直接相加。

原码,反码,补码的产生过程,就是为了解决,计算机做减法和引入符号位(正号和负号)的问题。

 

二、原码, 反码, 补码的基础概念和计算方法

对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.

 

1、原码

原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。

 

例如:

带符号的8位二进制:

[+1] = 0000 0001 = +1

[-1] = 1000 0001 = -1

 

若以带符号位的四位二进值数为例 

  1. 1010 : 最高位为‘1’,表示这是一个负数,其他三位为‘010’,

  2. 即(0*2^2)+(1*2^1)+(0*2^0)=2(‘^’表示幂运算符)

  3. 所以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

 

二进制减法运算

二进制减法如何借位
例如:100 - 1 = 4 - 1 = 3


二进制在减法运算时,当前一位为0就再向前借位。
直到不为零的位数,借1位当2算。

所以
  100
-   1

借位后相当于
  012  (实际二进制中没有2,这里需要自己体会一下)
-   1
----------
  011

即:3

 

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. 正数的补码等于他的原码
  2. 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+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]原= 1010_1011 ,[X]反=1101_0100,[X]补=1101_0101,[X]移=0101_0101
 

三、溢出

1、溢出的常用处理方法

用变形补码进行双符号位运算

使用双符号位法计算需要遵循以下两个规则:

  1. 两个符号位都看做数码一样参加运算;
  2. 两数进行以2^n+2为模的加法,即最高符号位上产生的进位要丢掉;
  3. 计算结果以符号位高位为结果符号
符号位结果
00 正数
01 正溢出
10 负溢出
11 负数

 

2、溢出解析

1.说到溢出,还是要先提一下自然丢弃。
请看下面这个例子:

  -3 - 6

  -3  -->   1101[补]

  -6  -->   1010[补]

  相加  --------

  结果 (1)0111

  结果的位数比原先的多出了一位,此处最左边的1,是会被自然丢弃的(就是不要了)。再看结果,对0111[补]=0111[原](也就是+7)。这和我们想要的-9有天壤之别。为什么会出现这个情况呢?

  原因就是这里出现了溢出!

首先来看溢出的定义:

  对一个N位有符号的二进制补码,其可以表达的范围是 [ - 2N-1 ~ 2N-1 - 1 ] 之间。如果超出这个范围就称为溢出了。

拿上面的-3-6来说,我们刚刚在计算时,转换为四位二进制补码(算上符号位),那么它的取值范围是-8~+7之间。而我们想要的结果是-9,比范围的最小值还要小,这个叫做负溢出。同理如果想要的结果比最大值还要大,那么就叫做正溢出,如取值范围是-8~+7之间,想要的结果是+9,那么就是正溢出。

说完了溢出的定义,我们来说说溢出的判定,就是怎么在计算开始时知道自己算的结果是不是溢出了?

采用双符号形式,我们再计算一次 -3-6
    11 101[补]

    11 010[补]

(1)10 111[补]

得到结果是 10 111[补](最高位超出位宽,自然舍弃),采用双符号运算得到的最后结果应该是单符号的,

就是:-3 = 11 101 前面双符号11代表负数, -6 = 11 010 前面双符号11代表负数,但是得到的结果10 111(发生负溢出),只有前面1是符号位,代表负数,真值是0 111

因此最后结果:10 111[补] = 1 0110[反] = 1 1001[原] = -9

 

四、反码与补码的原理

计算机中的数值是以补码形式存储的(只不过正数的补码跟原码一样。

强调原码,反码,补码的引入是为了解决做减法的问题。在原码,反码表示法中,我们把减法化为加法的思维是减去一个数,等于加上一个数的相反数,结果发现引入了符号位,却因为符号位造成了各种意向不到的问题。

反码+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]

 

posted @ 2020-09-06 11:02  我用python写Bug  阅读(1930)  评论(0编辑  收藏  举报