从模运算的角度看原码和补码
从模运算的角度看原码和补码
写作的背景:之前在学习计算机基础的过程当中,对于计算机原码、反码和补码的相关知识一直处在一知半解的状态,即仅仅只停留在会用的阶段,但是对于计算机中引入补码的原因,以及补码是怎么来的(从数学的角度看)类似这样的问题自己一直处于懵逼状态。虽然老师也曾经对此作出过解释,但是自己一直本着会用就行的原则,所以也一直没有学会。然而随着课程的深入,自己渐渐的发现,对于这些计算机的底层知识的深刻理解还是十分重要的,所以自己又通过翻阅书籍以及查找网上的相关资料试图对原码和补码的问题能有一些深刻理解,终于经过不懈努力,总算有些收获,特此记录,以备不时之需。
1 引入补码的原因
关于计算机中引入补码的原因,我想大致分为两个:
- 解决原码的局限性
- 将减法转化为加法
1.1 原码的局限性
关于原码的局限性,我想对于学过计算机的朋友都知道(当然没有今天这一遭,我是不知道的,脑子里一团浆糊),对有符号数来说(为了方便下面都以8位2进制数进行举例),最高位作为符号位,其余7位作为数值位,那么对于零这样一个比较特殊的值来说,对于它的原码表示是有两种:\([0]=0000\ 0000\)和\([-0]=1000\ 0000\)两种,众所周知,出现这样的情况是非常不好的,所以后面就引入了补码成功弥补了原码的局限性。
1.2 将减法转化为加法
学过计算机组成原理的都比较清楚的一点是,在计算机硬件里是没有做减法这么一说的,在计算机里所有的减法操作都会被转化为加法操作,为了到达这个目的,所以引入了补码,将两个数的减法运算,直接转换为对这两个数补码的加法运算(计算机中对于数的存储也是存的是一个数的补码),至于为什么可以这样做,在下面将会做出解答。
2 模运算的简单介绍
对于模运算,想必大家都不陌生,模运算又被称为时钟运算。为什么会被称为时钟运算呢?我们不妨用时钟来举例,假设现在是下午的三点,如果你想将时钟调整到下午的一点,想一想你会有几种办法?
- 将时钟逆时针拨动两格
- 将时钟顺时钟拨动十格
将上面的叙述转换为数学语言就是,由3变到1,有两种办法,第一种,通过3-2的方法;第二种,通过3+10的方法。这时候你肯定会感到怪了,\(3 + 10 = 13\neq 1\) 啊,但是你别忘了我们现在可是在时钟上举例子,你现在可以回想一下12小时制和24小时制,13 和 1在时钟上是不是指向了相同的位置?当然,说这个只是为了让你直观的感受一下,其实,通过观察时钟我们可以发现,在时钟上一共就有12个刻度,分别是1~12(这里仅仅只看时针),你任何一个大于12的整数通过你拨动时钟(当然你要假定一个起点),都会唯一对应时钟上的一个刻度。比如之前的那个例子,从3出发拨动10个格子,虽然从数学计算上是13,但是它在钟表上,就唯一对应了一个刻度1。(对于前面通过时钟运算唯一对应这件事情有没有让你觉得熟悉,会不会让你联想到映射)其实,模运算说的也是这么个东西。
对于模运算来说(比如:\(mod\ n\)),其实本质上就是就是将任何一个整数都映射到\([0,n-1]\)上,还是拿之前的那个例子来说,对于时钟来说,n自然取12,那么对于刚才那个3 + 10,我通过模运算\((3+10)\ mod\ 12\),就可以把3 + 10映射到时钟能够表示的一个具体刻度上去,最后的模运算的结果是1,这和我们之前通过拨动时针的出来的结果是一致的。现在你应该知道模运算为什么又叫做时钟运算了吧,其实模运算就是对时钟现象的一种数学抽象,仅此而已。
3 模运算与原码和补码
从上面的那个例子我们不难看出,对于时钟运算(即模运算)来说,\(3-2\)和\(3+10\)的结果是一样的(即\((3-2)\ \equiv (3+10)\ mod\ 12\),同余),前者是减法操作,后者是加法操作,这其实就给我们提供了一个将减法转换为加法的思路。
对于8位二进制数来说,能够表示出来的范围也就是0~255,对于大于255的数来说,我们可以通过模运算将它们都映射到0~255上来,这样对于一个负数,比如:-2来说,其实-2是不在0~255这个范围内的,于是我们就可以通过模运算,\(-2\ mod\ 256\)将它转换为0~255上的数,即\(-2\ mod\ 256 = 254\),不难发现\(254\ mod\ 256 = 254\),也就是说\(-2\ \equiv 254\ mod\ 256\),也即是对于任意的一个\(a\),\(a-2\)可以转换为\(a+254\),这样我们就成功的将减法转换为加法,对于任意的一个负数都可以通过上述的步骤进行转换。
下面我们来说一说,原码和补码的事,那刚刚的那个例子我们来看看\(-2\)的原码为1000 0010,\(-2\)的补码为1111 1110,再看看254的补码也为1111 1110,可以看到\(-2\)与254具有相同的补码,也就是说\(-2\)的补码其实就是\(-2\ mod\ 256\)的结果的二进制表示,而我们都知道对于正数来说,原码 = 补码,所以到这我们可以发现,所谓的将两个数的原码运算转换为补码运算,其实本质上就是为了解决将减法运算转换成减法罢了,而所谓的补码,其实就是负数模运算之后的二进制表示而已,只是后面为了叙述规范又统一给了个名字将负数模运算之后的二进制表示叫做补码。
再来看看之前对于原码的局限性的问题,在补码中是否依旧存在,先来看看\([0]_补=0000\ 0000\),而\([-0]_补=0000\ 0000\)。可以发现,对于补码来说,0就对应了一种编码,原先的问题在补码中也得到了较好的解决。
4 总结
补码概念的引入其实主要就是为了解决运算过程中将减法运算转换为加法运算的问题,而补码的本质其实就是对负数进行模运算结果的二进制表示,其实补码主要也是针对负数来说的,对于正数来说原码和补码相同,补码对于正数的意义也就没负数那么重要了。
5 参考资料
https://www.cnblogs.com/flowerslip/p/5933833.html
https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html