本文大多是借鉴网上资料加笔者个人的理解, 如有错误, 欢迎联系指正
# 一、为什么补码能用于计算?
# 1、借助时钟初步理解补码
- 我们现在想象世界上只有0 ~ 11这12个数,且11 + 1 = 0。即他们形成了一个环,就像一个时钟一样
- 现在我们计算3 + 5,很显然这是8点钟,但是我们发现3 - 7也等于8点钟。即我们可以认为在若干环形分布的数据计算中,减去一个数,等于加上(最大值+1)再减去这个负数后再取余(最大值+1),即 3 - 7 = (3 + (11+1) - 7) % (11+1)
- 注意在我们的语境下3-7就是等于8,这个值是正确的,不用想它为什么不是-4,因为我们没有定义负数(就像时钟的3点钟前7个小时就是8点一样)
- 相关理论可以看:同余定理 (opens new window),注意其中的保持基本运算。由它就可以证明上述是正确的
3 ≡ 15 % 12
7 ≡ 7 % 12
3 - 7 ≡ (15 - 7) % 12 = (3 + 12 - 7) % 12
# 2、我们计算的(11+1)-7就是计算"补码"
- 易知:补码+被减数 = 最大值 + 1。这个根据补码 = 最大值 + 1 - 被减数 得出
- 可以看到我们尝试用含有减法的公式去代替减法的计算,显然陷入了循环,但是放在二进制中就有很好的效果,因为二进制中的被减数(或者引入负数概念后的负数)补码计算规律正好是对机器非常友好的
- 二进制计算补码:先以无符号8bit整数举例子(即假设现在我们只有0~255数字组成的环),先去除符号逻辑避免干扰
- 4-2,我们想计算这个-2的补码(我们先不考虑符号位,把它看做是计算减2的补码):首先,现在最大值是11111111(255),即2的补码 = 11111111 + 1 - 00000010。
- 注意11111111 - 00000010就是计算的反码,其 = 11111101,由于二进制表示的原因,所以正好可以由口诀表示为一个数的反码等于其所有位取反,而这个口诀对于计算机来说很好实现
- 同样的,最后结果还要加1,所以补码口诀就是反码+1。对于计算机来说,也很好实现
- 对于计算时,上面时钟例子中的取余操作,二进制中天然就有取余,即高位溢出
- 还要注意,我们的计算结果是补码,而对于时钟这种情况或者只有正数的这种情况,补码就是其本身(为了让我们定义的补码更有普适性,我们发现加上一个数(或者在有符号里的正数)不用计算补码,那么其实就是补码就是它本身),所以结果无需处理。
综上所述,就回答了为什么"补码"可以用于计算,即不是我们先有"补码"后我们利用补码计算,而是我们先有计算规律,把规律中的一个步骤的数字转换,定义转换后数字是转换前数字的补码,为了普适性,我们把没有变动的加法也定义为补码,所以有了加法的补码不变,减法的补码是反码+1。这里说加法和减法的原因是我们到目前为止一直回避正负
# 二、为什么带符号位数字也可用补码计算?
为什么带符号位数字也可用补码计算?也可以说为什么负数的补码计算规则是符号位不变其它位取反+1,之后利用补码计算可以正确得到答案
- 由第一大部分可得,在加减计算中加数也就相当于正负中的正数的补码就是其本身,这个很好理解,不用再过多讨论
- 还是以8bit举例子,它再怎么变也就0000000 ~ 11111111 这256个数,现在我们定义最高位为符号位。这个只是我们的定义,计算机硬件不管这些,它只知道这些都是8bit的数据。
- 所以关键的地方来了,还是那个环,还是一共256个数字,只是我们最后如果得到的结果的二进制表达最高位是1,那么我们就让计算机将它显示为负数,注意,计算器只是听从命令
- 由于负数只是我们的定义,所以我们计算4 - 64 即 有正负号的4 + (-64)
- 我们先看不定义负数的情况,即类似时钟这种情况,我们要计算减去64对应的补码,然后只需加上减去64对应的补码,即11111111 + 1 - 01000000 = 11000000。00000100 + 11000000 = 11000100。这就是答案,我们发现这个数字在只有0~255的领域内是196(196的补码就是自己本身)(回忆一下,领域内没有负数,4-64就是196),显然正确
- 再看现在有符号位的,-64的表示为11000000,按照口诀,符号位不变,其它取反+1,即补码为11000000。发现了什么,和不定义负数的情况的结果一模一样(因为负64的补码计算规则本来就是由上面例子中减去64来的),所以。符号位不变这个口诀只是一个规律的总结(规律如何得出见下方等会解释)。之后我们结果当然也是11000100,此时,关键点又来了,在没有负数的领域,这个补码代表196,而此时此刻,有了负数,那么11000100这个补码代表谁呢,由于我们定义了最高位为1代表负数,则尝试还原,11111111 + 1 - x = 11000100。x = 00111100,注意我们计算的是-x中的x,所以结果应该为-60。
- 我们来探讨为何恰巧负数的补码口诀是符号位不变,后面取反+1。我们发现,8bit中,我们区分正负后,比如上面计算4+(-64),只是我们定义的-64 = 11000000,计算机计算的时候还是我们上面所使用时钟的例子,减去一个数等于计算加上他额补数。即还是按照11111111 + 1 - 01000000来计算的,我们发现,把我们定义的负数看成减去一个正数后,这个正数的最高位就是0,而被11111111 + 1减去这个最高位非1的数后,最高位就是1。所以有了符号位不变,至于后面的取反加1,就更容易理解了,第一部分的第二小节已经解释过了
- 所以根本原因还是定义了符号位后,负数或者说被减数,即-x中的x已经不能取大于127的值了,所以最高位本来就是0,再按时钟计算的逻辑计算减去x的补码,最高位肯定就是1
- 所以还可以这样理解,负数的补码才是一定最高位是1,负数本身最高位是1只是我们用8bit后半段来强行表示负数或者被减数而已
- 再从数字环型排列上解释一下上面的计算:
- 8bit无符号的话,0~255(补码和原码都是这个环)形成了一个环,4-64会从0溢出到255后继续减小直到196
- 8bit有符号的话,0~127, -0 ~ -127形成了一个环,计算对应补码环就是0 ~ 127 ,-128 ~ -1(注意-128只是我们为了去掉重复0定义的而已,计算结果补码为10000000就看做-128)。我们会发现我们计算的-60的补码,其实和196补码在环中是一个位置,所以这一切都取决于我们的定义。我们定义环的后半段是负数,那么计算机就会按我们的规则将补码的计算结果转换成对应的原码(由于我们上面已经推导了为什么负数的补码计算规则是符号位不变,其它位取反+1),所以机器看到计算结果是以1开头的,就知道结果位于环的后半段,就会按口诀逆向求出答案
- 定义的-128也满足我们的计算逻辑,即最大正整数溢出后应该是最大负数,完美的解决了重复0问题,还能够多表达一个数。定义了-128后的环。
- 其实可以这样认为,我们定义的-127 ~ -1本来就不在环上,我们计算的时候。相当于只是127内的加减,而0~127的减法用补码计算转换成加法时,相当于计算了-127 ~ -1的补码,而计算的这些补码(由于被减数最高位为0)正好分布在环的后半段,而当手动人为定义了-128后,整个补码环连贯了起来(分割点主要是2个,一个是127到-128,这里相当于溢出bug,但是我们适应之后,它就显得很自然,而从-1到0的这个连接点,也本来就符合我们的逻辑,即取余运算,二进制的高位抹除就相当于取余,所以可以正确计算),此时这个补码环就像只有正数的补码环一样,可以很好的用于计算。当得到计算结果时,再逆向求解结果是谁的补码,就得到了正确结果。而就像上面逆向求解得到-x(x=60)的x = 00111100一样,x为负数,此时计算机相当于再手动赋予定义的规则,最高位为1,即10111100,这样解析时也会按照规则解析,就是-60了