表达信息
文本已全部编辑完成,正在制作图例。
表达信息
计算机有着自己的语言。无论是不断变化着电压高低的电信号,还是经过一定转换得到的一串又一串的二进制数字,都需要为其定制一些编码方法。能够正确地表达信息,是计算机能够处理信息的必备条件之一。接下来,我们便开始了解一些最基础的有关信息本身的奥秘。
目录
什么是信息
信息无处不在。我们可以说某本书或某部电影中都包含了大量的信息,但却没有办法去精确地比较哪一边信息更多。换言之,在没有出现信息论之前,大家对信息这个概念很模糊,仅能做到不断地获取、解读信息而已,但并不知道信息有量的概念。
在很久以前,信息的定义确实很模糊,因为当时并没有什么办法能衡量信息的大小。直到 1948 年,信息论创始人香农(C. E. Shannon)发表了《A Mathematical Theory of Communication》(《通信的数学原理》)一文,提出了“信息熵(shang)”的概念后,信息才正式地有了确切定义、可以度量,成为了后来信息学发展的重要基石。
那么,信息用什么度量呢?香农在他的论文中自创了一个新词“bit”(比特),一比特相当于一位二进制数。通过规定确切的编码规则,就可以使用若干比特来表达任意的信息,而这批信息的量便是这若干个比特的数量。例如,有 16 个小球,其中 15 个质量完全一样,但还有一个是比较轻的次品。我们可以将其分为两份并分别放在天平的两侧,其中有一侧必然会因为有次品而翘起。然后我们再将有次品那一侧的 8 个球再次平分并放在天平的两侧,如此往复。直到最后,我们只比较了 4 次就找到了次品球,根本不需要一个一个去比较。如果想将这样的比较结果编码为信息,我们可以规定二进制的“0”为左侧,“1”为右侧,那么就只需要 4 比特长度的信息就能够定位这批球中次品球的位置。
然而,现实生活中的信息量还是太大了,仅仅用比特这种很小的单位显然无法表达更大量的信息,所以就需要像长度、质量、时间那样,定义更高等级的信息计量单位。
需要注意的是,各种存储器的信息计量与计算机的信息计量有点不同:由于计算机使用的进制底数为 2,相邻单位间的换算大小为 1024(也就是 2 的 10 次方);在日常生活中,像内存、硬盘这类存储器则没有此种限制,因此对于它们来讲,相邻单位的换算大小为 1000(也就是 10 的 3 次方)。这也是为什么市场上 U 盘这类存储器的实际容量比在外壳标的容量“小”的原因。
在本文中,我们一概使用相对于计算机的信息度量跨度:
字长与大端序、小端序
我们在日常生活中,经常能接触到计算机的“位数”。无论是在商场中,还是在各种技术论坛,都能或多或少地看到它的身影。
那么,什么是计算机的处理位数?它决定了什么?
简单来讲,计算机的位数就是计算机每一周期要处理的指令长度。例如,16 位的计算机每次都处理 2 字节(也就是 16 比特)。计算机的位数又称作它的字长。常见的计算机字长有 32 位与 64 位。
计算机的字长对计算机的运行速度没有多大关系,但它却对计算机的可扩展性有极大的影响。例如,16 位的计算机内存最大只能有 64 KB(2 的 16 次方字节大小),因为 16 位的二进制数只能表达共 65536 个数字,对应共 65536 个比特的内存地址;32 位的计算机则最高能够支持 4 GB(2 的 32 次方字节大小)的内存寻址,而 64 位计算机在现行标准下最大可以支持 16 EB 的虚拟内存地址的寻址。
规定了计算机的字长,就意味着计算机每次进行运算时就处理它的字长长度的数据。例如,在 32 位的机器上输入一串十六进制数字:
ABCD由于超过了 1 个字节所能表示的数字范围,所以就需要用到多个字节合并在一起来容纳这个数字。在这个例子中,我们恰好仅需要 2 个字节就可以存储,而 32 位机器支持占用 4 字节的数字。
所以,这个 2 字节的数字在 32 位机器下可以这样存储:
(00)(00)(AB)(CD)但对于计算机来讲,各个字节的存储顺序不一定要遵循低位在右、高位在左的规则。更具体的讲,设计者完全可以选择将低位放在左边、高位放在右边:
(CD)(AB)(00)(00)为什么会有两种不同的数字存储方案呢?虽然前一种方案比较符合人们的书写习惯,但以这种方案设计具体的逻辑电路时会很不便(至于为什么,会在“计算器”一文再作解释)。后一种方案虽然看起来不太舒服,但在设计电路时会方便许多。
对于前一种方案,因为大的数位在前、小的数位在后,我们可以称这种方案的数据排列顺序为大端序;对于后一种方案,正好与前一种方案相反,小的数位在前、大的数位在后,这种数据排列顺序便是小端序。
大端序与小端序的由来“大”端序与“小”端序这两个词的由来很有趣,它们都来自于《格列佛游记》:
[正文]
小说作者 Swift 以此抨击了当时英国与法国之间的纷争。后来,早期的网络协议开创者之一 Danny Coken 首次使用了来自这本小说的这两个词,用于描述字节流的顺序。现在,这两个词已经成为了描述计算机的字节序方案的术语了。
计算机的字长与大小端的选择,决定了计算机的基本硬件属性,对计算机一系列的软硬件设计也有重要的意义。
字符编码与 BCD 码
计算机其实只会“0”和“1”,但实际上我们却能看到连接计算机的屏幕上能够显示着各种语言的文字。计算机是通过什么样的方式,才能将原本满是“0”和“1”的世界变得像你正看到的那样,能够被人类理解?要做到这样,首先就需要制定一份字符与数字之间对应的检索表。
影响最为深远、也最为古老的字符检索表是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。这个字符表的原始版本只定义了 128 个字符,包含了所有的数字、大小写字母,还包含了一些基本的符号、不可见的格式控制字符(例如换行、Tab 制表符)。由于只定义了 128 个基本字符,以 ASCII 表示的每个字符均只要 1 字节就足够了,简单且节省空间。
[待添加,部分必要的 ASCII 码表]
另一个在世界上使用最广泛的字符检索表是 Unicode(万国码)。它囊括了世界各族广泛使用的各种语言所需的文字,甚至也包含了古埃及和古巴比伦的语言文字,字符总量在十万左右。一般来讲,每个 Unicode 字符都需要占用 4 字节,这样就能够表达目前定义的所有的字符。不过,你还可以使用一些替代的精简字符集,毕竟有许多字符其实是并不常用的。目前最为常用的精简字符集有 UTF-8 与 UTF-16,它们的每个字符分别只占用 1 字节与 2 字节。
除此之外,还有许多来自各个国家自行制定、使用的字符集,这些字符集之间无法互相兼容。例如,简体中文常用的有 GB2312,繁体中文常用的有 Big5。
有了字符集,就能迈出人机交流的第一步。
字符不能被计算机直接诶用于运算,因为它仅仅起到了为字符“编号”的作用。要想让计算机表达十进制、以十进制进行运算,凭字符集包含的十个十进制数字字符来计算是很低效的方法。为了便于在二进制环境下进行十进制运算,我们可以采用 BCD 码。
BCD 码本质为二进制码,但它专用与在二进制下表示十进制数。一位 BCD 码数字的长度只有 4 比特,但它仅使用其中的 0000 到 1001 (即十进制的 0 到 9),另外的 1010 到 1111 (即十进制的 10 到 15)不使用。BCD 码的四则运算遵循十进制而非二进制的规则,所有的进位、退位均不会使得结果数字中出现 1010 到 1111 (即十进制的 10 到 15)的数字。例如:
待编辑
对于计算机来讲,因为 1 字节(拥有 8 比特)恰好塞得下 2 位 BCD 码,所以我们可以选择 1 字节存储 1 位或 2 位的 BCD 码。例如,我想存储十进制数字 233:
待编辑
前一种有些浪费空间,而后一种看起来便节省多了。我们称前一种表达方式为无压缩的 BCD 码,后一种为带压缩的 BCD 码。二者的区别仅在于空间利用率,两种方案在计算机中的数据处理速度并没有太大的出入。
许多的处理器指令集都为 BCD 码的运算提供了专用的指令,因此现代大部分计算机从硬件上就支持有关 BCD 码的各种运算。BCD 码对于计算器的硬件设计也有很大的作用,因为使用它可以避免有关二进制的不必要转换,提高了性能,也能节约制造成本。
原码、反码与补码
在计算机中,我们可以通过不同的二进制位序列来表示不同的数字,但似乎没有办法表示负数。如果不添加任何规则去处理数字,我们就只能表示 0 与比 0 大的数字了。
为了能让计算机处理负数,我们不妨将已经定好字长的二进制数最开头一位作为符号位。0 表示这是一个正数,1 表示这是一个负数。以 16 位机器举例:
待编辑
为了能让数字有正负属性,我们不得不定义确切的字长,以确定符号位所在的位置。除了符号位以外,这两个数字的其余部分都是一样的。换句话说,不看符号位,绝对值相等的两个数,表示数值的那些位是一致的。像这样仅改变符号位、数值位不受任何影响的二进制编码称为原码。
除了原码外,还有一种较为特殊的编码。它的符号位规则与原码一致,遇到正数也不作任何改变,但遇到负数时所有的数值位都要取反:
待编辑
像这样的二进制编码称为反码,可以理解为原码取反后的样子。
最后,还有一种更为特殊的编码,这种编码才是广泛用于处理器设计的数字编码。它的规则其实与反码的规则差不多,唯一的区别在于它的负数编码要在取反的基础上加 1。
待编辑
像这样的二进制编码称为补码,可以理解为负数的反码“补”加上了个 1。
以这三种方式之一进行编码的数字,均为有符号数。
为什么补码被广泛用于处理器的内部运算呢?因为使用补码可以更方便地进行减法运算。
我们看一个例子:
公式
它相当于:
公式
当我们使用 8 位的机器时,两个加数的补码可以这么表示:
公式
为了让过程更清晰些,这两个数字的相加以竖式演示:
竖式
由于我们规定了这个例子所使用的机器字长为 8 位,所以计算结果超过 8 位的部分便无法存储了,只能存储运算结果的低 8 位,而出现了超出字长导致一部分数据无法存储并被丢弃的情况叫做溢出。去除溢出的部分,其余的部分恰好表示数字 0。实际上,以此方式运算得到的结果恰好与实际运算结果吻合。
这会不会时巧合呢?我们再随便试两个数字:
公式
使用 8 位机器进行补码运算的过程是这样的:
演算过程
为什么会出现这样的情况呢?我们假定一个 8 位 的机器,并由此得知它能表达 0 到 255 的无符号数与 -128 到 127 的有符号数。现在我们分别为这两种表达范围建立数轴:
两个数轴
一个以补码表示的正数与 0 和其无符号数表示的对应关系如下所示:
数轴链接图
这没有什么特别的。重点在于以补码表示的负数与其无符号数表示的对应关系:
数轴链接图
因为计算机有字长的概念,所以在它手里的数字都仅能落在根据它字长所得到的表示范围内。我们可以形象地把它的表示范围的数轴前后连接,成为一个“环”:
数轴环
拿刚刚的例子来讲,122 - 23 在变为补码形式后,从计算机的角度来看就成了 122 + 233:
在数轴环上进行点移动
我们可以看到,当数字相加达到 256 时,实际上相当于点绕着“环”跑了一整圈,这时候会减掉一整圈的数字 256 后继续“跑”,最后得到了结果 99.
注明,这仅仅是感性的形象解释,是为了使补码背后的原理通俗易懂,并不能脱离和替代严谨的数学证明。
补码使得计算机能够化缺点为优点,利用定长的数字存储空间轻松处理减法,并由此还使得计算机能够处理负数。这些编码方式,是信息学中极为基础又极重要的知识。
定点数与浮点数
定点数与浮点数本质上就是小数,只不过是小数在计算机中不同的编码方式,就如同整数在计算机中也有不同的编码方式一样。
定点数就是小数点位置固定的二进制小数。由于小数点的位置早已被定下来,所以一个定点数可被分为符号位、整数与小数部分:
公式
在这个例子中,我们选择使用 16 位的机器,其中整数部分占用 7 位,小数部分占用 8 位。符号位规则与整数源码一致。
使用这种数字时,我们必须预先规定好各部分占据的大小,也就是整数部分分配多大、小数部分分配多大等问题。
浮点数相比较于定点数扩展性更强、应用更广,因为它是小数点位置可以变动的二进制小数。这使用了类似科学计数法的表示方法,我们暂定它有符号、系数(n)与幂(a)三个部分:
公式
为了表达一个浮点数,我们需要将一整个浮点数的存储空间分割成三部分,分别是符号位、系数与幂部分:
公式
我们可以看到,浮点数编码的空间利用要比定点数紧凑的多,因为它将系数部分分割出了很大的空间留给幂。
由于有计算机字长的限制,我们没办法去直接表达超出一定精度的小数,只能取一个尽可能接近于实际数值的近似值。下面的表格演示了精度对近似值的影响。
表格
我们可以看到,精度越高,所表达的数字便越接近真实数值。
为了统一不同处理器的浮点表示,大约在 1985 年时,IEEE(电气和电子工程师协会)专门制定了一个名为 IEEE 浮点的标准。现代计算机目前基本都以此为浮点数表达的标准。业界基本一致采用这种格式,大大增强了有关浮点运算的计算机程序的可移植性。
IEEE 浮点标准的浮点表示有点像本文先前提到的“浮点数”,同样有正负号、系数和冥三个要素,但 IEEE 浮点标准明确规定了它们的位置、长度等,还考虑到了一些特殊情况下的表示方法。
IEEE定义了两种规模的浮点数,分别使用了 4 个字节与 8 个字节存储数字,分别称为单精度浮点数与双精度浮点数。单精度浮点数使用了 8 比特表达幂部分的部分,1 比特表达这个数的符号,剩余的 23 位全部为这个浮点数的系数部分。双精度浮点数所能表达数字的范围则更大,使用了 11 比特表示幂,除去符号位外的剩余 52 位全部为系数部分。
IEEE 还为这些数字的各部分赋予了具体的名称。开头仅一位描述数字正负属性的被称作符号(sign,可简写为 s);在符号之后的一串“幂”数字被称作阶码(exponent);剩余的一大块空间全部作为“系数”,称为尾数(significand)。
位宽示意图
位宽在电路设计与底层开发中,我们经常可以见到以小括号或中括号包裹着一堆以冒号隔开的数字:
(a:b) (a > b ≥ 0)
或
[a:b] (a > b ≥ 0)
像这样的数对称为位宽,表示第 a 位到第 b 位着一段二进制位。一般情况下,这样子表示方法仅在底层开发中才会用到。
需要注意的是,对于计算机硬件来讲,这里的位数是从 0 而非从 1 数起。例如,我们应当将一条宽 32 比特的线路位宽描述为“[31:0]”,其中 0 是 它的最低位,31 是它的最高位。
位宽被广泛应用于硬件电路设计中。例如,在 Verilog 语言中,便以中括号位宽的形式描述线路的具体分配方案。
IEEE 浮点数有三大类型:
规格化的值
这是最常规也最正常的浮点数。从二进制的角度来看,这类数字的阶码部分既不全为 0(相当于不等于 0),也不全为 1(单精度下不等于 255,双精度下不等于 2047):
图
而如果要将这批整数转换为具体的二进制小数,应当用下面的公式:
公式
单精度与双精度下的偏置值有所不同,单精度下为 127,双精度下为 1023。偏置值实质上是由阶码的长度决定的,它恰好为阶码的第一位为 0、其余位全为 1 的二进制值。也因此,阶码可以表示底数的负数次幂,单精度的幂范围为 -127 至 128,双精度的幂范围为 -1023 至 1024。
这里的小数点放在尾码的第一个数字前,且小数点前的整数部分总是为 1。换句话讲,先准备个数字 1.0₂,然后将尾数直接作为这个数字的小数部分组合起来。例如,尾数开头为 1101₂ 且后续位均为 0,那么不考虑其它因素时这个数等于 1.1101₂。
非规格化的值
这种浮点数比较特殊,因为它的阶码值为 0。出现这种情况时,很可能是由于数字已经小到开头 0 的位数太多、没办法达到所需的精度造成的。换句话讲,就是阶码所能表示的数字范围不够大,导致太小的数字只能近似地表示为“0”。这种情况又被称为“逐渐溢出”,即随着数字不断趋近于 0,阶码部分在尽其所能表达到最小的数字后,尾数的有效部分会被逐渐向右(后)“挤”走,最终所有有效数字全部溢出,只剩下一大堆的 0。而当计算机的运算模块遇到这样的情况时,也便会自动将阶码部分全部设置为 0,以表示这个奇怪的数字是溢出得到的。
非规格化的值还有一个特殊的作用,它可以用于表示 0.0₂。如果没有这种特殊规定,由于规格化数字总是无法等于 0(不信你可以拿规格化的值的形式试试看),我们就没有办法表示 0.0₂(其实就是 0)了。
需要注意的是,非规格化的值小数点前的 1 将不再使用,而是改用 0 作为小数点前的整数。
图
对于非规格化的值来讲,符号为虽然仍然存在,但其分别对应的 +0.0 与 -0.0 这两个数字实际上是一个数字(也就是 0)。使用非规格化数字进行加减法没有效果,但进行乘除法时会影响到结果的符号位(即使运算结果仍然是一个非规格化的数)。
特殊值
特殊值时指阶码部分的二进制位全为 1 的浮点数。它还有两种类型
无限大(∞)
无限大浮点数的尾数部分二进制位全部为 0。根据符号位的不同,无限大浮点数还有正负之分。
图
无限大浮点数的运算规则与数学上的定义没有什么区别,但对于无效计算,运算的结果将会是一个 NaN 类型的数字。
NaN(Not a Number)
当尾数的值不为 0 时,我们就称这类数字为 NaN (Not a Number)类型的数字,即“不是一个数”。
图
这种数字通常是由于错误的浮点数运算造成的运算结果。例如 1 ÷ 0、[[根号 -1]]、与 ∞ - ∞ 这类在实数范围不成立的运算,运算结果就是 NaN。
有些时候,程序员为了方便,也可以先将还未初始化的浮点变量初始化为一个 NaN 类型的值,用以标记“这个变量还未初始化,还未能被赋予一个有效的值”。
NaN 类型的数字不能用于正常的浮点运算。任何数字与 NaN 类型浮点数进行任何运算的结果都仍然是一个 NaN 类型的数字。
IEEE 浮点标准很贴近对计算机科学运算的实际需求,因而被普遍使用。现在,它已经成为了现代计算机浮点运算的通用标准。
Last updated