在我们进行计算机的知识学习过程中,很多基础教材一开始就向我们介绍计算机的三种编码,即原码,反码,补码三种形式;相信大家对这三种编码都会多少留有印象,事实上,计算机的存储系统以比特(bit)以基本单位,一个比特代表一个0或1,很多很多的比特来共同完成计算机的内部存储。由于这种存储结构只能储存二进制的数字,原码,反码,补码就是计算机储存实际数字的方法!
1.原码的第一个比特位为数字符号位,0表示正数,1表示负数,后面位数为十进制数字的二进制表示;
2.负数的反码为原码的除第一个符号位之外,其他的位置数字全部取反;正数的反码不变;
3.负数的补码为反码除符号位的二进制数字再加1,即补码为原码取反再加1;正数的补码不变。
这样的定义基本上会出现在每一本计算机入门书籍或是涉及到计算机的内部存储结构的书籍上,但是为什么要这样子定义三种编码,这样子定义又有什么样的好处呢?本文将从数学原理的角度向读者深入的揭示三种编码的奥秘!
一、原码与正数加法
事实上,对于一台计算机而言,对所有的数字只是单纯的支持加法(对二进制做减法过于复杂,因此计算机只有加法器的实现),即二进制的传统加法满一进一法则;而对于减法操作,计算机采取的方法是让减数加上被减数的相反数来实现的。因此在我们的讨论中只考虑正数的加减,这样就可以把所有的加减操作全部覆盖了。
由于计算机只可以对加法进行操作,即计算机本身是不认识所谓的正数和负数的,它只会对两个数字进行盲目的二进制相加;我们以16个比特存储单位为例,他的二进制存储可以表示的数字范围为
0
≤
x
<
65535
0\leq x<65535
0≤x<65535
因此,我们要成功表示负数的目的就是建立二进制数字与实际数字的一个双向映射,他能够将一些二进制数映射为不同的正数,将另一些二进制数映射为不同的负数;我们很容易想到的就是将所有的二进制数字直接分成两个部分,通过如下规则完成映射
f
(
x
)
=
{
x
(
x
<
32767
)
32768
−
x
(
x
>
32367
)
f(x)=\left\{ \begin{aligned} x(x<32767)\\ 32768-x(x>32367) \end{aligned} \right.
f(x)={x(x<32767)32768−x(x>32367)
事实上,这就是原码的映射数学原理,这个映射将自变量的取值范围直接一刀切成两半,在第一位为0时,即
x
<
2
15
x<2^{15}
x<215时从小到大表示正数,而当第一位为1时,即
x
>
2
15
x>2^{15}
x>215时表示负数;这个对应法则直观的描述就是
原码的第一个比特位为数字符号位,0表示正数,1表示负数,后面位数为十进制数字的二进制表示
很明显,这样的存储方式有两个问题存在:
1.对于0而言,有两种不同的表示形式;
2.对于正数的相减,原码加法会出现明显的错误:他忽视负号的作用,将正数的绝对值与负数的绝对值直接相加最后再赋值负号
第一个问题影响并不是很大,无非只是浪费了一个数字的存储空间,但第二个问题影响就会很大了!他直接让我们对减法无从下手,为了解决问题二,我们试着去寻找相应的解决办法来实现减法。
二、反码与大数减小数
我们希望通过二进制加法来实现减法,这需要我们重新建立对应法则关系,我们让正数的对应法则不变,去考虑改变负数的对应法则,我们首先希望能够做到正数减去它本身得到0;我们将原码的负数映射由单调递减改变为单调递增,得到如下映射:
f
(
x
)
=
{
x
(
x
<
32767
)
x
−
65535
(
x
>
32367
)
f(x)=\left\{ \begin{aligned} x(x<32767)\\ x-65535(x>32367) \end{aligned} \right.
f(x)={x(x<32767)x−65535(x>32367)
这样,正数与他的相反数相加恰好能够填充到定义域的末端,而定义域的末端恰好映射到0,这样相反数的问题就得到解决了!这个映射就是反码的实现:
负数的反码为原码的除第一个符号位之外,其他的位置数字全部取反;正数的反码不变;
我们检验这个映射模型,让2加上-1,此时在数据运算时发生了溢出,在二进制的世界里2加上了65534,去除溢出的数字1后得到了00000000,映射回了0,显然反码的方式解决了相反数的运算问题,但是却遗留下了大数减小数的问题;并且我们发现大数减去小数只是和正确结果相差了1,并且这并不是简单的巧合。
三、补码与取模运算
我们的反码运算在数据溢出时计算发生了错误,然而实际上溢出的时候究竟发生了什么呢,实际上溢出操作相当于数学中的一个运算:取模!
仍然考虑四个比特的情景,四个比特能够储存的最大二进制数为1111,即
15
=
2
3
+
2
2
+
2
1
+
2
0
15=2^3+2^2+2^1+2^0
15=23+22+21+20,当1111再加上1时得到
16
=
2
4
16=2^4
16=24,实际上,而我们的四个比特存储位置上又变成了0000,就相当于我们让得到的数字结果对
2
4
2^4
24取模!
我们继续从数轴上考虑映射关系,我们发现我们建立的反码映射
x
=
0
x=0
x=0与
x
=
65535
x=65535
x=65535的情况都将
x
x
x映射为0;在我们进行大数减小数的时候,数据发生了溢出,即对数字进行了取模操作,发生了法则
f
(
x
)
=
f
(
x
−
65536
)
,
x
≥
65536
f(x)=f(x-65536),x\geq65536
f(x)=f(x−65536),x≥65536对此,我们只需要将我们的反码映射定义域向外所拓展1,即+1,然后继续从中间将定义域切开建立一个新的映射:
f
(
x
)
=
{
x
(
0
<
x
<
32767
)
x
−
65534
(
32367
<
x
<
65535
)
f
(
x
−
65536
)
(
x
>
65535
)
f(x)=\left\{ \begin{aligned} x(0<x<32767)\\ x-65534(32367<x<65535)\\ f(x-65536)(x>65535) \end{aligned} \right.
f(x)=⎩⎪⎨⎪⎧x(0<x<32767)x−65534(32367<x<65535)f(x−65536)(x>65535)
通过这样的映射,我们同时解决了发生溢出后的映射问题与0的唯一性问题,实际上,这就是补码的实现:
负数的补码为反码除符号位的二进制数字再加1,即补码为原码取反再加1;正数的补码不变。
事实上,补码通过取模思想的映射解决了计算机的加减法的所有问题
至此,我们的原码,反码,补码数学之旅到此结束!可能从这种角度理解可能非常晦涩,但这种思考方式却是理解很多计算机设计原理的重要方式!