发布这篇文章之前,查阅了大量资料。网上的回答质量参差不齐,要不就是概念之间过于跳跃,要不就是方向过于片面。写这篇博客,一来是帮助自己解决这个问题并做一些记录,二来是整理一个较好的思路流程帮助大家理解。
思路一
(本思路按原码、反码、补码的顺序介绍。这个思路相对简单一些,如果是只是学生或者暂时了解以下基础原理,只看这一部分就足够了。)
1.原码
所谓原码就是机器数,在数值基础上增加了一个符号位,用以表示正负。0代表正,1代表负。
上图中的数字,对于人类来说,正负很好区分,但计算机却不这么认为,因为对它来说,
(+2)+(-2)= 0010 + 1010 = 1100 = -4
大小相等符号相反的两个数相加却是-4,显然不能用于计算。为了解决正负相加不为0的问题,人们发明了“反码”。
2.反码
反码对负数进行了处理,符号位不变,其余位置均取反。(括号内容可以不看。所谓反码,英语里又叫ones' complement(对1求补),这里的1,本质上是计数系统里所能表示出的最大值,在4位二进制里就是1111,在1位十进制里就是9,在2位十六进制里就是FF(再大就要进位了)。“对一求补”的含义是,用这个最大值减去一个数就能得到它的反,很容易看出在二进制里1111减去任何数结果都是把这个数按位取反,0变1,1变零,所以才称之为反码。)
观察上图中的反码,
(+2)+(-2)= 0010 + 1101 = 1111 = -0
现在正负相加就为0了。但仔细观察,你还会发现一个问题,0存在两种表示形式,
(-0)+(1)= 1111 + 0001 = 0000 = 0
-0和其他数相加结果还是0,显然不正确。这个问题归根结底,还是因为0的两种表述方式。我们希望只有一个0,所以发明了"补码"。
3.补码
补码仍是对负数进行处理,补码 = 反码 + 1。(括号内容可以不看。所谓补码,英语里又叫two's complement(对2求补),这个2指的是计数系统的模。对4位二进制来说模是10000。这个模是不可能取到的,因为位数多一位。用模减去一个数就能得到这个数的补。)
如上图这样处理,0就只有唯一的表述形式0000,而且还没有影响其他计算。
(+2)+(-2)= 0010 + 1110 = 10000 = 000(丢掉进位)= 0
同时,观察上图,由于进行了+1处理,负数还能多表示一位(-8),简直是一举两得。
4.总结
至此,我们不仅解决了正负相加的问题,也解决了+0和-0同时存在的问题。需要注意的是,计算机内部进行运算时,存储的都是补码(正数的补码是其本身),计算结果也都是补码。对计算机来说,已没有原码的概念,这个概念更多是方便人们理解。
如果只是应付考试,或者是工作需要粗略了解,无需要看思路二,容易绕进去。
思路二
(从模引出)
1.减法的解决:模与补数
由于设计上的原因,计算机只会做加法运算,不会减法。计算机在做减法运算时通常把减一个数变为加上这个数的负数。
那么如何把减法化为加法呢?
想象一下日常使用的钟表,它可以显示0~12点的时间,假设现在是4点钟,现在想要拨到2点钟,有几种方式呢?
有两种方式:
-
逆时针将时针拨4小时
-
顺时针将时针拨8(12-4)小时
第二种方式,为什么逆时针拨(12-2)小时也可以满足要求呢?这里就引出了一个概念,模。在上面的例子中,12即为模。4-2 = 2和 4+10 由于超过了12点的“模”值,去掉进位即为2。
同样的,如果是十进制的两位数,80-10 和 80+90在不考虑百位数的基础上都是70。这里的90就是100-10得来的,这种情况下100就是模。
模就好比是一个极限数,在它的范围内,两个相加等于模的数互为补数,还是举100的例子,比如25和75就互为补数。
通过模我们可以得到一个数的补数,那么就可以把减法运算化为加法运算。(隐含的一点,通过模找到补数,我们就可把负数用正数表示)
比如“X-Y”的减法变更为“X+Y的补数“的加法,当然前提是不考虑百位数。
2.深入理解:符号位的由来
用补数计算有一个不对的地方,就是结果是负数的情况。
比如40-60,结果应该是-20,但如果按照40+(100-60),结果是80。
-20和80明显不是一个数,怎么办呢?
这里的处理方法就很简单了,直接用80来表示-20,舍去其80原本的含义。即:80就是-20。(有点拗口,仔细梳理一下关系)
所以在计算机世界中,负数的表达方式就是它绝对值的补数。看上去,这个决定有些草率,而且似乎只解决了减法结果是负数的问题。实际上,回顾上文,这个决定与前面把减法变成加法如出一辙,所有带负号的问题,我们都用补数将其抹去了。
还有一个没有解决的问题,就是80可以同时表达一个正数和一个负数,虽然我们的本意是舍去其原本的含义,那怎么才能做到呢?
为了解决这个问题,需要给这套规则划定一个范围,原来是0~99的正数,现在既然要有部分正数需要去表示负数,那就要规定一个范围来使得一个数只代表一个含义,正好一人一半,0~49这个区间就代表正数,50~99的区间就用来代表各自补数的负值,例:98就代表-2。
那么知道了补数的概念后,我们回到计算机世界,8位二进制数一共可以表示2的8次方,256个数,即0~255(0~11111111) ,在这基础上加00000001,出现了进位,即100000000。这个100000000就是8位二进制数的模=256。
用我们上面了解的原理去表示正数和负数,256中一半的数0~127,代表其正数本身,另一半的数 128~255,代表其补数的负值,即“-1~-128”的区间。仔细观察这一部分数,其首位均为1,现将其规定为符号位;剩下的位,取反加1即为该数对应的绝对值,所以称其为数值位。这就是1代表负号的由来,因为需要一半的数表示负数。
而 “X-Y”的减法 就用 “X+Y的补数” 的加法来表示。 唯一需要注意的事情是任何计算的输入值和输出结果值都需要严格遵守-128~127的范围,一旦溢出就会报错。
以我们人类的思维方式来说,虽然用加法表示减法可以使用补数这个概念,但是我们求补数这个过程就用到了减法,而计算机又不会减法,这样就进入了一个死胡同:想用加法表示减法需要先用到减法。
这里就体现了二进制的优越性。或许十进制我们不好解决这个问题,但二进制好解决。运用前文数值位上的规律,只要将其取反(电路中对应的只需反相器)加1,即可找到对应的补数。
3.溢出:符号位的优越性
符号位是关于补码最神奇的地方。人类“生硬”地添加了符号位,把256种状态剪成正负两半,还“生硬”地规定-128的补码为10000000,但用补码运算的时候,一切就像“水往低处流”般正确和谐自然:符号位参与运算,接受来自低位的进位,永远能忠实地指示结果的正负。
举个例子:
所谓的“负数加负数会变成正数,正数加正数会变成负数”,本质还是在于,计数系统是无法表示超出其取值范围的计算结果的。
120D+120D=01111000B+01111000B=11110000B,符号位的1来自低位进位,指示了结果是负数,所以需要求补得10010000B也就是-16D,放在钟面上就是从120顺时针旋转120格到240的位置,只不过系统最大只取到127,240的位置就是-16的位置,而且-16和240正是关于模256的一对补数。-120D-120D=16D也是一样的道理。在有限的计数系统内,由于位数的限制,发生溢出的情况下无法得到计算真实值,得到的是真实值关于模的补数。事实上,可以认为所有得到的结果都是补数,如果得到的结果符号位位正,则无需处理;如果是负,则取反加1即可。
思路二主要参考两篇博客,我在此列出,有兴趣的可以查看原文: