程序!程序!程序!—— 数据的表达
大部分计算机使用者感慨于五彩缤纷的应用程序但不知道计算机内部却只认识0和1,这么单一的表达形式如何能够做出那么多伟大的应用程序呢?原因就是计算机对于数据的理解是建立在严格考究的规范上的。

比如:一段二进制内容1010101,它可能是EXCEL程序中展示的一个数值,也有可能是你正在查看照片中的一部分,还有可能是属于正在播放流媒体视频中的一帧。数据形式看似这么单一,但不同的数据类型有不同的规范和要求,而不同的数据类型使得计算机能够被用来构建丰富的应用程序。
为何选用二进制
硬件如何描述数据呢?这么说有些抽象,换句话说,(我们先考虑)如何能够在集成电路(Integrated Circuit)上表示一个数字?集成电路有非常多的引脚,就好像蜈蚣一样,这里看一下集成电路芯片的样子,如下图所示:

如上图所示,芯片长的像蜈蚣一样,那些金色的引脚看起来都一样,但实际每个都有所不同。如果你是芯片的设计师,让你用这个芯片表示一个数字,该如何做呢?感觉挺难,不过我们慢慢来。首先芯片离不开电,它需要通电,毕竟没有电这玩意儿完全无法工作。接下来,如果只考虑一排引脚,可以像十进制数字一样,每个引脚都能表示一个数,这样10个引脚就能表示一个10位的数字了,最大能够表示几十亿呢。那问题就来了,每个引脚如何表示一个数?既然用电,那就用电压来区分,0到9伏表示0到9,岂不美哉?没错,这样完全可以,你设计出了一款十进制的芯片,如下图所示:

如上图所示,这款十进制的芯片被制造出来了,但是实际工作中,电压有些波动,一个引脚实际电压是4.5伏,这表示它的值是4呢?还是5呢?这就要加固整个芯片的电源以及容错机制了,感觉血压随着成本一下就上去了。因此现实中的芯片并没有做的这么灵活,而是只用有电和没电两个状态,也就是说一个引脚,在一个时刻,只会处于两个状态中的一个而已。使用直流电压0伏和5伏两个状态,如果芯片在有直流电源接入的情况下,不同的引脚就会处于不同的状态,如下图所示:

这样下面5个引脚的电压如果从左到右去看就是,0伏、0伏、5伏、0伏和5伏,如果我们认为0伏是0,而5伏是1,那么芯片的引脚输出可以表示为00101。再进一步,如果用二进制来理解00101,它不就是代表着十进制的5么?从工业(或量产需要)以及实现难易度考虑,一个引脚只有两个状态,以及使用二进制来描述数字是综合成本较低的解决方案,由于二进制在计算时还具有十进制不具备的逻辑运算特性,诸如:取反、求交(与或)以及异或,使得建立在布尔代数基础上的二进制运算有了理论依据,这才使得计算机沿着二进制走下去有了底气。
从十进制到二进制,单位信息密度低了不少,两位十进制可以描述的数字,二进制需要更多的位元,那么有没有在密度和实现难易度上更好的进制解决方案呢?实际是有的,记得在汇编语言这门计算机专业课上,老师板书推导过,最合适的进制是自然常数e,也就是2.718…进制,四舍五入,即三进制。如何能够实现3进制呢?可以规定0到3伏为0,4到6伏为1,而7到9伏为2,这样一定程度增加了使用电压来实现不同状态的适应性,但现实世界中谁会这么做呢?其实还真有人这么做,红色苏联就在这条路上探索了一段时间,还真做出来了三进制计算机,但最终没有继续下去,也真是挺可惜的。
二进制数据的表达
既然二进制是计算机存储和计算的基础,也是芯片引脚上非黑即白的状态,那么作为操作计算机的人,我们更加熟悉十进制,并且编写程序时,数值也是以十进制的形式体现的。计算机能够理解二进制,并用二进制进行运算,但是人们输入的十进制内容需要转换为二进制,同样计算机处理完成后的二进制内容也需要再转换为十进制输出,这样就需要看一下这两种进制如何进行互转了。
我们先看一下整数,以二进制数010011为例,可以用位权多项式的方式,计算出十进制,计算过程如下所示:

如上图所示,二进制010011对应十进制的值为19,其中绿色的指数为位权,红色的0或者1是二进制各位对应的值。类似010011,这样的6位二进制数值所能描述的数值是有上限的,并且它需要有6个引脚来对外(或者对内输入)作为物理基础,如果计算的场景不一样,芯片的引脚数量就不一样,那会造成工业界的分裂,无法实施量产,因此,一般以8个引脚为一个概念意义上的单位,称之为字节。
能够一次处理4个字节,即有至少32个引脚的芯片称为32位芯片,而能够一次处理8个字节数据的就是64位芯片了。
以8个引脚来描述一个整数数值听起来挺不容易了,毕竟要堆那么多晶体管,但问题是数值不可能都是正数,还有负数,那么负数该如何表示呢?我们可以使用8个引脚中的最高位引脚,如果它为1则表示是负数,这么以来原来十进制19,8位二进制就是00010011,-19是不是就是10010011呢?其实不是的,负数是使用补码的形式来存储的,计算补码的方式分为两步,如下所示:

如上图所示,经过取反和加1两步之后,得到-19对应的二进制表示形式为11101101,不过补码的表示形式显得不是那么易懂,为什么不使用10010011表示 -19呢?其实是可以的,只不过考虑到后续二进制运算的实现,使用补码表示会有一些优势。比如:考虑19 + (-19),如下图所示:

如上图所示,可以看到面对8位二进制加法,19 + (-19) = 0,在计算实现上会显得容易些,溢出到第9位的数会被舍弃。二进制整数转为十进制可以用多项式的方式进行相加,那么十进制数值如何转为二进制呢?答案是,循环除以2,计算的方式如下图所示:

如上图所示,将19不断的除以2,不能整除的余数会是1,如果能够整除则是0,自下而上就是二进制的值10011,为什么是自下而上呢?答案很简单,在下面的余数毕竟是除了更多的2得来的,在二进制数值中也就需要放在高位。
接着再看一下小数(或者浮点数),二进制形式表示小数还是需要用小数点用来划分整数和小数部分,十进制是这样,二进制也亦然。以二进制1011.0011为例,它代表十进制的小数是多少呢?之前的整数换算使用了多项式,其实小数换算也是一样的,如下图所示:

如上图所示,二进制的小数到十进制的转换方式同整数转换方式一致,就是多项式相加,只是位权在小数点之后为负数,二进制小数1011.0011对应的十进制小数为11.1875。十进制小数到二进制小数的转换方式就和整数转换方式不一样了,对于小数部分,它采用的方式是不断乘以2,然后取余数,以十进制1.625为例,整数部分是1,在二进制中也记为1,接下来看小数部分,也就是0.625,转换过程如下图所示:

如上图所示,二进制小数换算过程通过乘以2,取高于小数点的余数得到十进制0.625对应的二进制数值为0.101。经过上述介绍,我们对二进制与十进制在整数和小数上的互转有了认识,有兴趣的同学可以自己试一试。概念上能够完成数值的互转,还是要考虑一下二进制数值如何在芯片上完成读写,毕竟概念还是要落地的,前面提到了引脚呀、电压呀,那么我们把这个8位的芯片做一下延展,变成这个样子,如下图所示:

如上图所示,先看左上角的IC芯片,它拥有12个引脚,下面8个,上面4个。下面8个引脚很好理解,它们表示8位二进制数值的内容,可以向它们存入值或者读出值,存入和读出就需要感受8个引脚的电压,这就需要VCC(Volt Current Condenser)电路供电电压和GND(Ground)地线,其中VCC是5V的直流电压,时刻有电是IC能够保持状态的基础,如果掉电了,那就不用谈了。外部系统如何能够向芯片中存入数值呢?又如何读出数值呢?这就需要控制信号,即WR(Write)和RD(Read)两个引脚,当它们通电时,操作芯片数据才有意义。
以写入举例,可以先将8位二进制数值按照对应位给予芯片D0至D7这8个引脚进行通电,然后再向WR引脚通电(施加5V电压),这时芯片就会记录当前D0至D7对应的电压,使得给定的8位二进制数值被存储到芯片中。如果需要读出数据,可以向RD引脚通电,然后就可以 “感受”芯片D0至D7引脚对应的电压,而这电压就是先前存入在芯片中的二进制数值。
这么看来,使用芯片来存储二进制数据还是可以做到的,但实际上芯片中需要大量的晶体管来完成这个工作。如果是保存二进制整数,每个数据引脚就可以被设定对应不同的二进制位,这样64位芯片,在数据传输层面就至少需要64+4=68个引脚,虽然实现起来很复杂,但是64位的二进制整数已经可以描述非常大的数值了。整数可以这么做,那小数该怎么做呢?可以把芯片设计成,上面一排数据引脚表示小数点之前的值,下面一排数据引脚表示小数点之后的值,不就可以解决了吗?没错,只要你是计算机的设计者,这么做完全没有问题,但是仔细想想,这种方式描述小数的成本太高,或者说使用的引脚会很多。
考虑一个十进制小数:0.0001,转换成二进制后为:0.000000000000011010,如果使用之前的设计方式,就需要芯片下面至少有18个引脚,这才只是表达了一个十进制的0.0001,如果是十进制的0.00000001呢?可以看出来,这种方案虽然直接,但是无法在寸土寸金的引脚数量上做好文章。那么有没有“压缩”数值表达的方式?其实是有的,科学计数法,真的很科学哦。以十进制的0.00000001为例,就可以记为1 X 10-8,可以看到科学计数法描述的数值包含元素就只有尾数(1)、基数(10)和指数(-8)这三个,描述元素少了,自然就能用更少的引脚数量来支撑更丰富的小数了。以二进制科学计数法为例,相应模式如下图所示:

如上图所示,二进制科学计数法需要四个组成部分来描述一个小数(当然包括大于1的,比如:1.111),这四个组成部分包括:符号、尾数、基数和指数,由于专门用来描述二进制,所以基数就是2,那么就只剩下了符号、尾数和指数,以先前的二进制小数0.000000000000011010为例,它的科学计数法描述形式是:+1.101 X 2-1110,其中 -1110是十进制的 -14,但是它也等同于 +11.01 X 2 -1111,过于自由肯定不是好事,所以就需要有一个标准来描述尾数和基数了。
对于尾数而言,小数点可以左右移动,那么就以 “1.*”这种模式来统一对于小数的描述形式,比如:
(A)1111.1111 = 1.111111 X 211
(B)0.0011 = 1.1 X 2-11
(C)100.0011 = 1.000011 X 210
模式统一后,指数的符号又遇到了问题,毕竟指数也是带符号的呀!难道还要浪费一个指定的引脚?都已经到了这一步了,科学计数法表示的小数运算器都不一定搞得定,那就再拍一下脑袋,对于指数而言,使用一种特殊的表示系统,即EXCESS系统,它将二进制中间值作为0,向左为负数,向右为正数。以8位指数为例:01111111表示0,如果要正数3,那就是01111111 + 11 = 10000010,如果是-1,用EXCESS表示就是01111110。
小数用统一的模式来表达,尾数就确定了,而指数又用EXCESS系统表达,负数也能表示了,这样有限的引脚就能描述很大的小数了。在IEEE规定中,存在32位(或引脚)的单精度小数和64位的双精度小数,它们对于符号、尾数和指数的空间划分如下图所示:

如上图所示,单精度小数是32位的,它由1个符号位,8个指数位和23个尾数位构成,而双精度小数是64位的,它所能表示的范围和精度都要好于单精度小数。以二进制小数1011.0011为例,科学计数法表示为1.0110011 X 211,接着来看一下它在32位float下的存储结构。
首先,符号为是0,表示正数,这个很简单。然后计算指数,二进制11(十进制3)通过EXCESS系统,在8位二进制情况下,可以表示为10000010。最后,计算尾数,尾数为1.0110011,由于尾数的模式是 1.*,所以会省略 1.* ,可以得到7位二进制0110011,由于要放到23位的空间里,所以后面会补0。通过符号、指数和尾数的计算,二进制小数1011.0011在32位的单精度小数下会被表示为:11000001001100110000000000000000,这样32个引脚就能表示小数了。
可以看到小数的表达非常复杂,而小数的计算那就更麻烦了,想想两个指数不同的小数进行运算,感觉头都大了。因此对于计算机而言,整数运算要远比浮点运算简单和高效。