由于觉得机器码中“原码”、“反码”和"补码"这样的概念过于蛋疼,参考一些博客和问答之后,决定调整优化内容便于回顾,参考链接已经放在文章底部。如果内容有误,或者你有独到的见解,也欢迎你提出来。
二进制的概念
大家都知道尺子吧,小学开始就在使用的测量距离的工具。现在我允许你以厘米(cm)为唯一的单位,制作一定长度的尺子。例如,我可以制作 1cm 的尺子、2cm 的尺子,那么我可以用这两种长度的尺子作为刻度来测量三种长度:1cm、2cm 和 3cm(3=1+2)。有人会问为什么我不能先量 1cm 再量 1cm,逐渐累积到指定的目标呢?在这里我们规定:每个尺子在一次测量中无法重复使用多次。
有了以上的概念,我们来考虑一个问题:
至少需要制作多少把尺子,可以表示从 0 ~ 15 cm 之间的任意厘米长度?(当然,0 在实际中不需要考虑)
答案是 4 把,分别是 1cm,2cm,4cm 和 8cm. 不信?我们在下面的表格中列举出所有的情况,其中如果一个尺子被使用了,我们用 1 作为标记,没被使用则用 0 作为标记。
长度
8cm
4cm
2cm
1cm
二进制
0
0
0
0
0
0000
1
0
0
0
1
0001
2
0
0
1
0
0010
3
0
0
1
1
0011
4
0
1
0
0
0100
5
0
1
0
1
0101
6
0
1
1
0
0110
7
0
1
1
1
0111
8
1
0
0
0
1000
9
1
0
0
1
1001
10
1
0
1
0
1010
11
1
0
1
1
1011
12
1
1
0
0
1100
13
1
1
0
1
1101
14
1
1
1
0
1110
15
1
1
1
1
1111
可经过验证,这的确是这个问题的最优解。
继续思考这个问题,如果将长度的表示范围增加到 10000,需要多少把尺子?这篇博文不是为了详解二进制,这里只给大家一个直观的理解。
计算机中的二进制
早期的电子元件只能表示开(1)和关(0)两种状态,所以计算机就采用了“二进制”,这样设计的可行性很高,比如开关的接通和断开、晶体管的导通和截止、磁原件的正负剩磁、电位电平的高低等都可以表示 1 和 0 两个数。另外使用二进制运算,规则简单,可靠性高。
原码、反码和补码
先简单引用一下书上的概念:
原码表示法是机器数的一种简单的表示法。其符号位用 0 表示正号,用 1 表示负号,数值一般用二进制形式表示。
机器数的反码可由原码得到。如果机器数是正数,则该机器数的反码与原码一样;如果机器数是负数,则该机器数的反码是对它的原码(符号位除外)各位取反而得到的。
机器数的补码可由原码得到。如果机器数是正数,则该机器数的补码与原码一样;如果机器数是负数,则该机器数的补码是对它的原码(符号位除外)各位取反,并在未位加1而得到的。
好像多了一些概念,比如之前提到的二进制,以及看上去很玄乎的“符号位”。补码里面取反加一的操作,对于机器来说实在方便。如果单单地记住结论,拿去做题,当然没有问题,但想要了解一个知识的细节,最好还是追根溯源,搞清楚相应的概念是如何产生的。
原码的产生
回想一下之前尺子的例子,在自然界中,测量长度永远是正数,不可能存在长度为负的物体。我们数柴犬的时候,也是一只柴犬、两只柴犬,诸如此类。所以在计算机中存储的数字,都是没有符号的(unsigned)。但如果有人欠了你一些钱,该如何表示欠债的数量呢?人们为了面对这种情况,发明了正负符号,因此符号的概念很自然地被引入了计算机的数值表示。人们选择在最高位补上一个“符号位”,0 表示正号,1 表示负号。
正数
原码
负数
原码
0
0000
-0
1000
1
0001
-1
1001
2
0010
-2
1010
3
0011
-3
1011
4
0100
-4
1100
5
0101
-5
1101
6
0110
-6
1110
7
0111
-7
1111
所谓原码就是机器数,是加了一位符号位的二进制数,正数符号位为 0,负数符号位为 1,计算机中存储、处理、运算的数据通常是 8 位、16 位、32 位或 64 位的,这里以最简单的 4 位为例讲解。注意符号位是包含在 4 位中的其中 1 位,故可直观读出的数只有 3 位(只有后 3 位数可以按权展开)。
反码的产生
这里需要指出:计算机里面,只有加法器,没有减法器,所有的减法运算,都必须用加法进行。即:减去某个数字(或者说加上某个负数)的运算,都应该研究如何用加法来完成。根据原码的表示,1(0001) + -1(1001) = -2 (1010),而实际结果应该为 0,这样的设计存在着问题。为了使得相反数相加结果为 0,人们发明了“反码”。反码表示方式是用来处理负数的,符号位置不变,其余位置相反。
正数
原码/反码
负数
原码
反码
0
0000
-0
1000
1111
1
0001
-1
1001
1110
2
0010
-2
1010
1101
3
0011
-3
1011
1100
4
0100
-4
1100
1011
5
0101
-5
1101
1010
6
0110
-6
1110
1001
7
0111
-7
1111
1000
至此,相反数相加不为 0 的问题成功解决!但是,还有一个问题没有解决。
补码的产生
在原码和反码中,0 居然都有两种表示方式,+0 和 -0,哪个才是正统表示?为了完成这个最后的目标,人们又发明了“补码”,在反码的基础上 +1.
正数
原码/反码
负数
原码
反码
补码
0
0000
-0
1000
1111
/
1
0001
-1
1001
1110
1111
2
0010
-2
1010
1101
1110
3
0011
-3
1011
1100
1101
4
0100
-4
1100
1011
1100
5
0101
-5
1101
1010
1011
6
0110
-6
1110
1001
1010
7
0111
-7
1111
1000
1001
反码中的 -0 在加上 1 之后,变成了 (10000) ,舍弃最高位后与 0 的反码表示完美契合。因此两个相反数相加时,去掉最高位的 1,也能得到 0000.
换一个角度
数学角度,将负数用补码表示,实际上是实现了一种从 [-128, 127] 到[ 0, 255] 的映射。如下所示:
数值
映射值
二进制
127
127
01111111
4
4
00000100
3
3
00000011
2
2
00000010
1
1
00000001
0
0
00000000
-1
255
11111111
-2
254
11111110
-3
253
11111101
-4
253
11111100
-128
128
10000000
如果你想利用数轴来理解,不妨认为负数部分的数段 [-128, -1] 被移动拼接到了 [128, 255] 的位置,然后根据 0 - 255 的二进制表示来依次赋值。
模与补数
在日常生活当中,可以看到很多这样的事情:
把某物体左转 90 度,和右转 270 度,在不考虑圈数的条件下,最终的效果是相同的;
把分针倒拨 20 分钟,和正拨 40 分钟,在不考虑时针的条件下,效果也是相同的;
把数字 87,减去 25,和加上 75,在不考虑百位数的条件下,效果也是相同的;
上述几组数字,有这样的关系:
90 + 270 = 360
20 + 40 = 60
25 + 75 = 100
式中的 360、60 和 100,就是“模”。
式中的 90 和 270、20 和 40,以及 25 和 75,就是一对对“互补”的数字。
知道了“模”,求某个数字的“补数”,就是轻而易举的了:
如果模为 365,数字 120 的补数为:365 - 120 = 245。
用补数代替原数,可把减法转变为加法。出现的进位就是模,此时的进位,就应该忽略不计。
二进制数的模
前面说过的十进制数 25 和 75,它们是 2 位数的运算,模是 100,即 1 的后面加上 2 个 0。如果有 3 位数参加运算,模就是 1000,即 1 的后面加上 3 个 0。这里的 1000,是十进制数的一千,可以写成 10^3,即 10 的 3 次方。推论:有多少位数参加运算,模就是在 1 的后面加上多少个 0。
对于二进制数字,模也是这样推算。如果是 3 位二进制数参加运算,模就是 1000,即 1 的后面加上 3 个 0;那么当 8 位二进制数参加运算,模就是 1 0000 0000,即 1 的后面加上 8 个 0。16 位二进制数参加运算,模可就大了,是 1 的后面加上 16 个 0。注意:这里提到的 1、0,都是二进制数。8 位二进制数的模可以按照十进制写成 2^8,即 256。16 位数二进制数的模,就是 2^16,按照十进制,它就是 65536.
据此理解反码与补码
所谓反码,英语里又叫 ones' complement(对一求补),这里的一,本质上是一个有限位计数系统里所能表示出的最大值,在 8 位二进制里就是 11111111,在 1 位十进制里就是 9,在 3 位十六进制里就是 FFF(再大就要进位了)。求反又被称为对一求补,用最大数减去一个数就能得到它的反,很容易看出在二进制里 11111111 减去任何数结果都是把这个数按位取反,0 变 1,1 变 0,所以才称之为反码。
所谓补码,英语里又叫 two's complement(对二求补),这个二指的是计数系统的容量(模),就是计数系统所能表示的状态数。对 1 位二进制数来说只有 0 和 1 两种状态,所以模是 10 也就是十进制的 2,对 7 位二进制数来说就是 10000000,这个模是不可能取到的,因为位数多一位。用模减去一个数(无符号部分)就能得到这个数的补,比如 10000000-1010010 = 0101110,事实上因为10000000 = 1111111 + 1,稍加改变就成了(1111111-1010010)+1,所以又可以表述为先求反再加 1. 总结求补码的方法就是正数依旧不变,负数保留符号位不变,先求反码再加上1.
所以求取补码,就按照定义的规定,负数采用“模减去绝对值”的方法来求,这是求补数的通用方法,适合于各种进制、各种大小的数字。“求反加一”的计算方法只是适用于计算二进制形式的补数,它并不是通用的,而且把“求反加一”用于求 128 的补码,有个溢出的现象,很多人都在这里郁闷了很久。取反加 1 就是为了让逻辑门电路能方便的计算,这样就不存在减法了。至于人的计算,或者说对于理解补码的原理,取反加 1 只会让人糊涂。
参考链接