计数系统是一种表示数值的机制。今天,大多数人使用十进制(即基数为10)计数系统,而大多数计算机系统使用二进制(即基数为2)计数系统。弄混这两种计数系统会导致低级的编程错误。
十进制计数系统是阿拉伯人发明的(这是十进制数字被称为阿拉伯数字的原因)。十进制计数系统采用位值记数法来表示数值,只用到了少量符号。位值记数法中的符号除了本身的含义,其位置也有意义。这种机制比其他非位值记数的表示形式要先进得多。图2-1中数字25的tally-slash表示形式说明了非位值记数系统和位值记数系统之间的区别。
图2-1 采用tally-slash计数系统表示的数值25
为了表示数值 n ,tally-slash表示形式一共用了 n 个记号。为了便于理解,大多数人5个5个地将记号分开,如图2-1所示。tally-slash计数系统的优点在于容易统计对象的数量。但这种记数法很笨重,而且很难进行算术运算。tally-slash计数系统的最大问题在于数值所占物理空间较大。数值 n 的表示形式需要的空间与 n 成正比。如果 n 的值特别大,这种表示法就不能用了。
十进制位值计数系统使用由阿拉伯数字组成的字符串来表示数值,数值的整数部分和小数部分可以使用小数点来分隔。字符串中数字的位置影响其含义:小数点左边的每个数字代表的数值(0~9)为该数字乘以10的幂(见图2-2),指数按位递增。小数点左边的第一个数字的数值在0~9之间。当数字字符串超过两位时,小数点左边的第二个数字代表的数值(0~9之间)为该数字乘以10,依此类推。而小数点右边的数字距离小数点越远其数值越小。
图2-2 十迚制位值计数系统
数字字符串123.45代表:
(1×10 2 )+(2×10 1 )+(3×10 0 )+(4×10 -1 )+(5×10 -2 )
或:
100+20+3+0.4+0.05
和tally-slash计数系统相比,基数为10的位值计数系统的功能更强大:
●数值10的表示形式占用的空间只有tally-slash计数系统的1/3。
●数值100的表示形式占用的空间只有tally-slash计数系统的约3%。
●数值1000的表示形式占用的空间只有tally-slash计数系统的约0.33%。
数值越大,空间占比越小。位值计数系统之所以流行正是由于其记法紧凑且容易理解。
人类的双手一共有十根手指(“数字”的英文digit也有手指的意思),因此发明了十进制计数系统。但是,十进制并不是唯一的位值计数系统。实际上,对于大多数运行在计算机上的应用程序来说,十进制并不是最好的计数系统。我们来看看其他计数系统中数值的表示形式。
十进制位值计数系统的每一位都代表10个数字中的某一个再乘以10的幂。十进制数字使用10的幂,因此称为“基数为10”的数字。用另一组数字中的数字乘以另一个基数的幂,就得到了另外一套计数系统。基数又叫作底数,小数点(注意,术语小数点仅适用于十进制数字)左边的每一位数字都要乘以基数的幂,而且指数按位递增。
例如,可以用8个数字符号(0~7)和8的幂(指数按位递增)来创建一个基数为8(八进制)的计数系统。八进制数字123 8 (这里的下标表示基数)等于83 10 :
1×8 2 +2×8 1 +3×8 0
即
64+16+3
基数为 n 的计数系统需要 n 个不同的数字符号。这种计数系统的基数最小是2。如果基数在2~10之间,则使用0~ n -1的阿拉伯数字符号(基数为 n 的系统)。如果基数大于10,则使用字母数字符号a~z或A~Z(忽略大小写)表示大于9的数字。这种约定可以支持最大基数为36(10个数字加上26个字母数字)的计数系统。除此之外,没有更大的计数系统。
在本书中,我们应付的都是基数为2、8及16的数值。因为基数2(二进制)是大多数计算机使用的机器表示形式,基数8在一些老计算机系统上很流行,而基数16更紧凑。许多程序都使用这三个基数,所以要熟悉它们。
我们对基数为2的(二进制)计数系统并不陌生。尽管如此,还是有必要快速回顾一下。二进制计数系统的原理与十进制计数系统一致,差别在于二进制只用到了数字符号0和1(而不是0~9)及2(而不是10)的幂。
那么为什么要费心了解二进制呢?毕竟,几乎每一种计算机语言都允许程序员使用十进制表示形式(十进制表示形式会被自动转换为内部的二进制表示形式)。虽然可以自动转换,但大多数现代计算机系统和I/O设备之间的通信使用的是二进制,运算电路操作的也是二进制数据。许多算法采用二进制表示形式才能正确地运行。充分理解二进制表示形式才能写出好代码。
1.十进制和二进制表示形式的转换
要理解计算机的工作,需要知道十进制和二进制表示形式是如何相互转换的。
要将二进制数值转换为十进制,需要在二进制字符串中逢1加2 i ,这里的 i 是1这个数字的位置,从0开始。例如,二进制数值1100100 2 等于:
1×2 7 +1×2 6 +0×2 5 +0×2 4 +1×2 3 +0×2 2 +1×2 1 +0×2 0
即
128+64+8+2
即
202 10
将十进制数值转换为二进制也很简单。将十进制表示形式转换为对应的二进制表示形式的算法如下:
1.如果数值为偶数,则填写一个二进制字符0。如果数字为奇数,则填写一个二进制字符1。
2.将数字除以2,舍去小数部分或余数。
3.如果商为0,则转换完成。
4.如果商不为0且为奇数,则在二进制字符串之前填1。如果商不为0且为偶数,则在二进制字符串之前填0。
5.回到步骤2并重复后面的步骤。
例如,将十进制数值202转换为二进制:
1.202为偶数,因此填0并除以2(101):0
2.商101为奇数,因此填1并除以2(50):10
3.商50为偶数,因此填0并除以2(25):010
4.商25为奇数,因此填1并除以2(12):1010
5.商12为偶数,因此填0并除以2(6):01010
6.商6为偶数,因此填0并除以2(3):001010
7.商3为奇数,因此填1并除以2(1):1001010
8.商1为奇数,因此填2并除以2(0):11001010
9.商为0,算法转换完成,得到11001010。
2.让二进制数值便于理解
202 10 和1100100 2 虽然数值相等,但一眼就能看出来,十进制表示形式比二进制紧凑。我们需要通过一种方式让二进制数值中的数字(位)更简短、更容易理解。
在美国,为了让较大数值容易理解,很多人把数字分成三个一组,用逗号隔开。例如,1,023,435,208比1023435208更容易阅读和理解。本书也采用了类似的约定,将二进制字符串按四位一组用下画线分隔。例如,二进制数值1010111110110010 2 记为1010_1111_1011_0010 2 。
3.编程语言的二进制数值表示
上文中都是用下标符号(没有下标则代表十进制数值)表示二进制数值。但程序文本编辑器或编程语言编译器通常是无法处理下标的,因此,在标准ASCII文本文件中需要使用不同的方式来表示基数。
通常,只有汇编语言编译器(汇编器)允许程序使用二进制字面值常量。 由于不同的汇编器之间的差异很大,因此汇编语言程序中存在多种表示二进制字面值常量的方法。本书示例使用的是MASM和HLA,因此我们也采用它们的二进制表示约定。
MASM使用以b或B结尾的二进制数字(0和1)字符串表示。MASM源文件中9的二进制表示为1001b。
在HLA中,需要在二进制数值的前面加一个百分号(%)。HLA还允许在二进制字符串中插入下画线以提高可读性,例如:
前面提到,二进制数值的表示形式很冗长。而十六进制表示形式有两个非常重要的特性:一,十六进制表示形式非常紧凑;二,十六进制数值和二进制数值的相互转换非常简单。因此,为了让程序更容易理解,软件工程师通常使用十六进制表示形式。
因为十六进制表示形式的基数是16,所以十六进制小数点左侧的每个数字代表的值为该数字乘以16的幂,指数按位递增。例如,数字1234 16 等于:
1×16 3 +2×16 2 +3×16 1 +0×16 0
即
4096+512+48+4
即
202 10
十六进制表示形式使用字母A~F表示10个标准十进制数字(0~9)以外的6个数字。下面都是合法的十六进制数值:
234 16 DEAD 16 BEEF 16 0AFB 16 FEED 16 DEAF 16
1.编程语言的十六进制数值表示
十六进制表示形式存在一个问题:它(如“DEAD”)容易和标准的程序标识符混淆。因此,编程语言中的十六进制数值大多会加上特殊的前缀或后缀字符。下面列出了几种流行的编程语言的十六进制字面值常量写法:
●C、C++、C#、Java、Swift及其他C衍生编程语言使用前缀0x。十六进制数值DEAD 16 用字符串0xdead表示。
●MASM汇编程序使用后缀h或H。但这不能完全避免标识符和十六进制字面值常量之间的歧义(例如,“deadh”看起来仍然很像MASM的标识符),所以十六进制值还要以数字开头。在数值开头加上0(因为前缀0不会改变数字表示的数值)得到0deadh,这样表示DEAD 16 就不会产生歧义了。
●Visual Basic使用前缀&H或&h。前面的例子DEAD 16 在Visual Basic中表示为&Hdead。
●Pascal(Delphi)使用前缀$。在Delphi/Free Pascal中DEAD 16 被表示为$dead。
●HLA也使用前缀$,并且可以像在二进制中那样在十六进制数字中插入下画线,使数值更容易阅读(例如,$FDEC_A012)。
本书在多数情况下都采用HLA/Delphi/Free Pascal的十六进制表示方法,除非示例使用了其他编程语言。例如,本书中也有一些C/C++示例,因此我们也会经常看到C/C++表示方法。
2.十六进制和二进制表示形式之间的相互转换
二进制表示形式和十六进制表示形式之间的相互转换非常简单,这是十六进制表示形式流行的一个原因。只要记住表2-1中的简单规则即可。
表2-1 二进制和十六进制表示形式转换表
将十六进制数值中的每个数字替换成表中对应的四位二进制数字,就可以转换成二进制表示形式。例如,将$ABCD中的每个十六进制数字转换成表2-1中对应的二进制数字就完成了到二进制格式%1010_1011_1100_1101的转换:
将二进制表示形式转换为十六进制一样简单。首先,确保二进制数值的位数是4的倍数,位数不足则在左边补0。例如,二进制数值1011001010,在左边补两位0就可以变为12位,但数值并不会发生改变:001011001010。然后,将二进制数值分成四位一组:0010_1100_1010。最后,在表2-1中找到这些二进制数字,替换成对应的十六进制数字:$2CA。显而易见,这比从十进制到二进制的转换或十进制到十六进制的转换简单多了。
在早期的计算机系统中,八进制(基数为8)表示形式很常见。即使是现在,这种表示形式还时常出现。八进制非常适合12位和36位(或为3的倍数的位数)的计算机系统,但不适合8位、16位、32位和64位(或其他为2的幂的位数)的计算机系统。尽管如此,一些编程语言仍然允许使用八进制表示形式,而一些较老的UNIX应用程序仍然还在使用八进制表示法。
1.编程语言的八进制数值表示
C语言(及C++和Java等衍生语言)、MASM、Swift和Visual Basic都支持八进制表示法。要能认出这些编程语言中的八进制数值。
●C语言在数字字符串前面加0(零)来表示八进制数。例如,0123等于十进制数值83 10 而不是123 10 。
●MASM使用后缀Q或q。(微软/英特尔选择Q可能是因为它看起来像字母O,不太可能和0弄混。)
●Swift使用前缀0o。例如,0o14代表十进制数值12 10 。
●Visual Basic使用前缀&O(字母O,不是0)。例如,&O123代表十进制值83 10 。
2.八进制和二进制表示形式之间的相互转换
二进制与八进制表示形式之间的转换与二进制与十六进制表示形式之间的转换类似,区别是按三位一组还是四位一组。二进制和相等的八进制表示形式见表2-2。
表2-2 二进制/八进制转换表
将八进制数值中的每个数字替换成表2-2中对应的三位二进制数字,就可以转换成二进制表示形式。例如,将123q 转换为二进制形式为%0_0101_0011:
将二进制字符串分成三位一组(位数不足则在左边补0),然后把每一组三位二进制数字替换成表2-2中对应的八进制数字,就得到了八进制数值。
要将八进制数值转换为十六进制,可以先将八进制数值转换为二进制,再将二进制数值转换为十六进制。