目前,Delphi能够生成运行于x86架构上的代码,基本上只支持Win32。所以,这里要先讲一讲我们所以工作的关键:CPU。
我们常用的PC使用的CPU是x86架构的,Delphi目前的编译器(dcc32.exe)能够生成x86-32代码,也就是x86平台中的32位程序。关于x86这个称呼的来源这里就不介绍了,有兴趣的可以去看看wikipedia中的介绍(http://en.wikipedia.org/wiki/X86)。一个程序的运行,操作系统首先要将程序加载到内存中,然后CPU会从特定的位置开始执行。一般的概念中,程序的运行要将外存(如硬盘)中的程序(数据)加载到内存(RAM)中,再由CPU执行。CPU中有一些寄存器(Registers),用来储存临时数据并执行操作。打个比方的话,在CPU的小环境中,我们可以把寄存器看成“内存”,而内存(RAM)则成了“外存”。在x86-32 CPU中,提供了8个32位的通用寄存器(通用寄存器是最常用的,大部分指令都通过它们完成);这8个32位寄存器的低16位也可以作为单独的16位寄存器使用;其中的4个寄存器的低16位,每个又分别按高/低8位划分为两个8位寄存器。这样,我们的CPU一共支持3种最基本的数据类型:32位、8位、16位,它们又分别可以表示有符号/无符号的整数。在Delphi中,它们分别是:
32 Bit | 16 Bit | 8 Bit | |
signed | Integer, LongInt | SmallInt | ShortInt |
unsigned | Cardinal, LongWord | Word | Byte |
接下来,由于CPU有一些指令可以方便的执行64位整数的基本运算,所以编译器又做了一些额外的支持,让我们可以直接使用64位的整数。根据有无符号,64位整数分 Int64, UInt64 两种。对于一般的应用来说,32位的整数已经基本够用了。需要注意的是,32位的整数最大只能表示4G,而今天的硬盘已经开始用T作单位了,所以64位的整数在一些应用中,尤其是涉及硬盘的操作中也是非常有用的。如果要用再大的数,那就得自己动手实现了。CPU对整数的操作有以下几种类型:
四则运算:包括加法(运算符:+),减法(-),乘法(*),整除(div),取模(mod)。这些就不多介绍了。
位运算:与(and),或(or),异或(xor),取反(not),左移(shl),右移(shr)。除了最后两种位运算外,其它4种位运算是CPU最基本的操作,也是最快的操作。它们的运算规则如下:
按照2进制表示,对某一位上的数进行操作,其中not运算只需要一个操作数,其它三种运算需要两个操作数。and运算中,只有当两个数的相同位都是1时,该位的结果是1,其它都为0。or与and正好相反,只有当两个数位都是0时,结果是0。而xor则是当两个数位不同时返回1,否则为0。not运算是把0的位变为1,1的位变0,整个数相反。
and | 0 | 1 |
0 | 0 | 0 |
1 | 0 | 1 |
or | 0 | 1 |
0 | 0 | 1 |
1 | 1 | 1 |
xor | 0 | 1 |
0 | 0 | 1 |
1 | 1 | 0 |
not | 0 | 1 |
1 | 0 |
按位左移(shl)与右移(shr)这两个运算符的作用,则是向一个方向按位移动,空出来的位用0填齐。对于整数来说,左移n位,就相当于乘上2的n次方;而对于 无符号整数 和 非负有符号整数来说,右移n位则相当于整除2的n次方。对于使用C类语言的人来说,需要注意的是,Delphi的右移不像C/C++/C#/Java那样,对有符号整数最高位按符号位补齐,对无符号整数用0补齐,而是一视同仁的用0补位。
位运算是非常高速的运算,熟练使用它们可以有意想不到的效果。有本很著名的算法书《Hackers Delight》,里面有大量使用位运算完成一些常规运算会很慢的算法。这本书网上很容易找到,建议大概浏览一下有个印象,以后如果遇到的话直接查算法,实现起来会快很多。
接下来再介绍几种Delphi中的基本数据类型,它们和上面的整数类型一起,常被统一称作有序类型。首先是布尔类型家族,包括:Boolean, ByteBool, WordBool, LongBool,它们的宽应分别是8位,8位,16位,32位。其中,Boolean和其它三种的处理方式不同,它的值会被尽力控制在0、1两个数当中,其中0代表False,1代表True;而后三种0代表False,其它非0值代表True。布尔家族也支持and、or、xor、not运算符,这里xor与位运算相同;not运算理论上是not False=True、not True=False,但实际上当对Boolean类型做一些手脚之后可能会得到不同结果(其它3种没问题)。当然,正常使用Boolean类型的话不会产生奇怪的问题,这里只是为了研究稍微提一下。而布尔的and、or运算,在默认的Delphi编译参数条件下,是逻辑运算而不是简单的位运算。
逻辑运算与位运算虽然在理论上运算的结果相同,但实现是有很大不同的。现在有A、B两个Boolean变量,分别计算A and/or B,位运算会直接算两个变量的值。逻辑运算则是当能够确定结果之后,就跳过后面的值,而无需确定所有参与运算的值:
- 对于A and B,如果A为True的话,那么再来计算B的值,由B的值来决定结果。换句话说,如果A为False的话,则不会去关心B的值,直接跳到结果。
- 对于A or B,如果A为False的话,则由B来决定结果;否则A为True则直接得出结果,不会关心B的值。
这里说的都是变量,似乎看不出有什么意义,效果也不明显。但如果A、B不是变量而是函数的话,意义就很大了:由前一个函数的结果来决定后一个函数会不会去执行——而如果是位运算的话,两个函数都要执行完毕才能得到结果,并且位运算不规定函数的执行顺序。
还是向C类语言的人强调一下,Delphi中,位运算与逻辑运算使用相同的运算符,不像C类语言中用&、&&和|、||来区分。Delphi中布尔家族默认使用逻辑运算,但也可以通过预处理指令变成位运算,不过我绝对不推荐把它们改成位运算(事实上我也没试过)。因为改成位运算不但不符合一贯的代码逻辑,而且跟其它语言的方式也不同,不利于养成正确的编程思维。因此我不打算介绍这个预编译选项,有兴趣的话去看看帮助文档就可以了。
接下来还有两大类,一大类则是字符类型,包括8位的AnsiChar与16位的WideChar;还有一大类就是枚举类型,根据编译器参数和枚举类型的成员容量,数据可能会占用1、2、4字节。这里不打算对它们作更详细的介绍,否则篇幅就控制不住了。
对于这些有序类型来说,都能够以其一个连续的值域声明一个子类型。编译器会根据值域分配一个能容纳下它的宽度,并按照该宽度进行处理。
关于基本类型基础的介绍,这里就暂时告一段落了。像浮点数是属于FPU的内容,这里就不介绍了;而像指针之类,虽然本质上也可以看作整数,但在编译器的实现上却是完全不同的处理方式。以后还会对CPU(如endian)、有序类型、字符(字符串)类型、枚举类型、运算的复杂度(速度)分别作更详细的介绍。