admin 管理员组

文章数量: 1086019


2025年1月1日发(作者:随机数发生器种子)

PASCAL精要

目录

第一章: Pascal历史回顾

第二章: 编写Pascal代码

第三章: 类型、变量及常量

第四章: 用户自定义数据类型

第五章: 语句

第六章: 过程与函数

第七章: 字符串操作

第八章: 内存

第九章: Windows编程

第十章: Variant类型

第十一章: 程序与单元

附录A: 术语表

附录B: 例名表

作者介绍

意大利人,长期从事Delphi开发及教学工作,

《Delphi入门到精通》

《Delphi 开发人员手册》

的作者,本

《Essential Pascal》

1999年10月完成。 进一步了解 Marco Cantù请访问网址 .

译者语

编程技巧、窍门之类的内容在各大编程网站上不难找到,但象 Marco Cantù《Essential Pascal》那样既基础又

有深度的东西实属难见,其中包含了作者多年的工作经验及对Delphi Pascal的深入理解,是一本很实用的参考书,

尤其是初学者定能从中获益。

第一章

Pascal历史回顾

Delphi中使用的面向对象Pascal编程语言并不是Borland公司1995年发布可视化开发环境Delphi时才有的,

它只是已有Borland Pascal产品的简单扩展。 Borland没有发明Pascal,但它推广并扩展了Pascal。

这一章对Pascal语言的历史背景及其发展历程作一简短回顾。

沃斯的Pascal

Pascal 语言最初由瑞士苏黎士理工学院的尼古拉斯-沃斯(Niklaus Wirth)教授在1971年设计, 作为Algol语言

(1960年设计)简化本用于教学目的。

设计Pascal时,许多编程语言业已存在,但只有FORTRAN、C、Assembler、COBOL等少数语言在广泛应用。

Pascal这种新语言的灵魂是其语言规则,Pascal语言规则的管理是通过强健的数据类型概念、强制性的数据类型声

明与程序结构化控制来实现的,当时设计Pascal的初衷是想把这种语言用作程序设计课程的教学工具。

Turbo Pascal

1983年Borland公司推出了世界闻名的Pascal编译器 -- Turbo Pascal,实现了詹森和沃斯(Jensen & Wirth)

在 “Pascal User Manual and Report” 中提出的思想 。由于既简洁功能又强,Turbo Pascal成为当时最畅销的编译

器之一,而且在PC平台上非常流行。

Turbo Pascal中增添了集成开发环境(IDE),在这种开发环境中,你可在与WordStar 兼容的文字编辑器中编

辑代码,可以运行编译器,查看编译错误并直接跳回到包含错误的行中。现在听起来上述功能似乎微不足道,但在

Turbo Pascal之前你不得不退出代码编辑器返回到DOS,然后运行命令行编译器,记下错误行,再打开编辑器跳至

错误行,非常烦琐。

此外,Borland公司的Turbo Pascal 售价只49美元 ,而Microsoft公司的 Pascal 编译器售价几百美元。 Turbo

Pascal 取得多年的成功应归功于Microsoft最终放弃了Pascal 编译器产品。

Delphi中的Pascal

随着Turbo Pascal 编译器从第一版发布到第九版,Pascal语言得到了不断的发展,1995年Borland发布了

Delphi ,使Pascal成为一种可视化编程语言。

Delphi 在很多方面扩展了Pascal语言,其中包括许多面向对象的扩展,这些扩展的风格与Object Pascal有所

不同,同时Delphi 也提高了

Borland Pascal with Objects

编译器的性能。

第二章

编写Pascal代码

进入正题前先谈一下Pascal代码编写风格的问题。“除了遵循语法规则外,你应该怎样来写代码呢?” 关于这个

问题各人答案会有不同,因为各人喜欢的风格不同。总的来说,任何编码风格的目标都是使代码清楚、明晰,采用

某种风格和格式只是一种简略方法,用于更清楚地表达你的代码要干什么。实现代码清楚明晰的基本原则是保持代

码的一致性,也就是无论选用哪种风格,在整个工程中要始终保持同一风格。

注释

在Pascal中,注释括在大括号中或带星号的圆括号中。Delphi 也认可C++ 风格的注释,即把注释放在双斜线

后。例如

{this is a comment}

(* this is another comment *)

// this is a comment up to the end of the line

第一种注释方式较简略,使用也较为普遍;第二种方式在欧洲使用较广 ,因为欧洲的键盘缺少大括号;第三种

方式的注释是从C++借用来的,只在32位版本的Delphi中可用,它在给一行代码加短注释时非常有用。

在这本书中我用斜体表示注释,用粗体表示关键词,以此与默认的Delphi语法风格表示一致。

上述三种不同的注释方式有益于进行嵌套注释。例如你要注销一段代码,而代码行中又包含真正的注释行,这

时采用同一种注释方式是不对的:

{ ... code

{comment, creating problems}

... code }

正确的方法是插入第二种注释方式:

{ ... code

//this comment is OK

... code }

注意:如果左大括号或圆括号-星号后面跟美元符号($),那么其中的内容就成了编译指令,如 {$X+}。

实际上,编译指令仍是注释。例如,{$X+ This is a comment} 是合法的。这既是有效的编译指令又是一条注释,尽

管明智的程序员很可能会注意把编译指令和注释分开。

使用大写字母

Pascal 编译器(不象其他语言的编译器)不考虑字符的大小写,因此标识符Myname、 MyName、 myname、

myName、 和MYNAME是完全相同的。总体上来说,这是Pascal的一大优点,因为在大小写敏感的语言中,许多

语法错误是由不正确的大写引起的。

注意:Pascal语言的大小写不敏感特性有一个例外:控件包中的Register 过程必须以大写字母R开始,因为需要与

C++Builder 兼容。

然而大小写不敏感也有不便之处:第一,你必须注意大小写不一致的标识符实际上是相同的,以避免把他们当

成不同的元素使用;第二,你必须尽量保持大写使用的一致性,以提高代码的可读性。

大写使用的一致性不是编译器强制要求的,但是保持大写使用的一致性是值得提倡的好习惯。一个常用的方法

是将每个标识符的第一个字母大写,标识符若由几个词组合而成(中间不能插入空格),每个词的第一个字母应大写:

MyLongIdentifier

MyVeryLongAndAlmostStupidIdentifier

此外,编译器不编译代码中的空格、空行和Tab键空格,这些元素通称为空白,它们只用来提高代码的可读性,

不影响编译过程。

不同于BASIC, Pascal 语句允许分行书写,即将一条长指令分割成两个或更多的代码行。允许语句分行的缺点

(至少对许多BASIC程序员)是:语句结束时不能忘了加分号,更确切地说,必须记着把语句和紧接它的语句分开。

语句分行唯一的限制是字符串不能跨行。

关于空格和语句分行的使用没有既定的规则,以下是几点经验:

• Delphi 代码编辑器中有一条竖线叫右边线(Right Margin),你可以把右边线设置在60或70个字符处。如果

以这条线为基准,代码不超过这条界限,那么打印到纸上的代码看起来会很好看。否则,打印时长语句会被

随意分行,甚至在一个词的中间断开。

当一个函数或过程有多个参数,通常的做法是把各参数放在不同的行上。

你可以在注释行前留一行空白,或把长的代码句分成较小的部分,这样能提高代码的可读性。

用空格隔开函数调用的参数,表达式中的运算符也最好用空格隔开。一些程序员可能会对这些提议不以为然,

但我坚持认为:空格是免费的,你不必为使用空格付费,何乐而不为呢?

优化版面

关于代码编写风格的最后一条建议是:尽量使用空白优化版面。这一条很容易做到,只需要在写复合句时,以

上一句为参照,下一句向右缩进两个空格,复合句内嵌的复合句缩进四个空格,依此类推。例如:

if ... then

statement;

if ... then

begin

statement1;

statement2;

end;

if ... then

begin

if ... then

statement1;

statement2;

end;

相似的缩进格式常用于变量或数据类型声名区,也可用于语句的续行:

type

Letters = set of Char;

var

Name: string;

begin

{ long comment and long statement, going on in the

following line and indented two spaces }

MessageDlg (

'This is a message'

,

mtInformation, [mbOk], 0);

提出以上代码编写格式只是向你建个议而已,这样代码能更加易读,其实代码格式并不影响编译结果。在本书

的例子和代码段中我始终坚持使用上述代码风格,Delphi 中的源代码、手册和帮助例子均采用了相似的格式化风格。

突出Pascal元素

为了使Pascal 代码更易读写,Delphi 编辑器中增加了Pascal 元素的色彩设置功能,也就是编辑器会用不同的

颜色表示不同的Pascal 元素。缺省情况下,关键字以粗体表示,字符串和注释用蓝色表示(并且常常是斜体)。

用不同色彩显示不同的Pascal 元素对保留字、注释和字符串十分有利,因为着色后你一眼就可以看出拼错的关

键字、没有正常结束的字符串及多行注释。

使用编辑器环境选项对话框中的色彩(Color)页,很容易就能定制各种Pascal 元素的色彩(见图2.1)。如果独

自工作,那么你可随意选择喜欢的颜色。如果是与其他程序员合作,那么应该使用大家统一的标准颜色。我感觉在

同一台计算机上使用我不习惯的色彩配置确实很难受。

图2.1 编辑环境设置对话框

注意:本书中我选用了一种色彩方案来显示源代码清单,希望能使代码更易读。

使用代码模板

Delphi 3 中增加了用于代码编辑的新功能“代码模板”。由于写Pascal 语句时,常常会重复键入相同的一组关键

字,为此Borland 公司开发了名为“代码模板”的新功能,代码模板中存放了能与代码缩略形式对应的完整代码,你

输入缩略代码,然后按Ctrl+J,完整的代码就出现了。例如,你输入arrayd,然后按Ctrl+J,Delphi 编辑器会把你

的文本扩展为:

array [0..] of ;

由于同一种代码结构在预定义的代码模板中通常有多种样式,所以模板中的缩略形式一般加有一个后缀字母,

以便你选用。此外,你也可以只输入缩略形式的头几个字母,如你输ar,然后按Ctrl+J,那么,编辑器中会弹出一

个菜单,菜单中列出了代码缩略形式选项,见图2.2所示。

图2.2 代码模板选项

代码模板可以定制,就是你可以修改已有的模板也可以添加自己常用的代码段。用代码模板输入的代码文本中

通常会出现‘|’字符,它表示输入模板代码后光标应跳到的位置,就是说你应该从这个光标位置开始输入,写完这句代

码。

编程语句

标识符一经定义 ,你就可以在语句及组成语句的表达式中使用它们。Pascal 提供了许多语句和表达式,首先来

看看关键字、表达式和运算符。

关键字

关键字是Object Pascal 的保留标识符,在语言中有着特殊含义。保留字不能用作标识符,指令字也同样不应该

用作标识符,即使编译器允许也最好不用。在实际中你不应该把任何关键字用作标识符。

表2.1是面向对象 Pascal 语言(Delphi 4)中特殊标识符的完整列表,其中包括关键字及保留字。

表2.1:面向对象Pascal语言中的关键字及保留字

关键字

absolute

abstract

and

array

as

asm

assembler

at

automated

begin

作用

指令 (变量)

指令 (方法)

运算符 (布尔)

类型

运算符 (RTTI)

语句

向后兼容 (汇编)

语句 (异常处理)

访问类别符 (类)

块标记

case

cdecl

class

const

constructor

contains

default

destructor

dispid

dispinterface

div

do

downto

dynamic

else

end

except

export

exports

external

far

file

finalization

finally

for

forward

function

goto

if

语句

函数调用协定

类型

声明或指令(参数)

特殊方法

运算符 (集合)

指令 (属性)

特殊方法

dispinterface 界面类别符

类型

运算符

语句

语句 (for)

指令 (方法)

语句 (if 或 case)

块标记

语句 (异常处理)

向后兼容 (类)

声明

指令 (函数)

向后兼容 (类)

类型

单元结构

语句 (异常处理)

语句

函数指令

声明

语句

语句

implementation

单元结构

implements

in

index

inherited

initialization

inline

interface

is

label

指令 (属性)

运算符 (集合) - 工程结构

指令 (dipinterface界面)

语句

单元结构

向后兼容 (见 asm)

类型

运算符 (RTTI)

声明

library

message

mod

name

near

nil

nodefault

not

object

of

on

or

out

overload

override

package

packed

Pascal

private

procedure

program

property

protected

public

published

raise

read

readonly

record

register

reintroduce

repeat

requires

resident

程序结构

指令 (方法)

运算符 (数学)

指令 (函数)

向后兼容 (类)

数值

指令 (属性)

运算符 (布尔)

向后兼容 (类)

语句 (case)

语句 (异常处理)

运算符 (布尔)

指令 (参数)

函数指令

函数指令

程序结构 (控件包)

指令 (记录)

函数调用协定

访问类别符 (class)

声明

程序结构

声明

访问类别符 (类)

访问类别符 (类)

访问类别符 (类)

语句 (异常处理)

属性类别符

dispatch 界面类别符

类型

函数调用协定

函数指令

语句

程序结构 (控件包)

指令 (函数)

resourcestring

类型

safecall

set

shl

shr

函数调用协定

类型

运算符 (数学)

运算符 (数学)

stdcall

stored

string

then

threadvar

to

try

type

unit

until

uses

var

virtual

while

with

write

writeonly

xor

函数调用协定

指令 (属性)

类型

语句 (if)

声明

语句 (for)

语句 (异常处理)

声明

单元结构

语句

单元结构

声明

指令 (方法)

语句

语句

属性类别符

dispatch 界面类别符

运算符 (布尔)

表达式和运算符

建立表达式没有通用的方法,因为要取决于所用的运算符,Pascal包括有逻辑运算符、算术运算符、布尔运算

符、关系运算符和集合运算符等等。表达式可用于确定赋给一个变量的值、计算函数或过程的参数、或者判断一个

条件,表达式也可以包含函数调用。表达式是对一个标识符的值而不是标识符本身进行运算。

所有编程语言中的表达式都是常量、变量、数值、运算符和函数值的合法组合。表达式可以传递给过程或函数

的值参,但不能传递给过程或函数中的引用参数。

运算符及其优先级

如果你以前写过程序,那么你已经知道表达式是什么了。这里我专门讲一下Pascal 运算符的特殊部分:运算符

的优先级。表2.2中按优先级分组列出了Pascal语言的运算符。

与大多数编程语言相反,Pascal语言中and和or运算符的优先级比关系运算符高。因此,如果你的代码为a < b and

c < d,编译器首先会编译and运算符,由此导致编译出错。为此你应该把每个 < 表达式用小括号括起来: (a < b)

and (c < d)。

同一种运算符用于不同数据类型时它的作用不同。例如,运算符 + 可以计算两个数字的和、连接两个字符串、

求两个集合的并集、甚至给PChar 指针加一个偏移量。然而,你不能象在C语言中那样将两个字符相加。

另一个特殊的运算符是 div。在Pascal 中,你能用 / 计算两个数字(实数或整数)的商,而且你总能得到一个

实型结果。如果计算两个整数的商并想要一个整型结果,那么就需要用 div 运算符。

表 2.2: Pascal语言中的运算符及其优先级

@

取变量或函数的地址(返回一个指针)

not

逻辑取反或按位取反

*

/

相乘或集合交集

浮点相除

div

整数相除

mod

取模 (整数相除的余数)

as

程序运行阶段类型转换 (RTTI运算符)

and

逻辑或按位求和

shl

按位左移

shr

按位右移

+

-

or

相加、集合并集、字符串连接或指针增加一个偏移量

相减、集合差集或指针减少一个偏移量

逻辑或按位或运算

xor

逻辑或按位异或运算

=

判断是否相等

<>

判断是否不相等

<

>

判断是否小于

判断是否大于

<=

判断是否小于或等于,或是否是一个集合的子集

>=

判断是否大于或等于,或是否是一个集合的父集

in

is

判断是否是集合成员

判断对象是否类型兼容 (又一个RTTI运算符)

集合运算符

集合运算符包括并(+)、差(-)、交(*)、成员检测(in),及一些关系运算符。要把一个元素添加到集合

中,你可以采用集合并运算。下面是一个选择字体的Delphi 例子:

Style := Style + [fsBold];

Style := Style + [fsBold, fsItalic] - [fsUnderline];

另一种方法是利用标准过程Include 和Exclude,它们效率更高(但不能用于控件的集合类型属性,因为只能操

纵一个元素):

Include (Style, fsBold);

结束语

从上面内容我们已经了解了Pascal 程序的基本布局,下面开始探究它的细节。先从预定义和自定义数据类型开

始,然后是利用关键词组织编程语句。

第三章

类型、变量及常量

最初的Pascal 语言是以一些简单的概念为基础建立起来的,这些概念现在普遍出现在编程语言中。最重要的概

念当属数据类型,数据类型决定了变量可取的值,以及可在这些值上进行的操作。Pascal 数据类型的概念强于C语

言及早期的BASIC语言,在C语言中算术数据类型是可以互换的,而早期的BASIC语言中根本没有与数据类型相似

的概念。

变量

Pascal 变量在使用前必须声明,声明变量时必须指定一种数据类型。下面是变量声明的例子:

var

Value: Integer;

IsCorrect: Boolean;

A, B: Char;

关键字

var

可以在许多地方使用,例如放在函数或过程的开始部分,用来声明函数或过程的局部变量;也可以

放在单元中,用于声明全程变量。

var

关键字之后是一组变量名列表,每个变量名后跟一个冒号和数据类型名,一行

中可以声明多个变量,如上例中最后一句。

一旦变量的类型被指定,你只能对变量执行该变量类型支持的操作。例如,在判断操作中用布尔值,在数字表

达式中用整型值,你不能将布尔值和整型值混用(在C语言中可以这样)。

使用简单的赋值语句,可写出下面的代码:

Value := 10;

IsCorrect := True;

但下面的语句是不正确的,因为两个变量数据类型不同:

Value := IsCorrect;

// error

在Delphi中编译这句代码,会出现错误信息:

Incompatible types: 'Integer' and 'Boolean'.(类型不兼容:‘整型’

和‘布尔型’)

。象这样的错误通常是编程错误,因为把一个

True

False

的值赋给一个整型变量没有什么意义。你

不该责怪Delphi 提示这样的错误信息,代码中有不对的地方Delphi当然要提出警告。

把变量的值从一种类型转换到另一种类型往往不难做到,有些情况下类型转换会自动实现,不过一般情况下需

要调用特殊的系统函数,通过改变数据内部表示来实现类型转换。

在Delphi 中,当你声明全程变量时,你可以赋给它一个初值。例如,你可以这样写:

var

Value: Integer = 10;

Correct: Boolean = True;

这种初始化方法只能用于全程变量,不能用于过程或方法的变量。

常量

对于在程序运行期间保持不变的值,Pascal 允许通过常量来声明。声明常量不必特定数据类型,但需要赋一个

初值。编译器会根据所赋初值自动选用合适的数据类型。例如:

const

Thousand = 1000;

Pi = 3.14;

AuthorName =

'Marco Cantù'

;

Delphi 根据常量的值来决定它的数据类型。上例中的Thousand 变量,Delphi会选用SmallInt数据类型 (短整

型--能容纳Thousand变量的最小整数类型)。如果你想告诉Delphi 采用特定的类型,你可在声明中加入类型名,方

法如下:

const

Thousand: Integer = 1000;

对于声名的常量,编译器有两种编译选择:第一种为常量分配内存,并把常量的值放入内存;第二种在常量每

次使用时复制常量值。第二种方法比较适合简单常量。

注意:16位的Delphi 允许你在程序运行期间改变已定义的常量值,就象一个变量一样。32位的Delphi为了向后兼

容仍容许这种操作,只要你附加

$J

编译指令,或选择工程选项对话框中

Compiler

(编译器) 页的

Assignable typed

constants

复选框就行。尽管如此,这里我还是要强烈建议万不得以不要使用上述操作,因为把新值赋给常量将使编

译器不能对常量进行优化,与其如此不如直接声明一个变量。

资源串常量

当定义字符串常量时,你可这样写:

const

AuthorName =

'Marco Cantù'

;

从Delphi 3 开始,你可以用另一种方式写:

resourcestring

AuthorName =

'Marco Cantù'

;

上面两个语句都定义了一个常量,也就是定义了一个在程序运行期间保持不变的值,但两者的实现过程却不同,

resourcestring

指令定义的字符串变量将被保存到程序资源的字符串表中。从例子ResStr你可了解资源串的实际

作用,例子中设置了一个按钮, 相应代码如下:

resourcestring

AuthorName =

'Marco Cantù'

;

BookName =

'Essential Pascal'

;

procedure 1Click(Sender: TObject);

begin

ShowMessage (BookName + #13 + AuthorName);

end;

以上代码中的两个字符串将分两行输出显示,因为字符串被分行符

#13

隔开。

有趣的是,当你用资源编辑器打开执行文件时,你会在程序资源中看到你所定义的字符串。这意味着字符串并

没有进入编译代码,而是保存在执行文件 (EXE文件) 的一个单独区域。

注意:简而言之,采用资源的好处一方面可让Windows 来完成有效的内存处理,另一方面不用更改源代码就可实现

程序的本地化 (把字符串翻译成不同的语言)。

数据类型

Pascal 中有多种预定义的数据类型,它们可分为三大类:有序数据类型,实数类型和字符串类型。下面我们先

讨论有序类型和实数类型,字符串类型放在以后讨论。同时这一节还将介绍几种Delphi 库中定义的类型 (不是编译

器预定义的类型),这些类型也可看作是预定义的类型。

Delphi 还包括一种

无类型

的可变数据类型,称作

variant

,在本书的第十章将讨论这一类型。variant是一种无

需类型检测的数据类型,它在Delphi 2 中引入,用于处理OLE Automation(OLE 自动化)。

有序类型

有序类型是建立在概念“顺序”或“序列”基础上的数据类型。你不仅可比较两个有序值的大小,而且可以求取给定

有序值的前驱及后继,或者计算它们的最大或最小值。

三种最重要的预定义有序类型是整数类型、布尔类型和字符类型(Integer,Boolean,Char)。各种类型根据其内

部表示和取值范围不同又可进一步细分。表3.1列出了表示数字的有序数据类型。

表 3.1: 表示数字的有序数据类型

8 bits

ShortInt

-128 to 127

SmallInt

-32768 to 32767

LongInt

Byte

0 to 255

Word

0 to 65,535

LongWord (从 Delphi 4)

16 bits

32 bits

-2,147,483,648 to 2,147,483,647

0 to 4,294,967,295

64 bits

16/32 bits

Int64

Integer

Cardinal

从表中可看到,不同数据类型与不同的数据表示法相对应,这要取决于数据值的数位和符号位。有符号类型的

数值可正可负,但取值范围较小,因为符号位占一个数位。下一节在例Range中说明了每种类型的实际取值范围。

表中最后一组类型标志着16/32,它表明其数值表示方法在16位和32位Delphi中不同,该组的Integer及

Cardinal 类型比较常用,因为它们与CPU内部的数字表示法相对应。

Delphi 4中的整数类型

在 Delphi 3中,Cardinal类型所表示的32位无符号值实际占31位,取值最高为20亿。Delphi 4新增了一种

无符号数字类型--LongWord,它是真正的32位值,取值最高达40亿。现在Cardinal 类型已成了LongWord类型的

别名,只是LongWord能容纳大于20亿的无符号数,而且它的数值表示法与CPU内部数值表示法一致。

Delphi 4 中新增的另一个数据类型是

Int64

类型,这一类型能表示长达18个数字的整数。系统中的有序类型

例程(如High 和Low)、数字例程(如Inc 和 Dec)及字符串转换例程(如IntToStr)都支持这一新类型。反过

来,有两个新增的专用函数

StrToInt64

StrToInt64Def

支持从字符串向数字的转换。

布尔类型

布尔值不同于布尔类型,平时很少用到。ByteBool、 WordBool 和LongBool这三种布尔类型的布尔值比较特殊,

只在Windows API 函数中才用到它们。

在Delphi 3 中,为了与Visual Basic 和 OLE Automation兼容,修改了

ByteBool

WordBool

LongBool

布尔值,将

TRUE

值设置为1,

FALSE

值仍为0;

Boolean

类型布尔值保持不变(

TRUE

为1,

FALSE

为0)。如果在

Delphi 2代码中使用了布尔值显式类型转换 ,那么在以后的Delphi中可能会出错。

字符类型

字符有两种不同的表示法::

ANSIChar

WideChar

。第一种类型代表 8 位的字符,与Windows一直沿用的

ANSI(美国国家标准协会)字符集相应;第二种类型代表 16 位的字符,与Windows NT、Windows 95 和 98支持

的双字节字符(Unicode)相应。在Delphi 3 中,Char 类型字符与ANSIChar一致。切记,不管在什么环境,前 256

个Unicode 字符与ANSI 字符是完全一致的。

也可用数字符号表示,如

#78

。后者还可用

Chr

函数表示为

Chr(78)

常量字符可用代表它们的符号表示,如

‘k’

Ord

函数可作相反的转换

Ord(k)

一般来说,对字母、数字或符号,用代表它们的符号来表示较好;而涉及到特殊字符时用数字符号较好。下面

列出了常用的特殊字符:

#9

跳格 (Tab 键)

#10

换行

#13

回车 (Enter 键)

一个例子:Range

为使你对一些有序类型的不同取值范围有一个认识,我写了一个名为Range 的Delphi程序简例。结果见图3.1。

图3.1 简例Range显示有序数据类型信息(本例中采用整型)

Range 程序基于一个简单的窗体,上面有六个按扭 (按有序数据类型命名),还有几个标签(Label)用于显示信息,

见图3.1。窗体最左边的一列标签显示的是静态文本,左边第二列标签在每次单击按扭时显示数据类型信息。

每当你按一下窗体右边的一个按钮,程序就会更新第二列标签的显示内容,显示的内容包括数据类型、字节数、

该类型可存储的最大值和最小值。每个按钮都带有各自的OnClick 事件,因为各自的计算代码略有不同。例如,以

下是Integer按钮(BtnInteger)OnClick 事件的源代码:

procedure egerClick(Sender: TObject);

begin

n :=

'Integer'

;

n := IntToStr (SizeOf (Integer));

n := IntToStr (High (Integer));

n := IntToStr (Low (Integer));

end;

如果你有Delphi 编程经验,你可以看一下程序的源代码,弄明白程序到底是如何工作的。对于初学者,注意一

SizeOf、 High、 Low

这三个函数的使用就可以了。High、 Low两个函数返回与参数相同的有序类型(这里是整

型),SizeOf 函数返回整型数据。函数的返回值先用IntToStr 函数转成字符串,然后赋给三个标签的caption属性 。

其他按钮事件与上面相似,唯一的不同点在于传递给函数的参数类型是不同的。图3.2 显示了Windows 95 下

的16位Delphi编译程序的执行结果。比较图3.1和图3.2,可以看出16位整型和32位整型之间的差异。

图3.2 :16位Delphi中Range程序运行结果显示的整型信息

整型类型的字节大小取决于你所使用的CPU和操作系统。在16位的Windows中,整型变量占两个字节,在32

位的Windows中,整型变量占4个字节。因此,在两个环境中编译的Range程序会得到不同的结果。

Integer

类型在不同版本中的差异并不是个大问题。如果你

如果你的程序对整数类型的字节大小没有特殊要求,

在一个版本中保存了一个整数,那么在另一个版本中取出这个整数时可能会遇到一些问题,这种情况下,你应该采

用平台无关的数据类型如

LongInt

SmallInt

。对于数学计算或非特殊的代码中,你最好的选择是坚持使用平台相

应的标准整型,这就是说,使用CPU最喜欢的整型类型。当处理整数时,

Integer

应是你的首选,不到迫不得已最

好不要采用其他的整型类型。

有序类型系统例程

Pascal 语言和Delphi System 单元中定义了一系列有序类型操作例程,见表 3.2。C++ 程序员会注意到其中的

Inc 例程,它可与 ++ 和 += 运算符对应(Dec 例程也同样)。

表 3.2: 有序类型系统例程

例程 作用

Dec

将例程中的参数值递减1或一个特定的值,其中特定值可在第二个可选参数中定义

Inc

将例程中的参数值增加1或一个特定的值

Odd

如果参数为奇数返回真

Pred

根据参数在其数据类型定义中的序列,返回参数值的前驱值

Succ

返回参数值的后继值

Ord

返回参数值在其数据类型值集合中的序号

Low

返回参数对应的有序数据类型的最小取值

High

返回参数对应的有序数据类型的最大取值

注意,当有些例程用于常量时,编译器会自动用计算值替代例程。例如你调用

High(X)

,设定X为一个整数,

那么编译器会用整数类型中最大的可能值代替这个表达式。

实数类型

实数类型代表不同格式的浮点数。

Single类型

占的字节数最小,为4个字节;其次是

Double

浮点类型,占8

个字节;

Extended

浮点类型,占10个字节。这些不同精度的浮点数据类型都与IEEE( 电气和电子工程师协会)

标准的浮点数表示法一致,并且 CPU数字协处理器直接支持这些类型,处理速度也最快。

Real

类型在Delphi 2 和 Delphi 3 中的定义与 16 位版本一样,都占 6 个字节。不过Borland公司一直不提

倡使用这种类型,而建议用Single、 Double、 Extended 类型代替。这是由于

Real

这种 6 字节的旧格式既不受

Intel CPU 的支持,又没有列在官方的IEEE 实型中。为了完全解决这一问题,Delphi 4 不得不修改

Real

类型的定

义,将其改成标准的 8 字节浮点型, 由此引起了兼容性问题,不过如果有必要,你可以采用下面编译指令克服兼

容性问题,恢复Delphi 2 和 Delphi 3 的

Real

类型定义:

{$REALCOMPATIBILITY ON}

另外还有两种奇怪的数据类型:

Comp

类型和

Currency

类型,

Comp

类型用 8 个字节描述非常大的整数(这

种类型可支持带有 18 位小数的数字);

Currency

类型 (16 位版的Delphi不支持该类型) 表示一个有四位小数位

的值,它的小数位长度是固定的,同Comp 类型一样也占 8 个字节。正如名字所示,Currency 数据类型是为了操

作很精确的四位小数货币数值才添加的。

对实型数据,我们没办法编一个类似Range的程序,因为High 、Low及 Ord函数不能用于实型值。理论上说

实型类型代表一个无限的数字集合;有序类型代表一个有限的数字集合。

注意:让我进一步把上述问题解释一下。对于整数 23,你能确定23 后面的数是什么 ,因为整型数是有限的,它

们有确定的值域范围及排列顺序。而浮点数即使在一个很小的值域范围内也无限、无序。 事实上,在 23 和 24 之

间有多少值? 哪个值是 23.46 后面的值? 23.47 还是 23.461,或者 23.4601? 这是很难说清的。

因此,如问Char 类型字符

w

的顺序位置是有意义的, 但同样的问题对浮点类型数 7134.1562 就毫无意义。

对于一个实型数,你能确切知道有没有比它大的实型数,但是,如想探究给定的实数前到底有多少个实型数(这是

Ord

函数的作用),是得不到结果的。

实型类型在用户界面编程中用得不多,但是Delphi从各方面支持实型类型,包括在数据库方面的支持。由于支

持IEEE浮点数运算标准,Object Pascal 语言完全适合于各类数值计算编程。如果对这部分感兴趣,你可以参考Delphi

在System单元中提供的算术函数(详细见Delphi 帮助)。

注意:Delphi 带有一个

Math

单元,其中定义了一些高级数学例程,这些例程包括三角函数(如

ArcCosh

函数)、

金融函数(如

InterestPayment

函数)和统计函数(如

MeanAndStdDev

过程)。有些例程,它的名字听起来很怪,

MomentSkewKurtosis

例程,它是作什么用的呢? 还是留你自己查吧。

日期和时间

Delphi 也用实型数表示日期和时间数据。但为了更准确起见,Delphi 特别定义了

TDateTime

数据类型,这是

一个浮点类型,因为这个类型必须足够宽,使变量能容纳年、月、日、时、分和秒、甚至毫秒。日期值按天计数,

从1899-12-30开始,放在TDateTime 类型的整数部分;时间值则位于十进制数的小数部分。

TDateTime 不是编译器可直接识别的预定义类型,它在System单元定义:

type

TDateTime = type Double;

使用TDateTime 类型很简单,因为Delphi 为该类型定义了一系列操作函数,表3.3列出了这些函数。

表3.3: TDateTime类型系统例程

Now

Date

Time

DateTimeToStr

返回当前日期及时间

返回当前日期

返回当前时间

按缺省格式将日期和时间值转换为字符串;特定格式转换可用 FormatDateTime函数

DateTimeToString

按缺省格式将日期和时间值拷贝到字符串缓冲区

DateToStr

TimeToStr

将TDateTime值的日期部分转为字符串

将TDateTime值的时间部分转为字符串

FormatDateTime

按特定格式将日期和时间值转换为字符串

StrToDateTime

StrToDate

StrToTime

DayOfWeek

DecodeDate

DecodeTime

EncodeDate

EncodeTime

将带有日期和时间信息的字符串转换为TdateTime类型值,如串有误将引发一个异常

将带有日期信息的字符串转换为TDateTime类型格式

将带有时间信息的字符串转换为TDateTime类型格式

根据传递的日期参数计算该日期是一星期中的第几天

根据日期值返回年、月、日值

根据时间值返回时、分、秒、毫秒值

组合年、月、日值为TDateTime类型值

组合时、分、秒、毫秒值为TDateTime类型值

为了显示怎样使用日期时间类型及其相关例程,我建了一个简单的例子TimeNow。该例子在主窗体中设置了一

个按钮和一个列表框(ListBox)。开始执行时,程序自动计算并显示当前的时间及日期,以后每次单击按钮 ,显示

从程序开始至当前的时间。

下面列出了窗体的OnCreate 事件代码:

procedure eate(Sender: TObject);

begin

StartTime := Now;

(TimeToStr (StartTime));

(DateToStr (StartTime));

(

'Press button for elapsed time'

);

end;

第一句中调用了Now 函数,这个函数返回当前的日期和时间,它的值保存在StartTime 变量中,StartTime 变

量是全程变量,其声明如下:

var

FormTimeNow: TFormTimeNow;

StartTime: TDateTime;

我只添加了第二个声明,第一个是由Delphi自动添加的。默认情况下的代码如下:

var

Form1: TForm1;

窗体名改变后,这个声明被自动更新。使用全程变量实际上不是最好的办法,更好的方法是使用窗体类的私有

域,这涉及到面向对象的编程技术。

接下来的三个语句向位于窗体左面的列表框添加三个条目,结果见图3.3。列表框中的第一行显示了TDateTime

值的时间部分字符串、第二行显示的是同一值的日期部分,最后一行显示了一个简单的提示。

图 3.3:例TimeNow启动时的输出显示

当用户单击Elapsed 按钮时,上图第三行字符串被程序的计算结果代替:

procedure ElapsedClick(Sender: TObject);

var

StopTime: TDateTime;

begin

StopTime := Now;

[2] := FormatDateTime (

'hh:nn:ss'

,

StopTime - StartTime);

end;

这串代码再次计算当前的时间,并显示当前与程序开始之时的时间差,其中用到了其它事件中的计算值,为此

不得不把该值存入全程变量。实际上,最好是采用基于类的变量。

注意:上面代码中所用ListBox的索引号为2,,而它代表的是第三行的显示输出,其原因是listbox的数据项是从零

开始计数的:第一项计为0,第二项为1,第三项为2,依次类推,后面涉及数组时再详细讨论这方面内容。

除了调用TimeToStr和 DateToStr 外,以上例子中还用到了功能强大的FormatDateTime 函数(关于格式化参

数详见Delphi 帮助文件)。需要注意的是:当时间和日期转换成字符串时,其转换格式取决于Windows 的系统设

置。Delphi 从系统中读这些值,并把它们拷贝到SysUtils 单元中声明的几个全程常量中,例如:

DateSeparator: Char;

ShortDateFormat: string;

LongDateFormat: string;

TimeSeparator: Char;

TimeAMString: string;

TimePMString: string;

ShortTimeFormat: string;

LongTimeFormat: string;

ShortMonthNames: array [1..12] of string;

LongMonthNames: array [1..12] of string;

ShortDayNames: array [1..7] of string;

LongDayNames: array [1..7] of string;

大部分全程常量与currency 和浮点数格式化有关,在 Delphi 帮助的

Currency and date/time formatting

variables

主题下,你可找到完整的清单。

注意:Delphi 中有一个DateTimePicker 控件,它提供了选择日期的常用途径,即从一个日历中选择日期。

特定的Windows 类型

到目前为止,我们所看到的预定义数据类型都是Pascal 语言自身定义的类型。 Delphi 中还包含Windows系统

定义的数据类型,这些数据类型不是Pascal语言的组成部分,而是Windows 库的一部分。Windows 类型包括新增

的缺省类型(例如DWORD 或UINT)、各种记录(或结构)类型及指针类型等。

Windows 定义的数据类型中,最重要的类型是句柄(handle),第九章中将讨论这一类型。

类型映射及类型转换

正如所知,你不能把一个变量赋给另一个不同类型的变量,如果你需要这么做,有两种方法供选择。第一种方

法是采用类型映射(Typecasting),它使用一个带有目标数据类型名的函数符号:

var

N: Integer;

C: Char;

B: Boolean;

begin

N := Integer (

'X'

);

C := Char (N);

B := Boolean (0);

你可以在字节长度相同的数据类型之间进行类型映射。在有序类型之间或实型数据之间进行类型映射通常是安

全的,指针类型及对象之间也可以进行类型映射 ,只要你明白自己在做什么。

然而,一般来说类型映射是一种较危险的编程技术,因为它允许你访问一个似是而非的值,该值好象是其它值

的替身。由于数据类型的内部表示法之间通常互相不匹配,所以当遇到错误时会难以追踪,为此你应尽量避免使用

类型映射。

第二种方法是使用类型转换例程。表3.4中总结了各种类型转换例程。其中有些例程所涉及的数据类型将在下

一节中讨论。 注意表中没有包括特殊类型(如TDateTime 和variant)的转换例程,也没包括用于格式化处理的特

殊例程,如

Format

FormatFloat

例程。

表3.4:类型转换系统例程

Chr

Ord

Round

Trunc

Int

IntToStr

IntToHex

StrToInt

StrToIntDef

Val

Str

StrPas

StrPCopy

StrPLCopy

将一个有序数据转换为一个ANSI字符

将一个有序类型值转换为它的序号

转换一个实型值为四舍五入后的整型值

转换一个实型值为小数截断后的整型值

返回浮点数的整数部分

将数值转换为字符串

将数值转换为十六进制数字符串

将字符串转换为一个整型数,如字符串不是一个合法的整型将引发异常

将字符串转换为一个整数,如字符串不合法返回一个缺省值

将字符串转换为一个数字(传统Turbo Pascal例程用于向后兼容)

将数字转换为格式化字符串(传统Turbo Pascal例程用于向后兼容)

将零终止字符串转换为Pascal类型字符串,在32位Delphi中这种类型转换是自动进行的

拷贝一个Pascal类型字符串到一个零终止字符串, 在32位Delphi中这种类型转换是自动进行的

拷贝Pascal类型字符串的一部分到一个零终止字符串

FloatToDecimal

将一个浮点数转换为包含指数、数字及符号的十进制浮点记录类型

FloatToStr

FloatToStrF

FloatToText

将浮点值转换为缺省格式的字符串

将浮点值转换为特定格式的字符串

使用特定格式,将一个浮点值拷贝到一个字符串缓冲区

FloatToTextFmt

同上面例程,使用特定格式,将一个浮点值拷贝到一个字符串缓冲区

StrToFloat

TextToFloat

将一个Pascal字符串转换为浮点数

将一个零终止字符串转换为浮点数

注意:在最近版本的Delphi Pascal 编译器中,Round 函数是以 CPU 的 FPU (浮点部件) 处理器为基础的。

这种处理器采用了所谓的 "银行家舍入法",即对中间值 (如 5.5、6.5) 实施Round函数时,处理器根据小数点前数

字的奇、偶性来确定舍入与否,如 5.5 Round 结果为 6,而 6.5 Round 结果也为6, 因为 6 是偶数。

结束语

本章讨论了Pascal的基本数据类型。Pascal语言还有一个非常重要的特征:它允许编程者自定义数据类型,称

为“用户自定义数据类型”,这在下一章进行讨论。

第四章

用户自定义数据类型

Pascal 语言的一个重要特征是它能自定义数据类型。通过各种类型构造器,你可以定义自己的数据类型,如子

界类型、数组类型、记录类型、枚举类型、指针类型和集合类型。最重要的用户定义数据类型是类(class),类是

Object Pascal的面向对象扩展部分,本书不讨论这部分。

你可能会认为其它编程语言也有诸如此类的类型构造器,确实如此,但是Pascal 是第一个完美实现这一理论的

语言。至今仍然没有语言有能力定义那么多的数据类型。

命名及不命名的类型

为了后续使用或直接用于变量,需要给自定义类型命名。如果自定义一个命名的类型,你必须将代码放在特定

的type区,如下所示:

type

// subrange definition

Uppercase = 'A'..'Z';

// array definition

Temperatures = array [1..24] of Integer;

// record definition

Date = record

Month: Byte;

Day: Byte;

Year: Integer;

end;

// enumerated type definition

Colors = (Red, Yellow, Green, Cyan, Blue, Violet);

// set definition

Letters = set of Char;

你也可使用类型定义构造器直接定义一个变量,此时无需显式命名,如下面的代码:

var

DecemberTemperature: array [1..31] of Byte;

ColorCode: array [Red..Violet] of Word;

Palette: set of Colors;

注意:一般来说,你应该避免使用上述不命名类型,因为你不能把它们作为参数传给例程,也不能用于声名同一类

型的其他变量。实际上,Pascal的类型兼容规则是基于类型名的,而不是基于实际的类型定义。两个类型相同的变

量仍有可能是不兼容的,除非他们的类型有完全相同的名字。对于不命名类型,需要编译器给它分配一个内部名字,

因此对于数据结构复杂的变量,要习惯于定义命名数据类型,你一定不会为此白费工夫的。

但是上述自定义类型有什么意义呢?如果你不太熟悉Pascal类型构造器,通过下面内容你会了解它,此外下面

还谈到了同类构造器在不同语言中的差异,因此如果你已熟知上面例举的类型定义,不妨往下读,你会对其中内容

感兴趣的。最后,我将演示一些Delphi例子,并介绍一些能动态访问类型信息的工具。

子界类型

子界类型定义了某种类型的取值范围(因此定名

subrange

)。你可定义整数类型的子界类型,如取值从1到10

或从100到1000,或者定义字符类型的子界类型,如下所示:

type

Ten = 1..10;

OverHundred = 100..1000;

Uppercase =

'A'

..

'Z'

;

定义子界类型时,你不需要指定基类的名字,而只需提供该类型的两个常数。所用基类必须是有序类型,定义

结果将是另一种有序类型。

如定义一个子界变量,那么赋给该变量的值必须是子界定义范围内的值。下面代码是正确的:

var

UppLetter: UpperCase;

begin

UppLetter :=

'F'

;

以下代码则是不正确的:

var

UppLetter: UpperCase;

begin

UppLetter :=

'e'

;

// compile-time error

以上代码将导致一个编译错误:“

Constant expression violates subrange bounds

”。

如果代之以下面代码:

var

UppLetter: Uppercase;

Letter: Char;

begin

Letter :=

'e'

;

UppLetter := Letter;

Delphi 编译会通过,但在运行时,如果你开启了范围检查编译选项(在工程选项对话框的编译器页设置),你

将得到

Range check error

(范围检测错误)信息。

注意:建议你在开发程序时开启上述编译选项,以使程序更健壮并易于调试。这样即使遇上错误,你也会得到一个

明确的信息而不是难以琢磨的行为。最终完成程序时你可以去掉这个选项,使程序运行得快一些,不过影响很小。

因此我建议你开启所有运行时的检测选项,如溢出检查和堆栈检查,甚至提交程序时仍然保留它们。

枚举类型

枚举类型又是一种自定义有序类型。在枚举类型中,你列出所有该类型可能取的值,而不是指定现有类型的范围。

换句话说,枚举类型是个可取值的序列。见下例:

type

Colors = (Red, Yellow, Green, Cyan, Blue, Violet);

Suit = (Club, Diamond, Heart, Spade);

序列中每个值都对应一个序号,序号从0开始计数。使用

Ord

函数,即可得到一个枚举类型值的序号。例如,

Ord (Diamond)

返回值1。

注意:枚举类型有多种内部表示法。缺省时,Delphi 用8位表示法;如果有多于256个不同的值,则用16位表示

法。还有一种32位表示法,需要与C、C++库兼容时会用到。使用

$Z

编译指令可改变缺省设置,请求更多位的表

示法。

Delphi VCL(可视控件库)在很多地方用了枚举类型。例如,窗体边框类型定义如下:

type

TFormBorderStyle = (bsNone, bsSingle, bsSizeable,

bsDialog, bsSizeToolWin, bsToolWindow);

当属性值是枚举类型时,你可以从Object Inspector显示的下拉列表框中选值,如图4.1所示。

图 4.1 Object Inspector 中的枚举类型属性

Delphi 帮助文件中列出了各种Delphi VCL枚举类型的可能值。你也可以通过

OrdType

程序(可从

下载)查看Delphi 枚举类型、集合类型、子界类型及任何其他有序类型的取值列表。图

4.2为这个例子的输出结果。

图 4.2: 程序 OrdType 显示的枚举类型详细信息

集合类型

集合类型表示一组值,该组值由集合所依据的有序类型定义。定义集合的常用有序类型不多,一般为枚举类型

或子界类型。如果子界类型取值为1..3,那么基于它的集合类型值可以是1、或2、或3、或1和2、或1和3、或2

和3、或取所有3个数、或一个数也没有。

一个变量通常包含该类型对应的一个值,而集合类型可以不包含值、包含一个值、两个值、三个值,或更多,

它甚至可以包含定义范围内所有的值。下面定义一个集合:

type

Letters = set of Uppercase;

现在我可以用上面类型来定义变量,并把原始类型的值赋给变量。为了在集合中表示一组值,需要用逗号将值

隔开,最后用方括号结尾。下例显示了多值、单值和空值的变量赋值:

var

Letters1, Letters2, Letters3: Letters;

begin

Letters1 := ['A', 'B', 'C'];

Letters2 := ['K'];

Letters3 := [];

在Delphi中,集合一般用于表示有多种选择的标记。例如下面两行代码(摘自Delphi库)声明了一个枚举类型,

其中列出了窗口条上可选的图标,并声明了相应的集合类型:

type

TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);

TBorderIcons = set of TBorderIcon;

实际上,给定的窗口中可以没有图标,也可以有一个或多个图标。用Object Inspector设置时(见图4.3),双

击属性名,或单击属性左边的加号,自行选择,从而添加或删除集合中的值。

图 4.3: Object Inspector中的集合类型属性

另一个基于集合类型的属性是字体。字体类型值可以是粗体、斜体、带下画线、带删除线等,一种字型可以既

是斜体又是粗体,也可以没有属性,或者带有全部的属性。因此用集合类型来表示它。你可以象下面代码那样,在

程序中给集合赋值:

:= [];

// no style

:= [fsBold];

// bold style only

:= [fsBold, fsItalic];

// two styles

你也能对一个集合进行许多不同方式的操作,包括把两个相同类型的集合变量相加(或更准确地说,计算两个

集合变量的并集):

:= OldStyle + [fsUnderline];

// two sets

OrdType

放在本书源代码的TOOLS

此外,你可以通过

OrdType

查阅Delphi 控件库中定义的集合类型取值列表。

目录中。

数组类型

数组类型定义了一组指定类型的元素序列,在方括号中填入下标值就可访问数组中的元素。定义数组时,方括

号也用来指定可能的下标值。例如,下面的代码中定义了一个有24个整数的数组:

type

DayTemperatures = array [1..24] of Integer;

在数组定义时,你需要在方括号中填入一个子界类型的值,或者用两个有序类型的常量定义一个新的子界类型,

子界类型指定了数组的有效索引。由于子界类型指定了数组下标值的上界和下界,那么下标就不必象C、C++、JAVA

和其他语言那样必须从零开始。

由于数组下标基于子界类型,因此Delphi 能够对它们进行范围检查。不合法的常量子界类型将导致一个编译时

间错误;如果选上编译器范围检查选项,那么超出范围的下标值将导致一个运行时间错误。

使用上述数组定义方法,定义一个

DayTemperatures

类型的变量如下:

type

DayTemperatures = array [1..24] of Integer;

var

DayTemp1: DayTemperatures;

procedure AssignTemp;

begin

DayTemp1 [1] := 54;

DayTemp1 [2] := 52;

...

DayTemp1 [24] := 66;

DayTemp1 [25] := 67;

// compile-time error

数组可以是多维的,如下例:

type

MonthTemps = array [1..24, 1..31] of Integer;

YearTemps = array [1..24, 1..31, Jan..Dec] of Integer;

这两个数组建立在相同的核心类型上,因此你可用前面定义的数据类型声明它们,如下面代码所示:

type

MonthTemps = array [1..31] of DayTemperatures;

YearTemps = array [Jan..Dec] of MonthTemps;

上例的声明把索引的次序前后调换了一下,但仍允许变量之间整块赋值。例如:把一月份的温度值赋给二月份:

var

ThisYear: YearTemps;

begin

...

ThisYear[Feb] := ThisYear[Jan];

你也能定义下标从零开始的数组,不过这似乎不太合逻辑,因为你需要用下标2来访问数组第三项。然而,

Windows一直沿用了从零开始的数组(因为它是基于C语言的),并且Delphi 控件库也在往这方向靠拢。

使用数组时,你总要用标准函数

Low

High

来检测它的边界,

Low

High

返回下标的下界和上界。强烈建

议使用

Low

High

操作数组,特别是在循环中,因为这样能使代码与数组范围无关,如果你改变数组下标的范围

声明,

Low

High

代码不会受影响;否则,如果代码中有一个数组下标循环体,那么当数组大小改变时你就不得不

更新循环体的代码。

Low

High

将使你的代码更易于维护、更稳定。

注意:顺便提一下,使用

Low

High

不会增加系统运行额外开销。因为在编译时,他们已被转换成常数表达式,

而不是实际函数调用。其他简单的系统函数也是这样。

Delphi主要以数组属性的形式使用数组。我们已经在 TimeNow 例子中看到过数组属性,也就是ListBox控件

Items

属性。下一章讨论Delphi循环时,我将向你介绍更多有关数组属性的例子。

注意:Delphi 4 的Object Pascal中增加了动态数组,所谓动态数组是在运行时动态分配内存改变数组大小。使用动

态数组很容易,不过我认为在这里讨论这类数组不合适。你将在第八章看到对Delphi 动态数组的描述。

记录类型

记录类型用于定义不同类型数据项的固定集合。记录中每个元素,或者说域,有它自己的类型。记录类型定义

中列出了所有域,每个域对应一个域名,通过域名可以访问它。

下面简单列举了记录类型的定义、类型变量的声明以及这类变量的使用:

type

Date = record

Year: Integer;

Month: Byte;

Day: Byte;

end;

var

BirthDay: Date;

begin

:= 1997;

:= 2;

:= 14;

类和对象可以看作是记录类型的扩展。Delphi 库趋向于用类替代记录类型,不过Windows API中定义了许多记

录类型。

记录类型中允许包含variant 域,它表示多个域能公用同一内存区,而且域可以是不同类型(这相应于C语言

中的联合union)。换句话说,你可以通过variant 域或说是一组域访问记录中同一个内存位置,但是各个值仍需区

别对待。variant类型主要用来存贮相似但又不同的数据,进行与类型映射(typecasting)相似的类型转换(自从

typecasting 引入Pascal,已很少用到这种方法了)。虽然Delphi在一些特殊情况下还在用variant 记录类型,但是

现在已经被面向对象技术或其他现代技术代替了。

variant 记录类型的应用不符合类型安全原则,因此不提倡在编程中使用,初学者更是如此。实际上,专家级的

编程人员确实需要用到variant 记录类型,Delphi 库的核心部分就用到了这一类型。不管怎样,除非你是个Delphi 专

家,否则你应避免使用variant记录类型。

指针

指针是存放指定类型(或未定义类型)变量内存地址的变量,因此指针间接引用一个值。定义指针不需用特定

的关键字,而用一个特殊字符,这个特殊字符是脱字符号(^),见下例:

type

PointerToInt = ^Integer;

一旦你定义了指针变量,你就可以用@ 符号把另一个相同类型变量的地址赋给它。见下例:

var

P: ^Integer;

X: Integer;

begin

P := @X;

// change the value in two different ways

X := 10;

P^ := 20;

如果定义了一个指针P,那么P表示指针所指向的内存地址,而P^表示内存所存储的实际内容。因此,在上面

的代码中, P^ 与X相等。

除了表示已分配内存的地址外,指针还能通过

New

例程在堆中动态分配内存,不过当你不需要这个指针时,你

也必须调用

Dispose

例程释放你动态分配的内存。

var

P: ^Integer;

begin

// initialization

New (P);

// operations

P^ := 20;

ShowMessage (IntToStr (P^));

// termination

Dispose (P);

end;

如果指针没有值,你可以把nil 赋给它。这样,你可以通过检查指针是否为nil 判断指针当前是否引用一个值。

这经常会用到,因为访问一个空指针的值会引起一个访问冲突错误,也就是大家知道的“一般保护错”(GPF)。见下

例:

procedure Click(Sender: TObject);

var

P: ^Integer;

begin

P := nil;

ShowMessage (IntToStr (P^));

end;

通过运行例GPF,或者看图4.4,你可以看到上述这种结果。

图 4.4: 访问nil指针引起的系统错误

将上面程序加以修改,访问数据就安全了。现在将一个已存在的局部变量赋给指针,指针使用就安全了,虽然

如此,我还是加上了一个安全检查语句:

procedure eClick(Sender: TObject);

var

P: ^Integer;

X: Integer;

begin

P := @X;

X := 100;

if P <> nil then

ShowMessage (IntToStr (P^));

end;

Delphi 还定义了一个

Pointer

数据类型,它表示无类型的指针(就象C语言中的

void*

)。如果你使用无类型

指针,你应该用

GetMem

例程,而不是

New

例程,因为

GetMem

例程能用于内存分配大小不确定的情况。

实际上,Delphi 中必须使用指针的情况很少,这是Delphi开发环境一个诱人的优点。虽然如此,若要进行高级

编程和完全理解Delphi 对象模型,理解指针是很重要的,因为Delphi 对象模型在幕后使用了指针。

注意:虽然在Delphi中不常使用指针,但是你经常会用一个极为相似的结构--引用(references)。每个对象实例实

际上是一个隐含的指针,或说是对其实际数据的引用,利用引用,你能象用其他数据类型一样使用对象变量。

文件类型

另一个Pascal特定的类型构造器是文件类型(

file)

。文件类型代表物理磁盘文件,无疑是Pascal语言的一个

特殊类型。按下面的方式,你可以定义一个新的数据类型:

type

IntFile = file of Integer;

然后,你就能打开一个与这个结构相应的物理文件、向文件中写入整数、或者从文件中读取当前的值。

Pascal 文件类型的使用很直观,而且Delphi 中也定义了一些控件用于文件保存和装载,以及对数据流和数据库

的支持。

结束语

这一章讨论了自定义数据类型,完成了对Pascal 数据类型体系的介绍,为下一章“语句”作好了准备。语句用于

操作我们所定义的变量。

第五章

语句

如果说数据类型是Pascal 编程的一个基础,那么另一个则是语句。编程语言的语句主要由关键字和操作指令组

成。语句常放在过程或函数中,就象我们将在下一章中看到的那样。现在,我们集中讲解最基本的编程语句。

简单语句和复合语句

Pascal 简单语句中不包含任何别的语句,赋值语句和过程调用即是简单语句的例子。简单语句用分号隔开,如

下所示:

X := Y + Z;

// assignment

Randomize;

// procedure call

用begin 和end 将简单语句括起来即组成复合语句,复合语句用法与普通的Pascal 语句相同,见下例:

begin

A := B;

C := A * 2;

end;

end之前的最后一条语句末尾分号不是必需的,你可以写成:

begin

A := B;

C := A * 2

end;

这两种写法都是正确的。第一种多了一个无用(但也无害)的分号。分号实际上是一个空语句,也就是说,是

一个没有代码的语句。有时,空语句可用在循环体或其他特殊情况中。

注意:虽然最后一条语句末尾的分号没有用,我却总是加上它,并且建议你也这样做。因为有时你可能需要在末尾

添加语句,如果最后没有加分号,你就必须记着加上它,与其如此不如一开始就加上它。

赋值语句

在Pascal 语言中赋值语句用冒号-等号操作符“:=”,对使用其他语言的编程人员来说这是一个奇怪的符号。在其

他语言中用作赋值符号的“=”在Pascal 中用作关系运算符,用于判断是否相等。

注意:赋值和相等判断使用不同的符号,使Pascal 编译器(象C编译器一样)能更快解译源代码,因为这样就不需

要通过检查上下文来判断符号的意义,此外使用不同操作符也使代码更易读。

条件语句

条件语句通过条件检测,判断是否执行该条件语句中包含的语句。条件语句可有两种基本形式:if语句和case

语句。

If语句

对if-then型语句,仅当条件满足时,语句才执行;对if-then-else型,if语句在两条语句中选择一条执行。条件

用布尔表达式建立,这里通过一个简单的Delphi 例子来示范如何写条件语句。首先,创建一个应用程序,在form

上面放两个复选框(check box)和四个按钮(button),不要改变复选框和按钮的名字,双击按钮为其OnClick 事

件添加响应程序。下面是第一个按钮事件代码中一条简单的if语句:

procedure 1Click(Sender: TObject);

begin

// simple if statement

if d then

ShowMessage (

'CheckBox1 is checked'

)

end;

当点击button1,如果第一个复选框中有复选标记,那么这个程序将显示一条消息(见图5.1)。我用了

ShowMessage 函数,因为它是Delphi中最简单的短信息显示函数。

图 5.1: 例IfTest显示的信息

如果点击按钮后没有反应,表明复选框未被选中。对于这种情况,最好能交代得更清楚些,为此在第二个按钮

的代码中,我用了if-then-else 语句:

procedure 2Click(Sender: TObject);

begin

// if-then-else statement

if d then

ShowMessage (

'CheckBox2 is checked'

)

else

ShowMessage (

'CheckBox2 is NOT checked'

);

end;

要注意的是,不能在第一句之后、else 关键词之前加分号,否则编译器将告知语法错误。实际上,if-then-else 语

句是单纯的一条语句,因此不能在语句中间加分号。

if 语句可以很复杂,句子中的条件部分可以是一系列条件(用and、 or 、 not等布尔操作符联接起来),if

语句又可以嵌套另一个if语句,见例IfTest中其它两个按钮的示范代码:

procedure 3Click(Sender: TObject);

begin

// statement with a double condition

if d and d then

ShowMessage (

'Both check boxes are checked'

)

end;

procedure 4Click(Sender: TObject);

begin

// compound if statement

if d then

if d then

ShowMessage (

'CheckBox1 and 2 are checked'

)

else

ShowMessage (

'Only CheckBox1 is checked'

)

else

ShowMessage (

'Checkbox1 is not checked, who cares for Checkbox2?'

)

end;

仔细阅读代码并执行程序,看看你能不能理解整个程序。当你搞不清某种编程结构时,可以先写一个简单程序,

这样可以帮你学习许多东西。你可以再加几个复选框,增加这个简例的复杂程度,并进行各种测试。

Case语句

如果你的if语句变得非常复杂,有时可以用case语句代替它。case语句包括用来选值的表达式、可能值序列或

一个取值范围。这些值应该是常量,并且它们必须唯一,而且应属于有序类型。Case语句最后可以带一个else 语句,

当没有一个标签与选择器的值一致时,执行else语句。下面是两个简单的例子:

case Number of

1: Text :=

'One'

;

2: Text :=

'Two'

;

3: Text :=

'Three'

;

end;

case MyChar of

'+' : Text := 'Plus sign';

'-' : Text := 'Minus sign';

'*', '/': Text := 'Multiplication or division';

'0'..'9': Text := 'Number';

'a'..'z': Text := 'Lowercase character';

'A'..'Z': Text := 'Uppercase character';

else

Text := 'Unknown character';

end;

Pascal语言中的循环

其它编程语言中使用的循环语句,Pascal语言中都有,它们包括

for

while

repeat

语句。如果你用过其

他编程语言,你会发现Pascal中的循环语句没什么特别的,因此这里我只作简要的说明。

For循环

Pascal 中的for循环严格地建立在计数器基础上,循环每执行一次,计数器不是增加一个值就是减小一个值。

下面是一个for语句的简例,用来将前十个数加起来:

var

K, I: Integer;

begin

K := 0;

for I := 1 to 10 do

K := K + I;

同样的for语句可以用正好相反的计数器来写:

var

K, I: Integer;

begin

K := 0;

for I := 10 downto 1 do

K := K + I;

Pascal 中的for循环语句其灵活性比其他语言小(它不能指定1之外的步长),不过简单也容易理解。如果需

判断的条件比较复杂,或想自定义计数器,你可以用while语句或 repeat 语句,而不是for循环语句。

注意:for循环计数器不必非是数字,它可以是任何有序类型的值,例如一个字符或一个枚举类型值。

while语句和repeat语句

while-do

循环语句和

repeat-until

语句的不同点在于

repeat

循环语句的代码至少要执行一次。从下面的简例

很容易理解这一点:

while (I <= 100) and (J <= 100) do

begin

// use I and J to

I := I + 1;

J := J + 1;

end;

repeat

// use I and J to

I := I + 1;

J := J + 1;

until (I > 100) or (J > 100);

从上可见即使

I

J

的初始值大于100,repeat-until循环中的代码也仍会执行一次。

注意:两种循环另一个关键的不同点是,

repeat-until

循环的条件是反向的条件,只要不满足这个条件,循环就执行;

当条件满足时,循环终止。这正好与

while-do

循环相反,

while-do

循环当条件是真值时才执行。为此,我不得不在

上面代码中用反向条件来获得相同的结果。

一个循环语句例子

为了探究循环的细节,让我们看一个Delphi 简例,这个循环例子表现了固定计数器循环和随机计数器循环之间

的差别。建一个新的工程,在主窗体上放一个listbox和两个button,通过设置Object Inspector中的

name

属性分

别命名button为BtnFor 和BtnWhile。你还可以把Caption 属性中的

Btn

去掉,或甚至加上 & ,让跟在 & 后面的

字母成为快捷键。下面是该窗体文本描述:

object Form1: TForm1

Caption = 'Loops'

object ListBox1: TListBox ...

object BtnFor: TButton

Caption = '&For'

OnClick = BtnForClick

end

object BtnWhile: TButton

Caption = '&While'

OnClick = BtnWhileClick

end

end

图 5.2: 单击For按钮后显示的结果

现在,我们分别给两个button 添加OnClick 事件代码。第一个button用一个简单的for循环来显示一列数字,

结果如图5.2。这个循环向listbox中的Items 属性添加一系列字符串。在执行循环之前,需要清除listbox 中的内容。

程序如下:

procedure Click(Sender: TObject);

var

I: Integer;

begin

;

for I := 1 to 20 do

(

'String '

+ IntToStr (I));

end;

第二个button的事件代码稍微复杂点。本例中让while 循环基于一个随机增长的计数器。为实现它,我调用了

Randomize

过程, 用它来重置随机数发生器,还调用了Random 函数, 其取值范围为100, 即函数返回0至99之间

的随机数,随机数序列控制while 循环的执行次数。

procedure leClick(Sender: TObject);

var

I: Integer;

begin

;

Randomize;

I := 0;

while I < 1000 do

begin

I := I + Random (100);

(

'Random Number: '

+ IntToStr (I));

end;

end;

每次点击While按钮,出现的数字都不同,因为这些数字取决于随机数发生器。图5.3显示了两次点击的结果,

可看到不仅每次产生的数字不同,而且数据项数也不同。也就是说,这个while循环执行的次数是随机的。

图 5.3: 按While按钮后显示的结果

注意:用

Break

Continue

系统过程可以改变循环执行的标准流程。

Break

中断循环;

Continue

直接跳至循环测

试句,或使计数器增加一个步长,然后继续循环(除非条件为空或计数器达到最大值)。还有两个系统过程

Exit

Halt

,让你立即从函数或过程中返回,或者终止程序。

With语句

我要讲的最后一种Pascal 语句是With语句,With语句是Pascal编程语言独有的语句,不过最近JavaScript 和

Visual Basic也添加了这种语句,它在Delphi程序设计中很有用。

With语句是一种用于简化代码的语句。如你要访问一个记录类型变量(或一个对象),用With语句就不必每

次重复变量的名字。例如对于以下的记录类型代码:

type

Date = record

Year: Integer;

Month: Byte;

Day: Byte;

end;

var

BirthDay: Date;

begin

:= 1997;

:= 2;

:= 14;

可以用with语句改进后半部分代码,如下:

begin

with BirthDay do

begin

Year := 1995;

Month := 2;

Day := 14;

end;

在Delphi程序中,这种方法能用于访问控件和类变量。现在通过with语句访问列表框的条目,我们重写上面循

环例子的最后部分:

procedure uttonClick(Sender: TObject);

var

I: Integer;

begin

with do

begin

Clear;

// shortcut

Randomize;

I := 0;

while I < 1000 do

begin

I := I + Random (100);

// shortcut:

Add (

'Random Number: '

+ IntToStr (I));

end;

end;

end;

当你使用控件或类时,with语句通常能简化你的代码,尤其对嵌套域。例如,你要改变窗体画笔的宽度和颜色,

你可以写代码如下:

:= 2;

:= clRed;

但如果用With语句代码会更简单:

with do

begin

Width := 2;

Color := clRed;

end;

当编写的代码很复杂时,with语句会很有用,也可省去一些临时变量。但是这样做也有缺点,因为这样将使代

码的可读性变差,特别对有相似或相同属性的对象。

更严重的是,使用with语句可能会在代码中融入微妙的逻辑错误,甚至连编译器都难以发现。例如:

with Button1 do

begin

Width := 200;

Caption :=

'New Caption'

;

Color := clRed;

end;

这段代码改变了按钮的Caption 和 Width属性,但也改变了窗体的Color属性,而不是按钮的颜色!其原因是

TButton 控件没有Color属性, 又由于执行的代码是针对窗体对象的(我们正在写窗体的方法),所以窗体对象即成

为默认的访问对象。如果这样写:

:= 200;

n :=

'New Caption'

;

:= clRed;

// error!

编译器会给出一个错误。通常,由于with语句在当前的块中定义了新的标识符,省略了原有的标识符,可能引

起在同一块内错误地访问另一个标识符(就象上面的这段代码)。即使存在种种缺陷,我还是建议你习惯于使用with

语句,因为with语句确实是非常便利,并且有时也会使代码更容易读懂。

然而,你应该避免使用多个

with

语句,如:

with ListBox1,

这样会使后面的代码非常难读,因为,对该块中定义的每个属性,你都要根据相应的属性以及控件的次序,才

能推出所访问的控件。

注意:说到可读性,要知道Pascal 没有endif 或endcase 语句。如果if语句有一个begin-end 块,那么end标志

语句结束;另外,case语句也总是以一个end结束。所有这些end语句,常常是一个接一个,使代码难以理解,只

有通过缩排跟踪,才能追出一个end所对应的语句。解决这个问题的一个通用办法, 也是使代码更可读的办法,是

在end后面加注释,如下例:

if ... then

...

end;

// if

结束语

本章描述了怎样编写条件语句和循环语句的代码。程序通常被分成例程、过程或函数,而不是把所有语句列成

长长的列表。这是下一章的主题,下一章也将介绍一些Pascal的高级内容。

第六章

过程与函数

例程(routine)是Pascal 的一个重要概念,例程由一系列语句组成,例程名是唯一的,通过例程名你可以多次调

用它,这样程序中只需要一个例程就够了,由此避免了代码多次重复,而且代码也容易修改维护。从这个角度看,

你可以认为例程是一种基本的代码封装机制。介绍完Pascal 例程的语法后,我会回过头来举例说明这个问题。

Pascal 过程与函数

Pascal中的例程有两种形式:过程和函数。理论上说,过程是你要求计算机执行的操作,函数是能返回值的计

算。两者突出的不同点在于:函数能返回计算结果,即有一个返回值,而过程没有。两种类型的例程都可以带多个

给定类型的参数。

不过实际上函数和过程差别不大,因为你可以调用函数完成一系列操作,跳过其返回值(用可选的出错代码或类

似的东西代替返回值);也可以通过过程的参数传递计算结果(这种参数称为引用,下一部分会讲到)。

下例定义了一个过程、两个函数,两个函数的语法略有不同,结果是完全相同的。

procedure Hello;

begin

ShowMessage ('Hello world!');

end;

function Double (Value: Integer) : Integer;

begin

Double := Value * 2;

end;

// or, as an alternative

function Double2 (Value: Integer) : Integer;

begin

Result := Value * 2;

end;

流行的做法是用Result 给函数赋返回值,而不是用函数名,我认为这样的代码更易读。

一旦定义了这些例程,你就可以多次调用,其中调用过程可执行操作;调用函数能计算返回值。如下:

procedure 1Click (Sender: TObject);

begin

Hello;

end;

procedure 2Click (Sender: TObject);

var

X, Y: Integer;

begin

X := Double (StrToInt ());

Y := Double (X);

ShowMessage (IntToStr (Y));

end;

注意:现在不必考虑上面两个过程的语法,实际上它们是方法。只要把两个按钮(button)放到一个Delphi 窗体上,

在设计阶段单击它们,Delphi IDE将产生合适的支持代码,你只需要填上begin 和end 之间的那几行代码就行。编

译上面的代码,需要你在窗体中加一个Edit控件。

现在回到我前面提到过的代码封装概念。当你调用

Double

函数时,你不需要知道该函数的具体实现方法。如

果以后发现了更好的双倍数计算方法,你只需要改变函数的代码,而调用函数的代码不必改变(尽管代码执行速度

可能会加快!)。

Hello

过程也一样,你可以通过改变这个过程的代码,修改程序的输出,

Button2Click

方法会自动

改变显示结果。下面是改变后的代码:

procedure Hello;

begin

MessageDlg (

'Hello world!'

, mtInformation, [mbOK]);

end;

提示:当调用一个现有的Delphi 函数、过程或任何VCL方法时,你应该记住参数的个数及其数据类型。不过,只要

键入函数或过程名及左括号,Delphi 编辑器中会出现即时提示条,列出函数或过程的参数表供参考。这一特性被称

为代码参数(

Code Parameters)

,是代码识别技术的一部分。

引用参数

Pascal 例程的传递参数可以是值参也可以是引用参数。值参传递是缺省的参数传递方式:即将值参的拷贝压入

栈中,例程使用、操纵的是栈中的拷贝值,不是原始值。

当通过引用传递参数时,没有按正常方式把参数值的拷贝压栈(避免拷贝值压栈一般能加快程序执行速度),

而是直接引用参数原始值,例程中的代码也同样访问原始值,这样就能在过程或函数中改变参数的值。引用参数用

关键字

var

标示。

参数引用技术在大多数编程语言中都有,C语言中虽没有,但C++中引入了该技术。在C++中,用符号 &表示引用;

在VB中,没有

ByVal

标示的参数都为引用。

下面是利用引用传递参数的例子,引用参数用

var

关键字标示:

procedure DoubleTheValue (var Value: Integer);

begin

Value := Value * 2;

end;

在这种情况下,参数既把一个值传递给过程,又把新值返回给调用过程的代码。当你执行完以下代码时:

var

X: Integer;

begin

X := 10;

DoubleTheValue (X);

x变量的值变成了20,因为过程通过引用访问了

X

的原始存储单元,由此改变了

X

的初始值。

通过引用传递参数对有序类型、传统字符串类型及大型记录类型才有意义。实际上Delphi总是通过值来传递对

象,因为Delphi对象本身就是引用。因此通过引用传递对象就没什么意义(除了极特殊的情况),因为这样相当于

传递一个引用到另一个引用。

Delphi 长字符串的情况略有不同,长字符串看起来象引用,但是如果你改变了该字符串的串变量,那么这个串

在更新前将被拷贝下来。作为值参被传递的长字符串只在内存使用和操作速度方面才象引用,但是如果你改变了字

符串的值,初始值将不受影响。相反,如果通过引用传递长字符串,那么串的初始值就可以改变。

Delphi 3增加了一种新的参数:out。out参数没有初始值,只是用来返回一个值。out参数应只用于COM过程和函

数,一般情况下最好使用更有效的var参数。除了没有初始值这一点之外,out参数与var参数相同。

常量参数

除了引用参数外,还有一种参数叫常量参数。由于不允许在例程中给常量参数赋新值,因此编译器能优化常参

的传递过程。编译器会选用一种与引用参数相似的方法编译常参(C++术语中的常量引用),但是从表面上看常参

又与值参相似,因为常参初始值不受例程的影响。

事实上,如果编译下面有点可笑的代码,Delphi将出现错误:

function DoubleTheValue (const Value: Integer): Integer;

begin

Value := Value * 2;

// compiler error

Result := Value;

end;

开放数组参数

与C语言不同,Pascal 函数及过程的参数个数是预定的。如果参数个数预先没有确定,则需要通过开放数组来

实现参数传递。

一个开放数组参数就是一个固定类型开放数组的元素。 也就是说,参数类型已定义,但是数组中的元素个数是

未知数。见下例:

function Sum (const A: array of Integer): Integer;

var

I: Integer;

begin

Result := 0;

for I := Low(A) to High(A) do

Result := Result + A[I];

end;

上面通过High(A)获取数组的大小,注意其中函数返回值 Result的应用, Result用来存储临时值。你可通过一

个整数表达式组成的数组来调用该函数:

X := Sum ([10, Y, 27*I]);

给定一个整型数组,数组大小任意,你可以直接把它传递给带开放数组参数的例程,此外你也可以通过

Slice

数,只传递数组的一部分元素(传递元素个数由

Slice

函数的第二个参数指定)。下面是传递整个数组参数的例子:

var

List: array [1..10] of Integer;

X, I: Integer;

begin

// initialize the array

for I := Low (List) to High (List) do

List [I] := I * 2;

// call

X := Sum (List);

如果你只传递数组的一部分,可使用Slice 函数,如下:

X := Sum (Slice (List, 5));

例OpenArr中可见到包括上面的完整代码(见图6.1)。

图 6.1: 单击 Partial Slice 按钮显示的结果

在Delphi 4中,给定类型的开放数组与动态数组完全兼容(动态数组将在第8章中介绍)。动态数组的语法与开放

数组相同,区别在于你可以用诸如

array of Integer

指令定义变量,而不仅仅是传递参数。

类型变化的开放数组参数

除了类型固定的开放数组外,Delphi 还允许定义类型变化的甚至无类型的开放数组。这种特殊类型的数组元素

可随意变化,能很方便地用作传递参数。

技术上,array of const 类型的数组就能实现把不同类型、不同个数元素组成的数组一下子传递给例程。如下面

Format 函数的定义(第七章中你将看到怎样使用这个函数):

function Format (const Format: string;

const Args: array of const): string;

上面第二个参数是个开放数组,该数组元素可随意变化。如你可以按以下方式调用这个函数:

N := 20;

S :=

'Total:'

;

n := Format (

'Total: %d'

, [N]);

n := Format (

'Int: %d, Float: %f'

, [N, 12.4]);

n := Format (

'%s %d'

, [S, N * 2]);

从上可见,传递的参数可以是常量值、变量值或一个表达式。声明这类函数很简单,但是怎样编写函数代码呢?

怎样知道参数类型呢?对类型可变的开放数组,其数组元素与

TVarRec

类型元素兼容。

注意:不要把

TVarRec

记录类型和

Variant

类型使用的

TVarData

记录类型相混淆。这两种类型用途不同,而且互

不兼容。甚至可容纳的数据类型也不同,因为

TVarRec

支持Delphi 数据类型,而TVarData 支持OLE 数据类型。

TVarRec

记录类型结构如下:

type

TVarRec = record

case Byte of

vtInteger: (VInteger: Integer; VType: Byte);

vtBoolean: (VBoolean: Boolean);

vtChar: (VChar: Char);

vtExtended: (VExtended: PExtended);

vtString: (VString: PShortString);

vtPointer: (VPointer: Pointer);

vtPChar: (VPChar: PChar);

vtObject: (VObject: TObject);

vtClass: (VClass: TClass);

vtWideChar: (VWideChar: WideChar);

vtPWideChar: (VPWideChar: PWideChar);

vtAnsiString: (VAnsiString: Pointer);

vtCurrency: (VCurrency: PCurrency);

vtVariant: (VVariant: PVariant);

vtInterface: (VInterface: Pointer);

end;

每种记录都有一个

VType

域,乍一看不容易发现,因为它与实际意义的整型类型数据(通常是一个引用或一个

指针)放在一起,只被声明了一次。

利用上面信息我们就可以写一个能操作不同类型数据的函数。下例的

SumAll

函数,通过把字符串转成整数、字

符转成相应的序号、True布尔值加一,计算不同类型数据的和。这段代码以一个case语句为基础,虽然不得不经常

通过指针取值,但相当简单,:

function SumAll (const Args: array of const): Extended;

var

I: Integer;

begin

Result := 0;

for I := Low(Args) to High (Args) do

case Args [I].VType of

vtInteger: Result :=

Result + Args [I].VInteger;

vtBoolean:

if Args [I].VBoolean then

Result := Result + 1;

vtChar:

Result := Result + Ord (Args [I].VChar);

vtExtended:

Result := Result + Args [I].VExtended^;

vtString, vtAnsiString:

Result := Result + StrToIntDef ((Args [I].VString^), 0);

vtWideChar:

Result := Result + Ord (Args [I].VWideChar);

vtCurrency:

Result := Result + Args [I].VCurrency^;

end;

// case

end;

我已在例OpenArr中加了这段代码,该例在按下设定的按钮后调用

SumAll

函数。

procedure 4Click(Sender: TObject);

var

X: Extended;

Y: Integer;

begin

Y := 10;

X := SumAll ([Y * Y,

'k'

, True, 10.34, '99999']);

ShowMessage (Format (

'SumAll ([Y*Y, ''k'', True, 10.34, ''99999'']) => %n'

, [X]));

end;

在图6.2中,你可以看到调用函数的输出和例OpenArr的窗体。

图 6.2: 例OpenArr的窗体,当按Untype按钮出现的信息框

Delphi 调用协定

32位的Delphi 中增加了新的参数传递方法,称为fastcall:只要有可能,传递到CPU寄存器的参数能多达三个,

使函数调用操作更快。这种快速调用协定(Delphi 3确省方式)可用register 关键字标示。

问题是这种快速调用协定与Windows不兼容,Win32 API 函数必须声明使用stdcall 调用协定。这种协定是

Win16 API使用的原始Pascal 调用协定和C语言使用的cdecl 调用协定的混合体。

除非你要调用外部Windows函数或定义Windows 回调函数,否则你没有理由不用新增的快速调用协定。 在后

面你会看到使用stdcall 协定的例子,在Delphi帮助文件的Calling conventions 主题下,你能找到有关Delphi调用

协定的总结内容。

什么是方法?

如果你使用过Delphi 或读过Delphi 手册,大概已经听说过“方法”这个术语。方法是一种特殊的函数或过程,

它与类这一数据类型相对应。在Delphi 中,每处理一个事件,都需要定义一个方法,该方法通常是个过程。不过一

般“方法”是指与类相关的函数和过程。

你已经在本章和前几章中看到了几个方法。下面是Delphi 自动添加到窗体源代码中的一个空方法:

procedure 1Click(Sender: TObject);

begin

{here goes your code}

end;

Forward 声明

当使用一个标识符(任何类型)时,编译器必须已经知道该标识符指的是什么。为此,你通常需要在例程使用

之前提供一个完整的声明。然而在某些情况下可能做不到这一点,例如过程A调用过程B,而过程B又调用过程A,

那么你写过程代码时,不得不调用编译器尚未看到其声明的例程。

欲声明一个过程或函数,而且只给出它的名字和参数,不列出其实现代码,需要在句尾加forward 关键字:

procedure Hello; forward;

在后面应该补上该过程的完整代码,不过该过程代码的位置不影响对它的调用。下面的例子没什么实际意义,

看过后你会对上述概念有所认识:

procedure DoubleHello; forward;

procedure Hello;

begin

if MessageDlg (

'Do you want a double message?'

,

mtConfirmation, [mbYes, mbNo], 0) = mrYes then

DoubleHello

else

ShowMessage (

'Hello'

);

end;

procedure DoubleHello;

begin

Hello;

Hello;

end;

上述方法可用来写递归调用:即

DoubleHello

调用Hello,而Hello也可能调用

DoubleHello

。当然,必须设置条

件终止这个递归,避免栈的溢出。上面的代码可以在例DoubleH 中找到,只是稍有改动。

尽管 forward 过程声明在Delphi中不常见,但是有一个类似的情况却经常出现。当你在一个单元(关于单元的

更多内容见下一章)的interface 部分声明一个过程或一个函数时,它被认为是一个forward声明,即使没有forward

关键字也一样。实际上你不可能把整个例程的代码放在interface 部分,不过你必须在同一单元中提供所声明例程的

实现。

类内部的方法声明也同样是forward声明,当你给窗体或其组件添加事件时, Delphi会自动产生相应的代码。

在TForm 类中声明的事件是forward 声明,事件代码放在单元的实现部分。下面摘录的源代码中有一个Button1Click

方法声明:

type

TForm1 = class(TForm)

ListBox1: TListBox;

Button1: TButton;

procedure Button1Click(Sender: TObject);

end;

过程类型

Object Pascal 的另一个独特功能是可定义过程类型。过程类型属于语言的高级功能,Delphi 程序员不会经常用

到它。因为后面章节要讨论相关的内容(尤其是“方法指针” Delphi用得特别多),这里不妨先了解一下。如果你是

初学者,可以先跳过这部分,当学到一定程度后再回过头阅读这部分。

Pascal 中的过程类型与C语言中的函数指针相似。过程类型的声明只需要参数列表;如果是函数,再加个返回

值。例如声明一个过程类型,该类型带一个通过引用传递的整型参数:

type

IntProc = procedure (var Num: Integer);

这个过程类型与任何参数完全相同的例程兼容(或用C语言行话来说,具有相同的函数特征)。下面是一个兼

容例程:

procedure DoubleTheValue (var Value: Integer);

begin

Value := Value * 2;

end;

注意:在16位Delphi中,如果要将例程用作过程类型的实际值,必须用far指令声明该例程。

过程类型能用于两种不同的目的:声明过程类型的变量;或者把过程类型(也就是函数指针)作为参数传递给

另一例程。利用上面给定的类型和过程声明,你可以写出下面的代码:

var

IP: IntProc;

X: Integer;

begin

IP := DoubleTheValue;

X := 5;

IP (X);

end;

这段代码与下列代码等效:

var

X: Integer;

begin

X := 5;

DoubleTheValue (X);

end;

上面第一段代码明显要复杂一些,那么我们为什么要用它呢?因为在某些情况下,调用什么样的函数需要在实

际中决定,此时程序类型就很有用。这里不可能建立一个复杂的例子来说明这个问题,不过可以探究一下简单点的

例子,该例名为ProcType。该例比前面所举的例子都复杂,更接近实际应用。

如图6.3所示,新建一个工程,在上面放两个radio按钮和一个push按钮。例中有两个过程,一个过程使参数

的值加倍,与前面的

DoubleTheValue

过程相似;另一个过程使参数的值变成三倍,因此命名为TripleTheValue

图 6.3: 例 ProcType 窗体

procedure TripleTheValue (var Value: Integer);

begin

Value := Value * 3;

ShowMessage (

'Value tripled: '

+ IntToStr (Value));

end;

两个过程都有结果显示,让我们知道他们已被调用。这是一个简单的程序调试技巧,你可以用它来检测某一代

码段是否或何时被执行,而不用在代码中加断点。

当用户按Apply 按钮,程序会根据radio按钮状态选择执行的过程。实际上,当窗体中有两个radio按钮时,你

只能选择一个,因此你只需要在Apply 按钮的OnClick 事件中添加代码检测radio按钮的值,就能实现程序要求。

不过为了演示过程类型的使用,我舍近求远选择了麻烦但有趣的方法:只要用户选中其中一个radio按钮,按钮对应

的过程就会存入过程变量:

procedure RadioButtonClick(Sender: TObject);

begin

IP := DoubleTheValue;

end;

当用户按Apply 按钮,程序就执行过程变量保存的过程:

procedure uttonClick(Sender: TObject);

begin

IP (X);

end;

为了使三个不同的函数能访问IP和 X变量,需要使变量在整个窗体单元中可见,因此不能声明为局部变量(在

一个方法中声明)。一个解决办法是,把这些变量放在窗体声明中:

type

TForm1 = class(TForm)

...

private

{ Private declarations }

IP: IntProc;

X: Integer;

end;

学完下一章,你会更清楚地了解这段代码的意思,目前只要能知道怎样添加过程类型定义、怎样修改相应的代

码就行了。为了用适当的值初始化上面代码中的两个变量,你可以调用窗体的OnCreate 事件(激活窗体后,在Object

Inspector中选择这一事件,或者双击窗体)。此外最好仔细看一看上例完整的源代码。

在第九章的 Windows 回调函数一节,你能看到使用过程类型的实例

函数重载

重载的思想很简单:编译器允许你用同一名字定义多个函数或过程,只要它们所带的参数不同。实际上,编译

器是通过检测参数来确定需要调用的例程。

下面是从VCL的数学单元(Math Unit)中摘录的一系列函数:

function Min (A,B: Integer): Integer; overload;

function Min (A,B: Int64): Int64; overload;

function Min (A,B: Single): Single; overload;

function Min (A,B: Double): Double; overload;

function Min (A,B: Extended): Extended; overload;

当调用方式为Min (10, 20)时,编译器很容易就能判定你调用的是上列第一个函数,因此返回值也是个整数。

声明重载函数有两条原则:

每个例程声明后面必须添加overload 关键字。

例程间的参数个数或(和)参数类型必须不同,返回值不能用于区分各例程。

下面是ShowMsg 过程的三个重载过程。我已把它们添加到例OverDef 中(一个说明重载和确省参数的应用程

序):

procedure ShowMsg (str: string); overload;

begin

MessageDlg (str, mtInformation, [mbOK], 0);

end;

procedure ShowMsg (FormatStr: string;

Params: array of const); overload;

begin

MessageDlg (Format (FormatStr, Params),

mtInformation, [mbOK], 0);

end;

procedure ShowMsg (I: Integer; Str: string); overload;

begin

ShowMsg (IntToStr (I) +

' '

+ Str);

end;

三个过程分别用三种不同的方法格式化字符串,然后在信息框中显示字符串。下面是三个例程的调用:

ShowMsg (

'Hello'

);

ShowMsg (

'Total = %d.'

, [100]);

ShowMsg (10,

'MBytes'

);

令我惊喜的是Delphi的代码参数技术与重载过程及函数结合得非常好。当你在例程名后面键入左圆括号时,窗

口中会显示所有可用例程的参数列表,当你输入参数时,Delphi会根据所输入参数的类型过滤参数列表。从图6.4

你可看到,当开始输入一个常量字符串时,Delphi只显示第一个参数为字符串的两个ShowMsg例程参数列表,滤掉

了第一个参数为整数的例程。

图 6.4: 窗口中代码参数提示条显示的重载例程参数

重载例程必须用overload关键字明确标示,你不能在同一单元中重载没有overload标示的例程,否则会出现错

误信息: "Previous declaration of '' was not marked with the 'overload' directive."。不过你可以重载在其

他单元中声明的例程,这是为了与以前的Delphi版本兼容,以前的Delphi版本允许不同的单元重用相同的例程名。

无论如何,这是例程重载的特殊情况不是其特殊功能,而且不小心会出现问题。

例如在一个单元中添加以下代码:

procedure MessageDlg (str: string); overload;

begin

eDlg (str, mtInformation, [mbOK], 0);

end;

这段代码并没有真正重载原始的MessageDlg 例程,实际上如果键入:

MessageDlg (

'Hello'

);

你将得到一个有意思的错误消息,告诉你缺少参数。调用本地例程而不是VCL的唯一途径是明确标示例程所在

单元,这有悖于例程重载的思想:

eDlg (

'Hello'

);

确省参数

Delphi 4 中添加了一个新功能,即允许你给函数的参数设定确省值,这样调用函数时该参数可以加上,也可以

省略。下例把应用程序全程对象的MessageBox 方法重新包装了一下,用PChar 替代字符串,并设定两个确省值:

procedure MessBox (Msg: string;

Caption: string = 'Warning';

Flags: LongInt = mb_OK or mb_IconHand);

begin

eBox (PChar (Msg),

PChar (Caption), Flags);

end;

使用这一定义,你就可以用下面任一种方式调用过程:

MessBox (

'Something wrong here!'

);

MessBox (

'Something wrong here!'

,

'Attention'

);

MessBox (

'Hello'

,

'Message'

, mb_OK);

从图6.5中可以看到,Delphi的代码参数提示条会用不同的风格显示确省值参数,这样你就很容易确定哪个参

数是可以省略的。

图 6.5: Delphi代码参数提示条用方括号标示确省值参数,调用时可以省略该参数

注意一点,Delphi 不产生任何支持确省参数的特殊代码,也不创建例程的多份拷贝,缺省参数是由编译器在编

译时添加到调用例程的代码中。

使用确省参数有一重要限定:你不能“跳过”参数,如省略第二个参数后,不能把第三个参数传给函数:

MessBox (

'Hello'

, mb_OK);

// error

确省参数使用主要规则:调用时你只能从最后一个参数开始进行省略,换句话说,如果你要省略一个参数,你

必须省略它后面所有的参数。

确省参数的使用规则还包括:

带确省值的参数必须放在参数表的最后面。

确省值必须是常量。显然,这限制了确省参数的数据类型,例如动态数组和界面类型的确省参数值只能是 nil;

至于记录类型,则根本不能用作确省参数。

确省参数必须通过值参或常参传递。引用参数 var不能有缺省值。

如果同时使用确省参数和重载可能会出现问题,因为这两种功能可能发生冲突。例如把以前ShowMsg 过程改

成:

procedure ShowMsg (Str: string; I: Integer = 0); overload;

begin

MessageDlg (Str +

': '

+ IntToStr (I),

mtInformation, [mbOK], 0);

end;

编译时编译器不会提出警告,因为这是合法的定义。

然而编译调用语句:

ShowMsg ('Hello');

编译器会显示

Ambiguous overloaded call to 'ShowMsg'

.(

不明确重载调用ShowMsg

)。注意,这条错误信息指

向新定义的重载例程代码行之前。实际上,用一个字符串参数无法调用ShowMsg 过程,因为编译器搞不清楚你是

要调用只带字符串参数的ShowMsg 过程,还是带字符串及整型确省参数的过程。遇到这种问题时,编译器不得不

停下来,要求你明确自己的意图。

结束语

过程和函数是编程的一大关键,Delphi 中的方法就是与类及对象关联的过程和函数。

下面几章将从字符串开始详细讲解Pascal的一些编程元素。

第七章

字符串操作

Delphi 中字符串的操作很简单,但幕后情况却相当复杂。Pascal 传统的字符串操作方法与Windows 不同,

Windows吸取了C语言的字符串操作方法。32位Delphi中增加了长字符串类型,该类型功能强大,是Delphi 确省

的字符串类型。

字符串类型

在Borland公司的Turbo Pascal和16位Delphi中,传统的字符串类型是一个字符序列,序列的头部是一个长

度字节,指示当前字符串的长度。由于只用一个字节来表示字符串的长度,所以字符串不能超过255个字符。这一

长度限制为字符串操作带来不便,因为每个字符串必须定长(确省最大值为255),当然你也可以声明更短的字符串

以节约存储空间。

字符串类型与数组类型相似。实际上一个字符串差不多就是一个字符类型的数组,因为用[]符号,你就能访问字

符串中的字符,这一事实充分说明了上述观点。

为克服传统Pascal 字符串的局限性,32位Delphi增加了对长字符串的支持。这样共有三种字符串类型:

ShortString 短字符串类型也就是前面所述的传统 Pascal 字符串类型。这类字符串最多只能有255个字符,

与16位Delphi中的字符串相同。短字符串中的每个字符都属于ANSIChar 类型(标准字符类型)。

ANSIString长字符串类型就是新增的可变长字符串类型。这类字符串的内存动态分配,引用计数,并使用了

更新前拷贝(copy-on-write)技术。这类字符串长度没有限制(可以存储多达20亿个字符!),其字符类型

也是ANSIChar 类型。

• WideString 长字符串类型与ANSIString 类型相似,只是它基于WideChar 字符类型,WideChar 字符为双

字节Unicode 字符。

使用长字符串

如果只简单地用String定义字符串,那么该字符串可能是短字符串也可能是ANSI长字符串,这取决于$H 编译

指令的值,$H+(确省)代表长字符串(ANSIString 类型)。长字符串是Delphi 库中控件使用的字符串。

Delphi 长字符串基于引用计数机制,通过引用计数追踪内存中引用同一字符串的字符串变量,当字符串不再使

用时,也就是说引用计数为零时,释放内存。

如果你要增加字符串的长度,而该字符串邻近又没有空闲的内存,即在同一存储单元字符串已没有扩展的余地,

这时字符串必须被完整地拷贝到另一个存储单元。当这种情况发生时,Delphi运行时间支持程序会以完全透明的方

式为字符串重新分配内存。为了有效地分配所需的存储空间,你可以用SetLength 过程设定字符串的最大长度值:

SetLength (String1, 200);

SetLength 过程只是完成一个内存请求,并没有实际分配内存。它只是把将来所需的内存预留出来,实际上并没

有使用这段内存。这一技术源于Windows 操作系统,现被Delphi用来动态分配内存。例如,当你请求一个很大的

数组时,系统会将数组内存预留出来,但并没有把内存分配给数组。

一般不需要设置字符串的长度,不过当需要把长字符串作为参数传递给API 函数时(经过类型转换后),你必

须用SetLength 为该字符串预留内存空间,这一点我会在后面进行说明。

看一看内存中的字符串

为了帮你更好地理解字符串的内存管理细节,我写了一个简例StrRef 。在程序中我声明了两个全程字符串:Str1

和 Str2,当按下第一个按钮时,程序把一个字符串常量赋给第一个变量,然后把第一个变量赋给第二个:

Str1 :=

'Hello'

;

Str2 := Str1;

除了字符串操作外,程序还用下面的StringStatus 函数在一个列表框中显示字符串的内部状态:

function StringStatus (const Str: string): string;

begin

Result :=

'Address: '

+ IntToStr (Integer (Str)) +

', Length: '

+ IntToStr (Length (Str)) +

', References: '

+ IntToStr (PInteger (Integer (Str) - 8)^) +

', Value: '

+ Str;

end;

在StringStatus 函数中,用常量参数传递字符串至关重要。用拷贝方式(值参)传递会引起副作用,因为函数执

行过程中会产生一个对字符串的额外引用;与此相反,通过引用(var)或常量(const)参数传递不会产生这种情况。

由于本例不希望字符串被修改,因此选用常量参数。

为获取字符串内存地址(有利于识别串的实际内容也有助于观察两个不同的串变量是否引用了同一内存区),

我通过类型映射把字符串类型强行转换为整型。字符串实际上是引用,也就是指针:字符串变量保存的是字符串的

实际内存地址。

为了提取引用计数信息,我利用了一个鲜为人知的事实:即字符串长度和引用计数信息实际上保存在字符串中,

位于实际内容和字符串变量所指的内存位置之前,其负偏移量对字符串长度来说是-4(用Length 函数很容易得到这

个值),对引用记数来说是-8。

不过必须记住,以上关于偏移量的内部信息在未来的Delphi版本中可能会变,没有写入正式Delphi文档的特性

很难保证将来不变。

通过运行这个例子,你会看到两个串内容相同、内存位置相同、引用记数为2,如图7.1中列表框上部所示。现

在,如果你改变其中一个字符串的值,那么更新后字符串的内存地址将会改变。这是copy-on-write技术的结果。

图 7.1: 例StrRef显示两个串的内部状态,包括当前引用计数

第二个按钮(Change)的OnClick 事件代码如下,结果如图7.1列表框第二部分所示:

procedure ngeClick(Sender: TObject);

begin

Str1 [2] :=

'a'

;

(

'Str1 [2] := ''a'''

);

(

'Str1 - '

+ StringStatus (Str1));

(

'Str2 - '

+ StringStatus (Str2));

end;

注意,BtnChangeClick 只能在执行完BtnAssignClick 后才能执行。为此,程序启动后第二个按钮不能用(按钮

的Enabled 属性设成False);第一个方法结束后激活第二个按钮。你可以自由地扩展这个例子,用StringStatus 函

数探究其它情况下长字符串的特性。

Delphi 字符串与 Windows PChar字符串

长字符串为零终止串,这意味着长字符串完全与Windows使用的C语言零终止串兼容,这给长字符串使用带来

了便利。一个零终止串是一个字符序列,该序列以一个零字节(或null)结尾。零终止串在Delphi中可用下标从零开始

的字符数组表示,C语言就是用这种数组类型定义字符串,因此零终止字符数组在Windows API 函数(基于C语言)

中很常见。由于Pascal长字符串与C语言的零终止字符串完全兼容,因此当需要把字符串传递给Windows API 函数

时,你可以直接把长字符串映射为PChar 类型。

下例把一个窗体的标题拷贝给PChar 字符串(用API 函数GetWindowText),然后再把它拷贝给按钮的Caption

属性,代码如下:

procedure 1Click (Sender: TObject);

var

S1: String;

begin

SetLength (S1, 100);

GetWindowText (Handle, PChar (S1), Length (S1));

n := S1;

end;

你可以在例LongStr 中找到这段代码。注意:代码中用SetLength函数为字符串分配内存,假如内存分配失败,

那么程序就会崩溃;如果你直接用PChar 类型传递值(而不是象以以上代码那样接受一个值),那么代码会很简单,

因为不需要定义临时字符串,也不需要初始化串。下面代码把一个Label(标签)控件的Caption 属性作为参数传递给

了API函数,只需要简单地把属性值映射为PChar类型:

SetWindowText (Handle, PChar (n));

当需要把WideString 映射为Windows兼容类型时,你必须用PWideChar 代替PChar进行转换,WideString

常用于OLE和 COM 程序。

刚才展现了长字符串的优点,现在谈谈它的弊端。当你把长字符串转换为PChar 类型时可能会引发一些问题,

问题根本在于:转换以后字符串及其内容将由你来负责,Delphi 不再管了。现在把上面Button1Click代码稍作修改:

procedure 2Click(Sender: TObject);

var

S1: String;

begin

SetLength (S1, 100);

GetWindowText (Handle, PChar (S1), Length (S1));

S1 := S1 +

' is the title'

;

// this won't work

n := S1;

end;

程序编译通过,但执行结果会令你惊讶,因为按钮的标题并没变,所加的常量字符串没有添加到按钮标题中。

问题原因是Windows写字符串时(在GetWindowText API调用中),Windows 没有正确设置Pascal 长字符串的长

度。Delphi 仍可以输出该字符串,并能通过零终止符判断字符串何时结束,但是如果你在零终止符后添加更多的字

符,那么这些字符将被忽略。

怎么解决这个问题呢?解决方法是告诉系统把GetWindowText API函数返回的字符串再转换成Pascal字符串。

然而,如果你用以下代码:

S1 := String (S1);

Delphi 系统将不予理睬,因为把一种类型转换为它自己的类型是无用的操作。为获得正确的Pascal 长字符串,

需要你把字符串重新映射为一个PChar 字符串,然后让Delphi 再把它转回到字符串:

S1 := String (PChar (S1));

实际上,你可以跳过字符串转换(S1 := PChar (S1));, 因为在Delphi中Pchar转换到string是自动执行的,

最终代码如下:

procedure 3Click(Sender: TObject);

var

S1: String;

begin

SetLength (S1, 100);

GetWindowText (Handle, PChar (S1), Length (S1));

S1 := String (PChar (S1));

S1 := S1 +

' is the title'

;

n := S1;

end;

另一个办法是用PChar 字符串的长度重新设定Delphi 字符串长度,可以这样写:

SetLength (S1, StrLen (PChar (S1)));

在例LongStr中你可以看到三种方法的结果,分别由三个按钮执行。如果只想访问窗体标题,仅需要用到窗体

对象本身的Caption 属性,没有必要写这段迷糊人的代码,这段代码只是用来说明字符串转换问题。当调用Windows

API 函数时会遇到这种实际问题,那时你就不得不考虑这一复杂情况了。

格式化字符串

使用加号(+)操作符和转换函数(如IntToStr),你确实能把已有值组合成字符串,不过另有一种方法能格式

化数字、货币值和其他字符串,这就是功能强大的Format 函数及其一族。

Format 函数参数包括:一个基本文本字符串、一些占位符(通常由%符号标出)和一个数值数组,数组中每个

值对应一个占位符。例如,把两个数字格式化为字符串的代码如下:

Format (

'First %d, Second %d'

, [n1, n2]);

其中n1和n2是两个整数值,第一个占位符由第一个值替代,第二个占位符由第二个值替代,以此类推。如果

占位符输出类型(由%符号后面的字母表示)与对应的参数类型不匹配,将产生一个运行时间错误,因此设置编译

时间类型检查会有利于Format 函数的使用。

除了%d外,Format 函数还定义了许多占位符,见表7.1。这些占位符定义了相应数据类型的默认输出,你可

以用更深一层的格式化约束改变默认输出,例如一个宽度约束决定了输出中的字符个数,而精度约束决定了小数点

的位数。例如

Format ('%8d', [n1]);

该句把数字n1转换成有8个字符的字符串,并通过填充空白使文本右对齐,左对齐用减号(-) 。

表 7.1: Format函数的占位符

d (decimal)

x

(hexadecimal)

p (pointer)

s (string)

将整型值转换为十进制数字字符串

将整型值转换为十六进制数字字符串

将指针值转换为十六进制数字字符串

拷贝字符串、字符、或字符指针值到一个输出字符串

e (exponential)

将浮点值转换为指数表示的字符串

f (floating

point)

将浮点值转换为浮点表示的字符串

g (general)

n (number)

m (money)

使用浮点或指数将浮点值转换为最短的十进制字符串

将浮点值转换为带千位分隔符的浮点值

将浮点值转换为现金数量表示的字符串,转换结果取决于地域设置,详见Delphi帮助文件的Currency

and date/time formatting variables主题

领会以上内容最好的办法是你亲自进行字符串格式化试验。为了简便起见,我写了FmtTest 程序,它能将整数

和浮点数转换为格式化字符串。从图7.2可见,程序窗体分为左右两部分,左边对应整型数字转换,右边对应浮点数

转换。

各部分的第一个编辑框显示需要格式化为字符串的数值。第一个编辑框下方有一个按钮,用来执行格式化操作

并在消息框中显示结果;紧接着第二个编辑框用于输入格式化类型串。你也可以单击ListBox 控件中的任一行,选择

预定义的格式化类型串,也可以自行输入,每输入一个新的格式化类型串,该类型串就会被添加到列表框中(注意,

关闭程序就失去了添加的类型)。

图 7.2: 程序 FmtTest 的浮点值输出

本例只简单使用了不同的控制文本来产生输出,下面列出了其中一个Show 按钮事件代码:

procedure Click(Sender: TObject);

begin

ShowMessage (Format (,

[StrToInt ()]));

// if the item is not there, add it

if f () < 0 then

();

end;

这段代码主要用EditFmtInt 编辑框的文本和EditInt 控件的值进行了格式化操作。如果格式化类型串没有在列

表框中列出,那么输入的串会被添加到列表框中;如果用户在列表框中进行点击,代码会把点击的串移到编辑框中:

procedure xIntClick(Sender: TObject);

begin

:= [

dex];

end;

结束语

字符串是一种很常用的数据类型,尽管在很多情况下不理解字符串怎样工作也能安全使用它们,不过通过本章,

了解了字符串的内部运行机制之后,你就能更充分地利用字符串类型的强大功能。

Delphi用特殊的动态方式处理字符串内存,正如动态数组一样,这将在下一章进行讨论。

第八章

内存

作者的话:本章内容涉及内存处理,讨论各种内存区,并介绍动态数组。目前暂时只有动态数组部分。

Delphi 4 的动态数组

传统的Pascal 语言其数组大小是预先确定的,当你用数组结构声明数据类型时,你必须指定数组元素的个数。

专业程序员也许知道些许动态数组的实现技术,一般是采用指针,用手工分配并释放所需的内存。

Delphi 4中增加了非常简单的动态数组实现方法,实现过程效仿我前面讲过的动态长字符串。与长字符串一样,

动态数组的内存动态分配并且引用记数,不过动态数组不支持 copy-on-write 技术。这不是个大问题,因为你可以

把变量值设置为nil释放数组内存。

这样你就可以声明一个不指定元素个数的数组,并用SetLength 过程给数组分配一个特定大小的内存,

SetLength 过程还可以改变数组大小而不影响其内容,除此外还有一些字符串过程也可用于数组,如Copy 函数。

以下摘录的代码突出了一点,这就是:定义数组后必须先为它分配内存,然后才能开始使用:

procedure 1Click(Sender: TObject);

var

Array1: array of Integer;

begin

Array1 [1] := 100;

// error

SetLength (Array1, 100);

Array1 [99] := 100;

// OK

...

end;

如果你只定义一个数组元素个数,那么索引总是从0开始。Pascal 中的普通数组既能用不为零的下标,也能用

非整数的下标,但动态数组均不支持这两种下标。象普通数组一样,你可以通过Length、High和Low 函数了解到

动态数组的状况,不过对于动态数组,Low 函数返回值总是0,High函数返回数组大小减1,这意味着空的动态数

组其函数High返回值是-1,这是一个很怪的值,因为它比Low的返回值还小。

图 8.1: 例 DynArr 窗体

以上作了简短的介绍,现在举个简例,例名DynArr ,见图8.1。例子实在是很简单,其实动态数组没有什么特

别复杂地方。我想通过该例说明几个程序员可能犯的错误。程序中声明了两个全程数组并在OnCreate 事件中初始化

了第一个数组:

var

Array1, Array2: array of Integer;

procedure eate(Sender: TObject);

begin

// allocate

SetLength (Array1, 100);

end;

这样就把数组所有值设置为0。完成这段代码你马上就能读写数组元素的值,而不用害怕内存出错,当然条件

是你没有试图访问超过数组上界的元素。为了更好地初始化,程序中添加了一个按钮,执行数组元素赋值操作:

procedure lClick(Sender: TObject);

var

I: Integer;

begin

for I := Low (Array1) to High (Array1) do

Array1 [I] := I;

end;

Grow 按钮用于修改数组大小,但并不影响数组内容。单击Grow 按钮后,你可以用Get value按钮进行检验:

procedure wClick(Sender: TObject);

begin

// grow keeping existing values

SetLength (Array1, 200);

end;

procedure Click(Sender: TObject);

begin

// extract

Caption := IntToStr (Array1 [99]);

end;

Alias 按钮的OnClick 事件代码稍复杂些,程序通过 := 算子把一个数组拷贝给另一个数组,从而有效地创建了

一个别名(一个新变量,但引用内存中同一数组)。从中可见,如果你改变了其中一个数组,那么另一个同样也会

改变,因为它们指向同一个内存区:

procedure asClick(Sender: TObject);

begin

// alias

Array2 := Array1;

// change one (both change)

Array2 [99] := 1000;

// show the other

Caption := IntToStr (Array1 [99]);

在btnAliasClick 事件中增加了两部分操作内容。第一部分是数组等同测试,不过并不是测试实际的数组元素,

而是测试数组所引用的内存区,检测变量是不是内存中同一数组的两个别名:

procedure asClick(Sender: TObject);

begin

...

if Array1 = Array2 then

Beep;

// truncate first array

Array1 := Copy (Array2, 0, 10);

end;

btnAliasClick 事件的第二部分内容是调用Copy 函数。该函数不仅把数据从一个数组移到另一个数组,而且用

函数创建的新数组取代第一个数组,结果变量Array1 所引用的是11个元素的数组,因此,按Get value 和Set value

按钮将产生一个内存错误,并且触发一个异常(除非你把范围检查range-checking 选项关掉,这种情况下,错误仍

在但屏幕上不会显示异常)。虽然如此,Fill 按钮仍能正常工作,因为需要修改的数组元素由数组当前的下标范围确

定。

结束语

这一章内容暂时只包括动态数组,动态数组的确是内存管理的重要组成部分,但仅仅是其中的一部分,其它内

容以后会逐步添加。

本章描述的内存结构属于典型的 Windows 编程内容,这方面内容将在下一章进行讨论。

第九章

Windows 编程

Delphi 利用Object Pascal 和可视控件库(VCL)对底层的Windows API 进行了完美的封装,所以很少需要使

用基础Pascal 语言来建立Windows应用程序,也无需直接调用Windows API 函数。尽管如此,如果遇到特殊情况,

VCL 又不支持,Delphi程序员还得直接面对Windows编程。不过只有在极其特殊的情况下,例如:基于不寻常API

调用的Delphi新控件开发, 你才需要这样做,这里我不想讨论这方面内容,我只想让大家看一下与操作系统交互的

几个Delphi元素以及Delphi程序员能从中获益的Windows编程技术。

Windows 句柄

Delphi从Windows 引入了不少数据类型,其中句柄最重要。这种数据类型名为THandle,该类型在Windows 单

元中定义:

type

THandle = LongWord;

句柄数据类型通过数字实现,但并不当数字用。在Windows 中,句柄是一个系统内部数据结构的引用。例如,

当你操作一个窗口,或说是一个Delphi 窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号

窗口,就此,你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口极小化为图

标,等等。实际上许多Windows API 函数把句柄作为它的第一个参数,如GDI (图形设备接口)句柄、菜单句柄、

实例句柄、位图句柄等等,不仅仅局限于窗口函数,。

换句话说,句柄是一种内部代码,通过它能引用受系统控制的特殊元素,如窗口、位图、图标、内存块、光标、

字体、菜单等等。Delphi中很少需要直接使用句柄,因为句柄藏在窗体、位图及其他Delphi对象的内部。当你要调

用Delphi不支持的Windows API 函数时,句柄才会有用。

现在举一个简单的Windows句柄例子,完善这节内容。例WHandle 程序的窗体很简单,只有一个按钮。正如

下面主窗体文本所定义的那样,我在代码中添加了窗体的OnCreate 事件和按钮的OnClick 事件:

object FormWHandle: TFormWHandle

Caption =

'Window Handle'

OnCreate = FormCreate

object BtnCallAPI: TButton

Caption =

'Call API'

OnClick = BtnCallAPIClick

end

end

窗体一创建,程序就会通过窗体本身的Handle 属性,获取窗体对应的窗口句柄。调用IntToStr ,把句柄数值

转换为一个字符串,然后再把它添加到窗体标题中,如图9.1:

procedure eate(Sender: TObject);

begin

Caption := Caption +

' '

+ IntToStr (Handle);

end;

因为FormCreate 是窗体类的方法,它可直接访问同类的其他属性和方法。因此,在这个过程中我们能够直接

访问窗体的Caption 属性和Handle 属性。

图 9.1: 例 WHandle 显示窗体句柄,每次运行程序得到的句柄值不同

如果你多此次执行该程序,通常会获得不同的句柄值。这个值实际上是由Windows 操作系统确定并返回给应用

程序的。(句柄从来不是由程序决定的,而且句柄没有预定义值,句柄是由系统决定的,每执行一次程序,产生一

个新值。)

当你单击按钮,程序将调用Windows API 函数SetWindowText,它会根据第一个传递参数改变窗口的标题。更

准确地说,所用的API 函数其第一个参数是需要修改窗体的句柄:

procedure lAPIClick(Sender: TObject);

begin

SetWindowText (Handle,

'Hi'

);

end;

这段代码与前面所讲的事件处理程序等效,它通过给窗体的Caption 属性赋一个新值,改变窗体的标题。对上

面这种情况,调用一个API 函数没有什么意义,因为用Delphi来做更简单。然而有些API在Delphi中没有相应的函

数,就需要直接调用API,这一点你会在后面的高级例子中看到。

外部声明

Windows 编程中涉及的另一个重要元素是外部声明。外部声明原先用于在Pascal代码中连接汇编语言写的外部

函数,现在外部声明用于Windows编程,用来调用动态连接库DLL函数。在Delphi的Windows 单元中有许多这种

声明:

// forward declaration

function LineTo (DC: HDC; X, Y: Integer): BOOL; stdcall;

// external declaration (instead of actual code)

function LineTo; external

''

name

'LineTo'

;

这段声明表示函数LineTo 的代码同名保存在 动态链接库中(最重要的Windows 系统库之一)。

实际应用时,外部声明中的函数名与DLL中的函数名可以不同。

一般你不需要象刚才所例举的那样写声明,因为Windows 单元和一些Delphi 系统单元中已包含了这些声明。

只有在调用自定义DLL,或调用Delphi 中未定义的Windows 函数时,你才能需要写外部声明。

注意:在16位Delphi中,外部声明使用不带扩展名的库名,后面跟name指令(如上所示)或是一个index指令,

后面跟DLL中函数的序号。尽管Win32 仍然允许通过序号访问DLL函数,但是微软公司已经声明未来将不支持这种

访问方式,这一改变反映了系统库访问方式的改变。还要注意的是:目前Delphi的Windows 单元已取代了16位Delphi

的WinProcs 和WinTypes 单元。

回调函数

从第六章已经了解到Objet Pascal 支持过程类型。过程类型常用于给Windows API函数传递回调函数。

首先,什么是回调函数呢?回调函数就是能对一系列系统内部元素执行给定操作的API函数,例如能对所有同

类窗口进行操作的函数。这种函数也叫枚举函数,它是作为参数传递的函数,代表对所有内部元素执行的操作,该

函数或过程的类型必须与给定的过程类型兼容。Windows 回调函数的应用不止上述一种,不过这里仅研究以上简单

应用。

现在考虑 EnumWindows API 函数,它的原型如下(从Win32 帮助文件拷贝而来):

BOOL EnumWindows(

WNDENUMPROC lpEnumFunc,

// address of callback function

LPARAM lParam

// application-defined value

);

当然,这是个C语言的定义。我们可以查看Windows 单元,从中找到相应的Pascal 语言定义:

function EnumWindows (

lpEnumFunc: TFNWndEnumProc;

lParam: LPARAM): BOOL; stdcall;

查阅帮助文件,我们发现作为参数传递的函数应该属于下面的类型(也是在C中):

BOOL CALLBACK EnumWindowsProc (

HWND hwnd,

// handle of parent window

LPARAM lParam

// application-defined value

);

这与下面的Delphi 过程类型定义一致:

type

EnumWindowsProc = function (Hwnd: THandle;

Param: Pointer): Boolean; stdcall;

其中第一个参数是各主窗体的句柄,第二个参数则是调用EnumWindows 函数时所传递的值。实际上,Pascal 中

没有相应的TFNWndEnumProc类型定义 ,它只是个指针。这意味着我们需要传递一个带有合适参数的函数,将它

用作一个指针,也就是取函数的地址而不是调用它。不幸的是,这也意味着如果某个参数类型出现错误时,编译器

不会给予提示。

每当调用Windows API函数或传递一个回调函数给系统时,Windows 要求程序员遵循stdcall 调用协定。缺省情况

下,Delphi使用另一种更高效的调用协定,其关键字为register。

下面是一个与定义兼容的回调函数,此函数把窗口的标题读到字符串中,然后添加到给定窗体的一个列表框中:

function GetTitle (Hwnd: THandle; Param: Pointer): Boolean; stdcall;

var

Text: string;

begin

SetLength (Text, 100);

GetWindowText (Hwnd, PChar (Text), 100);

(

IntToStr (Hwnd) +

': '

+ Text);

Result := True;

end;

窗体有一个几乎覆盖整个窗体的列表框,窗体顶部有一个小面板,面板上有一个按钮。当按下按钮时,

EnumWindows API函数被调用,并且GetTitle 函数作为参数传递给它:

procedure lesClick(Sender: TObject);

var

EWProc: EnumWindowsProc;

begin

;

EWProc := GetTitle;

EnumWindows (@EWProc, 0);

end;

你可以直接调用GetTitle函数,不必先把值保存到过程类型临时变量中,上例这么做是为了使回调过程更清楚。

程序运行结果确实很有意思,正如你在图9.2中看到的那样,结果显示了系统中正在运行的所有主窗口,其中大部分

是隐藏的,你通常看不到,许多实际上没有标题。

图 9.2: 例CallBack输出结果--当前所有主窗体,其中包括可见及隐藏的窗体

最小的Windows 程序

为了完整介绍Windows 编程及Pascal 语言,现在我展示一个简单但完整的应用程序,建立该程序没有使用VCL

库。这个程序只是简单地采用命令行参数(保存在系统全程变量cmdLine中),并利用ParamCount 和 ParamStr 这

两个Pascal 函数从参数中提取信息。其中第一个函数返回参数的个数,第二个返回给定位置的参数。

尽管在图形用户界面环境下用户很少操纵命令行参数,但是Windows 命令行参数对系统本身却很重要。例如,

一旦你定义了文件扩展名和应用程序的关联,只要双击所关联的文件就能执行这个程序。实际上,当你双击一个文

件,Windows 即启动关联程序并把选定的文件作为命令行参数传递给它。

下面是工程文件的完整源代码(一个DPR 文件,不是PAS 文件):

program Strparam;

uses

Windows;

begin

// show the full string

MessageBox (0, cmdLine,

'StrParam Command Line'

, MB_OK);

// show the first parameter

if ParamCount > 0 then

MessageBox (0, PChar (ParamStr (1)),

'1st StrParam Parameter'

, MB_OK)

else

MessageBox (0, PChar (

'No parameters'

),

'1st StrParam Parameter'

, MB_OK);

end.

输出代码使用MessageBox API 函数,很容易就避开了VCL库。实际上,象上面那样纯粹的Windows 程序,

其优点就是占的内存少,程序执行文件大约才16k字节。

为了给程序提供命令行参数,你可以用Delphi的 Run > Parameters 菜单命令。另一个方法是:打开Windows

资源管理器,查找包含程序执行文件的目录,然后把你要执行的文件拖到可执行文件上,Windows 资源管理器会把

拖放的文件名用作命令行参数,开始执行程序。图9.3显示了资源管理器及相应的输出。

图: 把一个文件拖放到执行文件上,给例StrParam提供命令行参数

结束语

在这一章中,我们对Windows 编程的底层内容进行了介绍,讨论了句柄和简单的Windows 程序。对于常规的

Windows 编程任务,通常只需使用Delphi 提供的可视开发工具及VCL可视控件库。但是这超出了本书讨论的范围,

因为本书讨论的是Pascal 语言。

下一章介绍variant类型。对Pascal 数据类型系统来说,它是一个非常特殊的外来物,引入它是为了提供完全

的OLE 支持。

第十章

Variant类型

为了完全支持OLE,32位Delphi 增加了Variant 数据类型,本节将从宏观角度来分析这种数据类型。实际上,

Variant类型对Pascal语言有普遍而深入的影响,Delphi 控件库中与OLE 无关的地方也使用到这种类型。

Variant变量没有类型

一般说来,你可以用Variant 变量存储任何数据类型,对它执行各种操作和类型转换。需要注意的是:这违反

了Pascal 语言的一贯原则,有悖于良好的编程习惯。variant 变量的类型检查和计算在运行期间才进行,编译器不

会提示代码中的潜在错误,这些错误在进一步测试中才能发现。总之,你可以认为包含variant变量的代码是解释性

代码,正如解释性代码一样,许多操作直到执行时才能知道,这对代码运行速度会有很大的影响。

上面对Variant 类型的使用提出了警告,现在来看看Variant 类型究竟能干什么。基本上说,如果声明了一个

variant 变量:

var

V: Variant;

你就可以把各种不同类型的值赋给它:

V := 10;

V :=

'Hello, World'

;

V := 45.55;

一旦得到一个variant 值,你可以把它拷贝给任何兼容或不兼容的数据类型。如果你把值赋给不兼容的数据类

型,Delphi 会力尽所能进行转换,无法转换则颁布一个运行时间错误。实际上,variant变量中不仅包含了数据还包

含有类型信息,并允许一系列运行时间操作,这些操作很方便,但运行速度慢且安全性差。

见例VariTest,它是上面代码的扩展。窗体上有三个编辑框,一对按钮,第一个按钮的OnClick 事件代码如下:

procedure 1Click(Sender: TObject);

var

V: Variant;

begin

V := 10;

:= V;

V :=

'Hello, World'

;

:= V;

V := 45.55;

:= V;

end;

很有趣是不是?你可以把一个值为字符串的variant 变量赋给编辑框Text 属性,还可以把值为整数或浮点数的

variant 变量赋给Text属性。正如你在图10.1中所看到的,一切正常。

(图10.1)按Assign按钮后,例VariTest的输出结果

图 10.1: 例 VariTest 的 Assign 按钮 Click 事件输出结果

更糟糕的是:你还可以用variant变量计算数值,从第二个按钮的Click事件代码就可看到这一点:

procedure 2Click(Sender: TObject);

var

V: Variant;

N: Integer;

begin

V := ;

N := Integer(V) * 2;

V := N;

:= V;

end;

至少这种代码带有一定危险性,如果第一个编辑框包含了一个数字,那么一切运行正常;如果不是,将会引发

异常。这里再重申一遍,如果不到万不得以,不要随便使用Variant 类型,还是应坚持使用传统的Pascal 数据类型

和类型检查方法。在Delphi 和 VCL中,variant变量主要是用于 OLE 支持和数据库域的访问。

Variant类型内部结构

Delphi中定义了一个 variant 记录类型,

TVarData

,它与Variant 类型有相同的内存布局。你可以通过

TVarData

访问variant变量的实际类型。

TVarData

结构中包含了Variant类型信息(由Vtype域表示)、一些保留域及当前值。

VType域的取值包括OLE 自动化中的所有数据类型,这些类型通常叫OLE 类型或variant 类型。以下是variant

类型的完整列表,按字母顺序排列:

varArray

varBoolean

varByRef

varCurrency

varDate

varDispatch

varDouble

varEmpty

varError

varInteger

varNull

varOleStr

varSingle

varSmallint

varString

varTypeMask

varUnknown

varVariant

你可以在Delphi 帮助系统的variants 主题下找到这些类型的说明。

还有许多操作variant 变量的函数,你可以用它们进行特定的类型转换,或通过它们获取variant变量的类型信

息(例如VarType 函数),当你用variant变量写表达式时,Delphi会自动调用这些类型转换和赋值函数。另外还

有操作variant 数组的例程,你可以通过帮助文件的Variant support routines 主题了解相关内容。

Variant类型运行很慢!

Variant 类型代码运行很慢,不仅数据类型转换如此,两个值为整数的Variant 变量相加也是如此。它们几乎跟

Visual Basic这种解释性代码一样慢!为了比较Variant变量和整型变量的运行速度,请看例VSpeed 。

程序中设置了一个循环,记录运行时间并在进程条中显示运行状态。下面是基于variant类型的一段代码,基于

整型的代码与此相似:

procedure 1Click(Sender: TObject);

var

time1, time2: TDateTime;

n1, n2: Variant;

begin

time1 := Now;

n1 := 0;

n2 := 0;

on := 0;

while n1 < 5000000 do

begin

n2 := n2 + n1;

Inc (n1);

if (n1 mod 50000) = 0 then

begin

on := n1 div 50000;

sMessages;

end;

end;

// we must use the result

Total := n2;

time2 := Now;

n := FormatDateTime (

'n:ss'

, Time2-Time1) +

' seconds'

;

end;

记时这段代码值得一看,因为你可以把它用到任何类型的性能测试中。正如你所看到的,程序用Now 函数获取

当前的时间,用FormatDateTime 函数格式化时间差,输出结果以分("n")和秒("ss")表示。除此之外,你可以用

Windows API的GetTickCount 函数,该函数能精确显示操作系统启动后至当前的毫秒数。

从上例可见两者的速度差异非常之大,以至于不用精确记时也能看到这种差异。图10.2是在本人计算机上运行

程序看到的结果。当然运行结果取决于运行程序的计算机,但是两者的数值比不会有太大变化。

图 10.2: 例Vspeed中整型与Variant类型的计算速度差异

结束语

Variant类型与传统Pascal 数据类型差别很大,所以本章以短小篇幅单独阐述了Variant类型的有关内容。尽管

Variant类型主要用于OLE 编程,但用来写一些潦潦草草的程序倒也便利,因为不用考虑数据类型,不过正如以上所

述,这样做会影响程序执行速度。

通过前面各章我们已经介绍了绝大部分的语言特征,下一章将讨论程序的总体框架和单元模块。

第十一章

程序和单元

Delphi 应用程序中的单元,或说程序模块可谓老道精深。实际上,单元是程序模块化的基础,类是继它之后才

有的。在Delphi 应用程序中,每个窗体都有一个相对应的单元。用相应的工具按钮, 或File > New Form 菜单命令,

在工程中添加一个新窗体,实际上是增加了一个新单元,也就是建立了该新窗体的类。

单元

虽然所有窗体都在单元中定义,但反之则不然。除窗体外,单元中还可以定义一系列能访问的例程。选择File >

New菜单命令,然后在Object Repository的New 页中选择Unit 图标,随即当前工程中就会加入一个空白单元。单

元代码分区存放,空白单元的代码如下:

unit Unit1;

interface

implementation

end.

单元的概念比较简单,单元名与文件名相同,而且必须唯一。单元包括界面区(interface)及实现区

(implementation),界面区用于声明其它单元能看到的部分;实现区存放界面的实现代码及外部不可见的声明。此外

还有两个可选的区,即初始化区及结束区,其中初始化区存放初始化代码,当程序加载到内存时执行;结束区存放

程序终止时执行的代码。

单元总体结构如下:

unit unitName;

interface

// other units we need to refer to

uses

A, B, C;

// exported type definition

type

newType = TypeDefinition;

// exported constants

const

Zero = 0;

// global variables

var

Total: Integer;

// list of exported functions and procedures

procedure MyProc;

implementation

uses

D, E;

// hidden global variable

var

PartialTotal: Integer;

// all the exported functions must be coded

procedure MyProc;

begin

// ... code of procedure MyProc

end;

initialization

// optional initialization part

finalization

// optional clean-up code

end.

界面区头部的uses子句表示需要访问的外部单元,这些外部单元中定义了你需要引用的数据类型,如自定义窗

体内所用的控件。

实现区头部的uses子句用于表示只在实现部分访问的单元。如果例程或方法的代码需要引用其他单元,你应该

把这些单元加到实现区子句中,而不是界面区。你所引用的单元必须在工程文件目录中能找到,或在工程选项对话

框的 Directories/Conditionals 页设定这些单元的搜索路径。

C++程序员应该知道uses语句和include 指令是不同的。uses语句只是用于输入引用单元的预编译界面部分,引用

单元的实现部分在单元编译时才考虑。你引用的单元可以是源代码格式(PAS),也可以是编译格式(DCU),但是

必须用同一版本的Delphi进行编译。

在单元的界面区中可以声明许多不同的元素,包括过程、函数、全程变量及数据类型。在Delphi 应用程序中,

数据类型可能用得最频繁。每创建一个窗体,Delphi 会在单元中自动建立一个新的数据类型--类(class)。在Delphi 单

元中不仅能定义窗体;还能象传统单元一样,只包含过程及函数;还可以定义与窗体和任何可视控件无关的类。

单元的工作空间

Pascal单元是封装性和可视性的关键,它很可能比类中的 private 和 public 关键字还要重要。(实际上,private

关键字与类单元的工作空间有关)。一个标识符(如一个变量、过程、函数或数据类型)的工作空间是指能访问标识

符的代码段。基本原则是:标识符在它工作空间内才有意义,也就是说,只在其声明的代码块中才有意义,在工作空

间外你不能访问这些标识符。例如:

• 局部变量:如果你在例程或方法代码块内声明一个变量,那么单元外部不能访问这个变量。该标识符的工作

空间就是定义标识符的整个例程,其中包括嵌套例程(除非嵌套例程内有一个同名标识符覆盖了外部定义)。

当调用到例程时,其变量压入栈内存中,例程一结束,栈中的内存就自动释放。

• 全程隐藏变量:如果你在单元的实现部分声明一个标识符,那么在单元外你不能使用它,但是能在单元内任

一代码块及过程中使用它。程序一启动就会为全程隐藏变量分配内存,程序终止内存释放,你可以在单元初

始化区给它赋初值。

• 全程变量:如果你在单元的界面部分声明标识符,那么该标识符的工作空间就扩大了,任何Use它的单元都

能访问它。这类变量的内存分配及生命周期与上类变量相同,唯一不同的是其可见性。

只要程序单元的uses子句中列出某一单元名,那么所列单元界面区中声明的任何标识符该程序都能访问。窗体

类的变量就是这样声明的,所以你可以在其他窗体代码中访问这个窗体以及它的公共域、方法、属性和组件。当然

把什么都声明为全局标识这种编程习惯并不好。除了明显的内存消耗问题外,使用全程变量使代码维护和更新变得

困难。一句话,你应该尽量少用全程变量。

单元用作命名空间

uses 语句是访问其他单元工作空间的标准技术,通过该语句你能访问其它单元的定义内容。如果恰巧两个单元

声明的标识符同名,也就是说你可能有两个同名的类或例程,遇到这种情况,你可以用单元名作前缀定义类型或过

程名,由此进行区分。例如用eTotal访问Totals 单元中的ComputeTotal 过程。不过这种情况最好

不要经常遇到,因此强烈建议不要在同一程序中用同一名字表示两个不同的东西。

然而,如果查阅VCL库和Windows 文件,你会发现一些Delphi 函数和Delphi 可用的Windows API 函数同名,

不过参数往往不同,下面以Beep 过程为例说明这个问题。

新建一个Delphi 程序,添加一个按钮,然后写入以下代码:

procedure 1Click(Sender: TObject);

begin

Beep;

end;

执行程序,单击按钮,你会听到一个短促的声音。现在移到单元的uses语句,把原先的代码:

uses

Windows, Messages, SysUtils, Classes, ...

改为下面的样式,只要把

SysUtils

单元移到

Windows

之前:

uses

SysUtils, Windows, Messages, Classes, ...

现在如果重新编译这段代码,你会得到一个编译错误:”Not enough actual parameters”(实际参数不够)。问

题在于Windows 单元定义了另一个带两个参数的Beep 函数。应该说uses子句中第一个单元的定义被后面单元的

定义覆盖,解决方法很简单:

procedure 1Click(Sender: TObject);

begin

;

end;

不管uses子句中单元顺序如何排列,以上代码都能编译通过。在Delphi中很少有其他命名冲突的情况,因为

Delphi 代码通常放在类的方法中,如果不同类中有两个同名的方法不会产生任何冲突,只是使用全程例程会产生冲

突问题。

单元和程序

Delphi 应用程序源代码文件可分成两类,它们是一个或多个单元文件及一个程序文件,单元文件可以看成是次

一级的文件,它被应用程序的主体——程序文件——引用。理论上如此,实际上程序文件通常是自动产生的,作用有

限,程序启动并运行主窗体时才会用到它。程序文件的代码,或说Delphi 工程文件(DPR),可用手工进行编辑,

也可通过工程管理器及其与应用程序、窗体相关的选项进行编辑。

程序文件的结构通常比单元文件的结构简单得多。下面是一个程序文件的源代码:

program Project1;

uses

Forms,

Unit1 in

?{Form1DateForm}

;

begin

lize;

Form (TForm1, Form1);

;

end.

从上可见,文件中只有一个uses区和应用程序的主体代码,主体代码包含在begin 和 end 关键字之间。程序

的uses子句特别重要,因为需要通过它来管理应用程序的编译和连接。

结束语

讨论完Delphi中Pascal 应用程序的结构,本书就翻过了最后一章,至少目前是这样,欢迎来email发表你的意

见和要求。

如果想进一步学习Delphi Object Pascal中面向对象的内容,你可以参考我已出版的书《精

通Delphi 5》(

Mastering Delphi 5

,Sybex,1999),详细情况可访问网址

,你也可以从该网址上下载本书的最新版本。

附录A

术语表

这里列出了本书中用到的一些技术术语,在别的地方你也能找到它们,不过我想还是把它们集中一处,以便查

找。

堆(内存)

堆表示程序可用的内存区,也叫动态内存区。堆内存的分配与释放次序是随机的,这就是说,如果你按次序分

配三块内存,那么到时并不按分配时的次序释放内存。 堆管理器会负责所有操作,你只需简单地使用GetMem 函

数请求新内存或调用constructor 建立对象, Delphi 会返回一个新的内存块(随意重用已经丢弃的内存块)。

堆是应用程序可用的三种内存区之一, 其它两种分别是全局内存区(存放全程变量) 和栈。与堆相反,全程变量

内存在程序启动时就分配,然后一直保留到程序终止才释放;栈的内容请详见术语表。

Delphi 使用堆为对象、字符串、动态数组及特殊的动态内存请求(GetMem)内存分配。

Windows 应用程序的地址空间最大允许有 2 GigaByte, 其中大部分能被堆使用。

栈(内存)

栈表示程序可用的内存区,栈内存动态分配,并按特定次序分配、释放。栈内存按后进先出次序(LIFO)分配,

这表示最后分配的内存区先被释放。栈内存一般在例程中使用(过程、函数及方法调用)。 当你调用例程时,例程参

数及返回值是放在栈中的(除非使用Delphi缺省调用方式,对调用过程进行优化)。此外,例程中声明的变量(在begin

所以当例程终止时,这些变量会被自动清除(在返回调用点之前以LIFO次序释放)。

语句前的

var

块中)也存放在栈中,

栈是应用程序可用的三种内存区之一,其它两种分别是全局内存区和堆。堆的内容请详见术语表。

Delphi 使用栈存放例程参数及其返回值(除非你使用Delphi缺省的 register 调用协定)、局部例程变量、

Windows API 函数调用等等。

Windows 应用程序可以预留大量的栈内存,在 Delphi 中你可以通过工程选项的 linker 页设置, 不过一般采用

缺省设置就可以了。 如果你收到一个栈溢出错误信息,这可能是因为你的函数进入了死循环自调用,而不是栈空间

太小。

其它

Dynamic

Static

Virtual

memory leak

painting

literal

array

API

class reference

class method

parent

owner

self

附录B

例名表

以下列出了《Pascal精要》中用到的所有例子,需要可以从网上免费下载:

第三章

ResStr: 资源串

Range: 有序类型取值范围

TimeNow: 时间类型数据操作

第四章

GPF: 空指针的一般保护错

第五章

IfTest: if 语句

Loops: for 和 while 语句

第六章

OpenArr: 开放数组参数

DoubleH: 几个简单的过程

ProcType: 过程类型

OverDef: 重载及缺省参数

第七章

StrRef: 字符串引用计数

LongStr: 使用长字符串

FmtTest: 格式化举例

第八章

DynArr: 动态数组

WHandle: Windows 句柄

Callback: Windows 回调函数

StrParam: 命令行参数

第十章

VariTest: 简单的variant变量操作

VariSpeed: variant变量运算速度


本文标签: 类型 代码 字符串 函数 参数