一、十进制与二进制
我们是如何去表示一位十进制数的呢?以123为例:
进制表示法中,一个数的每一位都有着不同的权重,例如十进制中每一位的权重都是10的次幂,对应图中的10º、10¹、10²,我们将每一位上的数字乘以这个位置对应的权重,然后全部相加,就能得到这个数对应进制的表示形式。
现在然我们来看看二进制是如何表示一个数的:
与十进制相同,二进制每一位的权重就是2的次幂,用每一位上的数字乘以对应权重然后全部相加就能得到这个数的二进制表达形式了。
二、溢出、截断和数值循环
几乎所有计算机都是利用二进制来存储和表达数据的。但是计算机并不能像我们人类一样在草稿纸上写出一个“接近无限”的数字,因为计算机存储数字的数位(比特位)是有限的,进而计算机能够表达的数字大小也就受到了限制。我们以4个数位为例:
结合上述二进制知识不难得到,用四个数位能够表示的最大数值为15。我们还能得出一个普遍规律:假设有n个数位,那么这些数位能够表达的数字上限为max = 2^n - 1。
倘若此时我们将这个数字与1相加会发生什么呢?
对应位相加进位可以得到图示结果,可以看到,最左端进位得到的1由于数位有限而被“挤”到了外边,这种现象就叫做“溢出”。在数字溢出的情况下,计算机会将这个溢出的数字直接舍去,这种现象就叫做“截断”。
截断之后,数字就变成了下面这样:
是的,它加1之后重新变成0了!如果我们再不断地将这个数字与1相加,那么便会出现0—>15—>0—>15......这样循环往复的局面,这就叫做“数值循环”。
它们三者的关系可以这样表示:有限存储 + 溢出截断 => 数值循环
“数值循环使得减法能够用加法来代替,这也是补码出现的根本原因。”
不理解这句话?没关系,且听我慢慢道来。我们先以时钟为例来解释前半句话:
这是一个正常的时钟:
这个时钟的时针循环往复的旋转着,一次又一次地经过同一个刻度......
此时时钟显示的时间为3点,假如我想要时钟变成1点,那么我可以逆时针旋转时针2格,也就是减去2个小时;另外,我还可以顺时针旋转时针10格,也就是加上10个小时。不难理解,这两种做法是等价的!也就是说,我可以用“加上10个小时”来代替“减去2个小时”!
三、模
这里,我们引入一个新的概念——模。“模”是指一个计量系统的计数范围(通俗理解就是能够表示的数字的个数)。例如:一个计量系统的计量范围是 0 ~ 2^n - 1,那么这个计量系统的模就是2^n。 再例如:上边四个数位的格子的模就是15+1=16。“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。
四、原码
(是“原码”而不是“源码”)
计算机本身其实是不能表示正数和负数的,为了表达正数和负数,科学家们就对数位进行了一个分类,第一个数位叫做“符号位”,后边的数位叫做“数值位”。由于计算机只能限制1和0,于是规定符号位上为1时,这个数为负数;符号位上为0时,这个数为正数。
通过这种编码方式去表示一个有符号整数我们称之为“原码表示法(Sign-Magnitude Representation)”。那么用原码表示法去表达一个数得到的二进制序列,我们就称之为“原码”。
举个例子:假如有5个数位,那么-3的原码就是10011。
在原码表示法下,假如有n个数位,那么此时能够表示的数字范围就变成了-(2^(n-1)-1)~2^(n-1)-1。但当我们用原码去进行表达或者运算时,却会出现一些问题:
(1)“正零”和“负零”
原码表示法中,数字零会有两种表达方式(以5个数位为例):
1.00000(正零)
2.10000(负零)
其实只有第一种表达方式才是正确的,第二种所谓的“负零”其实是没有实际意义的,但是它并非一无是处,它一般用来表示“模”的补码(详见下文)。
(2)两个数字相加得到的结果可能与实际不符
如图:
以3+(-3),我们将3和-3的原码相加,得到的结果居然是-6!这显然与实际部分,也就是有地方存在问题,这个问题便出在符号位:理论上,两个二进制数相加时,符号位是不能参与的,但是计算机可不认识符号位这个东西,当两个有符号的二进制数进行运算时,它会让符号位也参与到运算中。而出现这个问题的根本原因是CPU中只有加法运算器,也就是计算机不能直接进行减法运算!
3+(-3)其实就是3-3,这是一个减法,计算机算不了!这个时候,便有了“补码”的诞生。补码的作用是化减法为加法,让计算机能够正常运算。
五、补码与反码
那么补码到底是个什么东西呢?
补码其实就是计算机中一种表示负数的方法。它等价地顶替负数与其他数进行相加,从而以加法的方式实现减法。
如何求一个负数的补码呢?
课本给了我们明确的答案:首先拿到负数的原码,保持符号位不变,对其他位按位取反(1变0,0变1)(按位取反后得到的二进制序列就是“反码”,反码是原码转变为补码过程中的一个中间产物),然后加1,就得到了负数的补码。以-3为例:
原码:10011
反码:11100
补码:11101
此时我们再去试着计算3+(-3):
是的,最终的计算结果为0,这是正确的答案,不得不佩服卓越的科学家们。
但是课本上给出的求补码的方式似乎过于刻板,不贴近本质,难免让人觉得敷衍。下面就让我们从原理层面来探索补码的由来:
回到刚才的钟表:
要让时间变成1点,有下面两种方法:
(1)3 + 10
(2)3 - 2
假如现在让计算机来运算第二个式子,毫无疑问,它会直接懵逼。毕竟计算机不会进行减法运算,那么此时便要引入“补码”这个东西来帮助计算机完成运算,化减法为加法:
》》(2)3 + 10 = 13
这里的10其实就是所谓的补码了,10是-2的补码。补码让原本的减法变成了加法。
好的,现在让我们仔细观察式子,我们会发现 10 和 -2 的绝对值相加就是12。这并不是巧合,你不妨换几组数字试试,最后都将得到这个结果。现在让我们对钟表做个小小的改动:
此时,我们不妨将这个钟表看作一个计量系统,那么它的计量范围就是0 ~11,我们也不难得出,这个计量系统的模是12。
请允许我将-2称作“原数”,那么,数字10就应该叫做“补数”!“补数”10顶替“原数”-2化减法为加法。
并且“原数”和“补数”绝对值之和为“模”!请记住这句话。
下面再让我们揭开原码和反码之间微妙的关系:
-3的原码是10011,它的反码是11100,由于是符号位不变、数值位按位取反,那么得到的结果必然是符号位为0,数值为均为1,也就是这个计量系统能够表示的最大值(下文称max):01111。
总结一下就是:原码 + 反码 = max。也请记住这句话。
我们知道,一个计量系统的模就是这个计量系统能够表示出来的最大值加上1,也就是模=max+1
现在,我们将模 = 原码 + 补码(绝对值) 和 max = 原码 + 反码 带入上面的等式,再进行消元:
最终可以得到:补码 = 反码 + 1
这正是课本上给出的答案!
补充几点:
(1)正数的原码反码补码相同,只有负数才有上面的变换公式。
(2)用补码求原码时,也可以采用公式原码 = 补码的反码 + 1,因为原码是补码的补码。
(3)如何理解二进制中的 模 = 原码 + 补码:以-3为例(四个数位),原码是1011,补码是 1101,由于是绝对值相加,此时应当忽略掉符号位,即011 + 101,得 000 ,其实如果数值 位有4个数位,那么得到的结果应该是1000,但是由于只有3个数位,最前面的1被截断了, 无法显示出来。这也正呼应了上文的“模无法在计量器中表示出来”。
(4)笔者是名小白,博文中难免有偏颇疏漏之处,欢迎读者指正。我会在日后不断更正此文。
(5)至于为何char能表示的最小值是-128这个经典问题,详见笔者的另一篇博文:
【【C语言疑难杂症】char 为何能表示 -128 - 优快云 App】http://t.csdnimg.cn/3zWFH
参考来源:
bilibili-昕哥课堂:BV1uh4y1m7pi
bilibli-老左讲技术:BV1Nv4y1y7MF
优快云-楠c:http://t.csdnimg.cn/r5RZm
优快云-光阴老人:http://t.csdnimg.cn/6ZXSA
简书-刘岩2019:https://www.jianshu.com/p/bc9e4e6208d3