探究计算机中补码运算的本质

在我们学习计算机组成原理,涉及到二进制数运算时,所有的书中都会提出这么几个新手劝退的概念:原码、补码和反码。说是由于计算机硬件中只有加法器,没有减法器,因此需要通过补码将减法变成加法,什么正数的补码是其本身,负数的补码是其原码除符号位之外其余位取反加1 ,最高位代表符号位,0是正数1是负数,补码不但解决了减法问题也解决了负数问题balabalabala……每次学到这儿的时候我的心里就日了狗,一堆概念计算繁琐不说,似乎这个补码规则刚好是个特别完美的巧合一样,也似乎正是有了这个巧合计算机才能做加减运算……这他喵的算什么玩意儿?!如此严谨的一个学科,难道竟是凭巧合来做设计的吗?最气人的是,书中的笔墨向来都集中在补码现象及其运算规则上,却从来不会说它的理论基础到底是什么,只留我这种强迫症患者在风中凌乱……

那么对于计算机的设计者来说,到底是怎么脑洞大开想出这个补码系统的,它背后的理论基础又是什么?

经过一晚上的度娘和CSDN大扫荡,我终于揭开了谜底,补码的理论基础其实就是——数学中的补数。提到补数,就不得不提一下这个概念,而提到模,就又不得不提一下大学时代的宿敌高斯所提出的同余定理,而提到同余定理,就又让人联想到了计算机世界中的哈希……有点儿扯远了。

下面我简单的总结了一下模、同余、补数等这些概念:

  • 指的是一个计量系统的计数范围,例如:时钟的计量范围为:[0,11],总共能记12个数,其模就是12;8位二进制数的计量范围是[0,255],能记256个数,其模就是2^8=256。在这个计量系统中,任何一个整数一定会被映射到该模(m)的一个固定的位置中(有点类似哈希),而这个位置其实就是整除“模”之后产生的余数(从0到m-1),m-1的下一个位置又变回0,因此可把模中的位置想象为一个首尾相连的环。在这个环上顺时针(相加)走x步得到的位置,就相当于逆时针(相减)走 m-x 步得到的位置,因此任何有模的计量器均可化减法为加法。
  • 补数 补数的存在是为了将负数转化为正数,从而将减法转化为加法,因此正数的补数就是它本身(本来就是顺时针),负数补数的本质是将一个逆时针取值的数转换为顺时针取值的数,即“补数 = 模值 - |负数|”
  • 余数 指的是一个数在当前的“模”中被映射到了哪个位置
  • 同余 指的是如果不同的数都被映射到“模”中相同的位置,那么这些数同余
  • 补码 计算机中的二进制补数

有了上面的理论基础,我们可以大胆的想象一下,如果让我们来设计一个8位的二进制数加减法规则,该如何做?首先要明确一下需求:

  1. 硬件中只有加法器而没有减法器
  2. 每一个数只有8位的存储空间
  3. 要求必须能够表示负数

我们知道,8位二进制码总共可以表示[0,255]这么多的数,所以它的模是256。那么,我们可以先构建一个拥有256个位置的环,环上的每一个位置代表一个数字,如图所示:
在这里插入图片描述
很明显,当我们要做加法时,要加x就相当于顺时针走x步,要减x就相当于逆时针走x步,最后的落点位置即为所求结果。要表示正数,实际上就是表示0+x,表示负数,实际上就是表示0-x。所以很容看出,如果将这个环一分为二,那么从右走的数全是正数,而从左走的数全是负数,也就是说对于环本身来说其实根本就没有什么负数,只是我们的动作使其产生了正负,进而才有了“最高位表示符号位,0为正数,1为负数”的约定,惊喜不惊喜,意外不意外?之后图就变成下面这个模样:
在这里插入图片描述
以上的逻辑虽然对我们人类来说讲得通,但是对于计算机来说它可不知道什么顺时针和逆时针的玩意儿,因此我们必须想出一个别的办法让它也能理解这个过程。对于一个8位二进制数来说,其本质不过是这个环上的某一个位置,这个位置一定可以用两种方法来表示,即顺时针表示法和逆时针表示法。例如对于A点,它既可以用-3来表示(逆时针从0走3步),也可以用253来表示(顺时针从0走253步),还可以通过2+251来表示(顺时针由B走到A)……等等,也就是说实际上每一个负数都可以通过一个正数来表示(每一次减法都可以通过一次加法来表示),而这个正数,这个正数到底是什么呢? 这个正数它其实就是我们梦寐以求的补码 (补数) 啊! 正是因为有了它,计算机才可以只做加法不做减法,才可以用正数来表示负数。负数采用自己的补数来表示,减一个数相当于加上这个负数的补数,这样减法和负数的表示问题就都得到了解决!补数的具体算法如下:

补数 = 模值 - |负数| (例如253 = 256- |-3| )

当我们把这个过程转化为二进制时可以得到:

1111 1101 = 1 0000 0000 - 0000 0011
= (1111 1111 + 1) - 0000 0011
= (1111 1111 - 0000 0011) + 1
= 1111 1100 + 1

发现什么了吗?0000 00111111 1100互为反码!所以反码到底是什么? 反码就是那个和原码加起来等于最大值(111111111…)的数啊。 之所以要求得这个最大值,是因为最大值加1就是我们的模!就是我们要计算补数时必须得用到模啊!这才是 “负数的补码就是其原码除符号位之外其余位取反加1” 这种绕口令公式脱了马甲的样子,惊喜不惊喜,意外不意外?

好了,既然补码的衣服已被我们扒了个精光,接下来就看一个它裸奔的例子轻松一下吧!

    public static void main(String[] args) {
        byte x=127;
        x++;
        x++;
        byte y=-128;
        y--;
        System.out.println(x);
        System.out.println(y);
    }

我们都知道byte的取值范围是[-128,127],所以很明显这段代码的x和y在运算后都发生了数据溢出,那我们来猜测一下输出结果吧。嗯……怎么算好呢?还要把127和-128换算成二进制补码再做运算吗?NO!!!既然我们已经搞明白了二进制加减法运算的本质原理,那么就让这些乱七八糟磨磨唧唧的求补运算套路,统统给我滚犊子吧!咱今天只用小学数学!再上图:
在这里插入图片描述
由图可知,x的初始化位置是在C点上,现在开始掰指头数数,x两次加加,意味着顺时针走两步,走到了129这个点上,所以x在计算机中的最终数值为129。这就完了吗?还没有。

我们费这么大劲去构建这个环,为的不是在上面转圈圈,而是能让它在程序中显示负数、做减法。所以129虽然是计算机中存储的实际值,但我们必须将它转化为人类能理解的数字。对于人类来说,西半球的数字可以看成是负数,那么这个负数应该是多少?就是从0开始逆时针走到129这个位置所用的步数,需要256-129=127步。所以x在程序中的最终打印值为-127。

再来说y,这丫一出场就是个负数,很不友好。那么该在哪个位置表示它呢?-128,意味着要从原点逆时针走128步,也就是相当于顺时针走256-128=128步,所以它的初始值刚好落到B点上。y一次减减,意味着要逆时针走一步,于是走到了C点。此时我们看到C点处于东半球为正,这对人类来说理解起来就没什么障碍了,他大爷还是他大爷,所以取127本尊即可。

最终程序的打印结果为:
在这里插入图片描述
所以说,哪有什么负数,哪有什么溢出,哪有什么最高位符号位,只要是用byte类型去做加减,模就只有256那么大,加来减去的也不过就是在这个环上不停的绕圈圈,所谓正数、负数、溢出,只是我们人类用来哄自己玩儿的……(手动哭笑)

以上内容除了概念部分以外均为本人脑洞,并非补码的原本演化过程,只是为了帮助理解。如有不严谨之处还请多多指教。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值