发信人: bbbbbbbbbbbb (刀剑笑), 信区: BorlandDev
标 题: Pascal精要--paste by COOLREN
发信站: 哈工大紫丁香 (2002年12月29日09:51:43 星期天), 站内信件
发信人: coolren (茄子), 信区: BorlandDev
标 题: 一: Pascal历史回顾
发信站: 哈工大紫丁香 (2002年12月29日09:35:56 星期天), 站内信件
Delphi中使用的面向对象pascal编程语言并不是borland公司1995年发布可视化开发环境D
elphi时才有的,它只是已有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 代码
--
老茄子躲在一边偷着乐 :-)
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 二: 编写Pascal代码
发信站: 哈工大紫丁香 (2002年12月29日09:37:36 星期天), 站内信件
进入正题前先谈一下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、 My
Name、 myname、 myName、 和MYNAME是完全相同的。总体上来说,这是Pascal的一大优点
,因为在大小写敏感的语言中,许多语法错误是由不正确的大写引起的。
注意:Pascal语言的大小写不敏感特性有一个例外:控件包中的Register 过程必须以大写
字母R开始,因为需要与C++Builder 兼容。
然而大小写不敏感也有不便之处:第一,你必须注意大小写不一致的标识符实际上是相同
的,以避免把他们当成不同的元素使用;第二,你必须尽量保持大写使用的一致性,以提
高代码的可读性。
大写使用的一致性不是编译器强制要求的,但是保持大写使用的一致性是值得提倡的好习
惯。一个常用的方法是将每个标识符的第一个字母大写,标识符若由几个词组合而成(中间
不能插入空格),每个词的第一个字母应大写:
MyLongIdentifier
MyVeryLongAndAlmostStupidIdentifier
此外,编译器不编译代码中的空格、空行和Tab键空格,这些元素通称为空白,它们只用来
提高代码的可读性,不影响编译过程。
不同于BASIC, Pascal 语句允许分行书写,即将一条长指令分割成两个或更多的代码行。
允许语句分行的缺点(至少对许多BASIC程序员)是:语句结束时不能忘了加分号,更确切
地说,必须记着把语句和紧接它的语句分开。语句分行唯一的限制是字符串不能跨行。
关于空格和语句分行的使用没有既定的规则,以下是几点经验:
Delphi 代码编辑器中有一条竖线叫右边线(Right Margin),你可以把右边线设置在60或7
0个字符处。如果以这条线为基准,代码不超过这条界限,那么打印到纸上的代码看起来会
很好看。否则,打印时长语句会被随意分行,甚至在一个词的中间断开。
当一个函数或过程有多个参数,通常的做法是把各参数放在不同的行上。
你可以在注释行前留一行空白,或把长的代码句分成较小的部分,这样能提高代码的可读性
。
用空格隔开函数调用的参数,表达式中的运算符也最好用空格隔开。一些程序员可能会对
这些提议不以为然,但我坚持认为:空格是免费的,你不必为使用空格付费,何乐而不为
呢?
优化版面
关于代码编写风格的最后一条建议是:尽量使用空白优化版面。这一条很容易做到,只需
要在写复合句时,以上一句为参照,下一句向右缩进两个空格,复合句内嵌的复合句缩进
四个空格,依此类推。例如:
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 运算符 (RTTI)
asm 语句
assembler 向后兼容 (汇编)
at 语句 (异常处理)
automated 访问类别符 (类)
begin 块标记
case 语句
cdecl 函数调用协定
class 类型
const 声明或指令(参数)
constructor 特殊方法
contains 运算符 (集合)
default 指令 (属性)
destructor 特殊方法
dispid dispinterface 界面类别符
dispinterface 类型
div 运算符
do 语句
downto 语句 (for)
dynamic 指令 (方法)
else 语句 (if 或 case)
end 块标记
except 语句 (异常处理)
export 向后兼容 (类)
exports 声明
external 指令 (函数)
far 向后兼容 (类)
file 类型
finalization 单元结构
finally 语句 (异常处理)
for 语句
forward 函数指令
function 声明
goto 语句
if 语句
implementation 单元结构
implements 指令 (属性)
in 运算符 (集合) - 工程结构
index 指令 (dipinterface界面)
inherited 语句
initialization 单元结构
inline 向后兼容 (见 asm)
interface 类型
is 运算符 (RTTI)
label 声明
library 程序结构
message 指令 (方法)
mod 运算符 (数学)
name 指令 (函数)
near 向后兼容 (类)
nil 数值
nodefault 指令 (属性)
not 运算符 (布尔)
object 向后兼容 (类)
of 语句 (case)
on 语句 (异常处理)
or 运算符 (布尔)
out 指令 (参数)
overload 函数指令
override 函数指令
package 程序结构 (控件包)
packed 指令 (记录)
pascal 函数调用协定
private 访问类别符 (class)
procedure 声明
program 程序结构
property 声明
protected 访问类别符 (类)
public 访问类别符 (类)
published 访问类别符 (类)
raise 语句 (异常处理)
read 属性类别符
readonly dispatch 界面类别符
record 类型
register 函数调用协定
reintroduce 函数指令
repeat 语句
requires 程序结构 (控件包)
resident 指令 (函数)
resourcestring 类型
safecall 函数调用协定
set 类型
shl 运算符 (数学)
shr 运算符 (数学)
stdcall 函数调用协定
stored 指令 (属性)
string 类型
then 语句 (if)
threadvar 声明
to 语句 (for)
try 语句 (异常处理)
type 声明
unit 单元结构
until 语句
uses 单元结构
var 声明
virtual 指令 (方法)
while 语句
with 语句
write 属性类别符
writeonly dispatch 界面类别符
xor 运算符 (布尔)
表达式和运算符
建立表达式没有通用的方法,因为要取决于所用的运算符,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 程序的基本布局,下面开始探究它的细节。先从预定义
和自定义数据类型开始,然后是利用关键词组织编程语句。
下一页: 类型、变量及常量
--
# ■■■■■■■■■■〓▄▃▂▁愿你圣诞快樂 ︸︸||︸
. . . . o o o o o
_____ o _______
____==== ]OO|_n_n__][. |Go to|
[________]_|__|________)< | 滢滢| 天 外 来 客
oo oo 'oo OOOO-| oo\\_ ~~~|~~~
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 三: 类型、变量及常量
发信站: 哈工大紫丁香 (2002年12月29日09:38:33 星期天), 站内信件
三、
类型、变量及常量
最初的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 'Boo
lean'.(类型不兼容:‘整型’和‘布尔型’)。象这样的错误通常是编程错误,因为把
一个 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会选用Smal
lInt数据类型 (短整型--能容纳Thousand变量的最小整数类型)。如果你想告诉Delphi 采
用特定的类型,你可在声明中加入类型名,方法如下:
const
Thousand: Integer = 1000;
对于声名的常量,编译器有两种编译选择:第一种为常量分配内存,并把常量的值放入内
存;第二种在常量每次使用时复制常量值。第二种方法比较适合简单常量。
注意:16位的Delphi 允许你在程序运行期间改变已定义的常量值,就象一个变量一样。3
2位的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 TForm1.Button1Click(Sender: TObject);
begin
ShowMessage (BookName + #13 + AuthorName);
end;
以上代码中的两个字符串将分两行输出显示,因为字符串被分行符 #13 隔开。
有趣的是,当你用资源编辑器打开执行文件时,你会在程序资源中看到你所定义的字符串
。这意味着字符串并没有进入编译代码,而是保存在执行文件 (EXE文件) 的一个单独区域
。
注意:简而言之,采用资源的好处一方面可让Windows 来完成有效的内存处理,另一方面
不用更改源代码就可实现程序的本地化 (把字符串翻译成不同的语言)。
数据类型
Pascal 中有多种预定义的数据类型,它们可分为三大类:有序数据类型,实数类型和字符
串类型。下面我们先讨论有序类型和实数类型,字符串类型放在以后讨论。同时这一节还
将介绍几种Delphi 库中定义的类型 (不是编译器预定义的类型),这些类型也可看作是预
定义的类型。
Delphi 还包括一种无类型的可变数据类型,称作variant,在本书的第十章将讨论这一类
型。variant是一种无需类型检测的数据类型,它在Delphi 2 中引入,用于处理OLE Auto
mation(OLE 自动化)。
有序类型
有序类型是建立在概念“顺序”或“序列”基础上的数据类型。你不仅可比较两个有序值
的大小,而且可以求取给定有序值的前驱及后继,或者计算它们的最大或最小值。
三种最重要的预定义有序类型是整数类型、布尔类型和字符类型(Integer,Boolean,Char
)。各种类型根据其内部表示和取值范围不同又可进一步细分。表3.1列出了表示数字的有
序数据类型。
表 3.1: 表示数字的有序数据类型
大小 有符号值域 无符号值域
8 bits ShortInt
-128 to 127 Byte
0 to 255
16 bits SmallInt
-32768 to 32767 Word
0 to 65,535
32 bits LongInt
-2,147,483,648 to 2,147,483,647 LongWord (从 Delphi 4)
0 to 4,294,967,295
64 bits Int64
16/32 bits Integer Cardinal
从表中可看到,不同数据类型与不同的数据表示法相对应,这要取决于数据值的数位和符
号位。有符号类型的数值可正可负,但取值范围较小,因为符号位占一个数位。下一节在
例Range中说明了每种类型的实际取值范围。
表中最后一组类型标志着16/32,它表明其数值表示方法在16位和32位Delphi中不同,该组
的Integer及Cardinal 类型比较常用,因为它们与CPU内部的数字表示法相对应。
Delphi 4中的整数类型
在 Delphi 3中,Cardinal类型所表示的32位无符号值实际占31位,取值最高为20亿。Del
phi 4新增了一种无符号数字类型--LongWord,它是真正的32位值,取值最高达40亿。现在
Cardinal 类型已成了LongWord类型的别名,只是LongWord能容纳大于20亿的无符号数,而
且它的数值表示法与CPU内部数值表示法一致。
Delphi 4 中新增的另一个数据类型是Int64 类型,这一类型能表示长达18个数字的整数。
系统中的有序类型例程(如High 和Low)、数字例程(如Inc 和 Dec)及字符串转换例程
(如IntToStr)都支持这一新类型。反过来,有两个新增的专用函数StrToInt64 和 StrT
oInt64Def支持从字符串向数字的转换。
布尔类型
布尔值不同于布尔类型,平时很少用到。ByteBool、 WordBool 和LongBool这三种布尔类
型的布尔值比较特殊,只在Windows API 函数中才用到它们。
在Delphi 3 中,为了与Visual Basic 和 OLE Automation兼容,修改了ByteBool、 Word
Bool 和LongBool的布尔值,将TRUE值设置为1,FALSE值仍为0;Boolean类型布尔值保持不
变(TRUE为1,FALSE为0)。如果在Delphi 2代码中使用了布尔值显式类型转换 ,那么在
以后的Delphi中可能会出错。
字符类型
字符有两种不同的表示法:: ANSIChar 和 WideChar。第一种类型代表 8 位的字符,与W
indows一直沿用的ANSI(美国国家标准协会)字符集相应;第二种类型代表 16 位的字符
,与Windows NT、Windows 95 和 98支持的双字节字符(Unicode)相应。在Delphi 3 中
,Char 类型字符与ANSIChar一致。切记,不管在什么环境,前 256 个Unicode 字符与AN
SI 字符是完全一致的。
常量字符可用代表它们的符号表示,如‘k’,也可用数字符号表示,如 #78。后者还可用
Chr函数表示为 Chr(78),用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 TFormRange.BtnIntegerClick(Sender: TObject);
begin
LabelType.Caption := 'Integer';
LabelSize.Caption := IntToStr (SizeOf (Integer));
LabelMax.Caption := IntToStr (High (Integer));
LabelMin.Caption := 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 或 Sm
allInt。对于数学计算或非特殊的代码中,你最好的选择是坚持使用平台相应的标准整型
,这就是说,使用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个字节;其次是Doubl
e 浮点类型,占8个字节;Extended 浮点类型,占10个字节。这些不同精度的浮点数据类
型都与IEEE( 电气和电子工程师协会)标准的浮点数表示法一致,并且 CPU数字协处理器
直接支持这些类型,处理速度也最快。
Real 类型在Delphi 2 和 Delphi 3 中的定义与 16 位版本一样,都占 6 个字节。不过B
orland公司一直不提倡使用这种类型,而建议用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 位版的De
lphi不支持该类型) 表示一个有四位小数位的值,它的小数位长度是固定的,同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 过程)。有些例程,它的名字听起来很怪,如MomentSkewKurtos
is 例程,它是作什么用的呢? 还是留你自己查吧。
日期和时间
Delphi 也用实型数表示日期和时间数据。但为了更准确起见,Delphi 特别定义了TDateT
ime 数据类型,这是一个浮点类型,因为这个类型必须足够宽,使变量能容纳年、月、日
、时、分和秒、甚至毫秒。日期值按天计数,从1899-12-30开始,放在TDateTime 类型的
整数部分;时间值则位于十进制数的小数部分。
TDateTime 不是编译器可直接识别的预定义类型,它在System单元定义:
type
TDateTime = type Double;
使用TDateTime 类型很简单,因为Delphi 为该类型定义了一系列操作函数,表3.3列出了
这些函数。
表3.3: TDateTime类型系统例程
例程 作用
Now 返回当前日期及时间
Date 返回当前日期
Time 返回当前时间
DateTimeToStr 按缺省格式将日期和时间值转换为字符串;特定格式转换可用 FormatDat
eTime函数
DateTimeToString 按缺省格式将日期和时间值拷贝到字符串缓冲区
DateToStr 将TDateTime值的日期部分转为字符串
TimeToStr 将TDateTime值的时间部分转为字符串
FormatDateTime 按特定格式将日期和时间值转换为字符串
StrToDateTime 将带有日期和时间信息的字符串转换为TdateTime类型值,如串有误将引发
一个异常
StrToDate 将带有日期信息的字符串转换为TDateTime类型格式
StrToTime 将带有时间信息的字符串转换为TDateTime类型格式
DayOfWeek 根据传递的日期参数计算该日期是一星期中的第几天
DecodeDate 根据日期值返回年、月、日值
DecodeTime 根据时间值返回时、分、秒、毫秒值
EncodeDate 组合年、月、日值为TDateTime类型值
EncodeTime 组合时、分、秒、毫秒值为TDateTime类型值
为了显示怎样使用日期时间类型及其相关例程,我建了一个简单的例子TimeNow。该例子在
主窗体中设置了一个按钮和一个列表框(ListBox)。开始执行时,程序自动计算并显示当
前的时间及日期,以后每次单击按钮 ,显示从程序开始至当前的时间。
下面列出了窗体的OnCreate 事件代码:
procedure TFormTimeNow.FormCreate(Sender: TObject);
begin
StartTime := Now;
ListBox1.Items.Add (TimeToStr (StartTime));
ListBox1.Items.Add (DateToStr (StartTime));
ListBox1.Items.Add ('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 TFormTimeNow.ButtonElapsedClick(Sender: TObject);
var
StopTime: TDateTime;
begin
StopTime := Now;
ListBox1.Items [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 dat
e/time formatting variables 主题下,你可找到完整的清单。
注意:Delphi 中有一个DateTimePicker 控件,它提供了选择日期的常用途径,即从一个
日历中选择日期。
特定的Windows 类型
到目前为止,我们所看到的预定义数据类型都是Pascal 语言自身定义的类型。 Delphi 中
还包含Windows系统定义的数据类型,这些数据类型不是Pascal语言的组成部分,而是Win
dows 库的一部分。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 将一个有序数据转换为一个ANSI字符
Ord 将一个有序类型值转换为它的序号
Round 转换一个实型值为四舍五入后的整型值
Trunc 转换一个实型值为小数截断后的整型值
Int 返回浮点数的整数部分
IntToStr 将数值转换为字符串
IntToHex 将数值转换为十六进制数字符串
StrToInt 将字符串转换为一个整型数,如字符串不是一个合法的整型将引发异常
StrToIntDef 将字符串转换为一个整数,如字符串不合法返回一个缺省值
Val 将字符串转换为一个数字(传统Turbo Pascal例程用于向后兼容)
Str 将数字转换为格式化字符串(传统Turbo Pascal例程用于向后兼容)
StrPas 将零终止字符串转换为Pascal类型字符串,在32位Delphi中这种类型转换是自动进
行的
StrPCopy 拷贝一个Pascal类型字符串到一个零终止字符串, 在32位Delphi中这种类型转换
是自动进行的
StrPLCopy 拷贝Pascal类型字符串的一部分到一个零终止字符串
FloatToDecimal 将一个浮点数转换为包含指数、数字及符号的十进制浮点记录类型
FloatToStr 将浮点值转换为缺省格式的字符串
FloatToStrF 将浮点值转换为特定格式的字符串
FloatToText 使用特定格式,将一个浮点值拷贝到一个字符串缓冲区
FloatToTextFmt 同上面例程,使用特定格式,将一个浮点值拷贝到一个字符串缓冲区
StrToFloat 将一个Pascal字符串转换为浮点数
TextToFloat 将一个零终止字符串转换为浮点数
注意:在最近版本的Delphi Pascal 编译器中,Round 函数是以 CPU 的 FPU (浮点部件)
处理器为基础的。这种处理器采用了所谓的 "银行家舍入法",即对中间值 (如 5.5、6.
5) 实施Round函数时,处理器根据小数点前数字的奇、偶性来确定舍入与否,如 5.5 Rou
nd 结果为 6,而 6.5 Round 结果也为6, 因为 6 是偶数。
结束语
我们讨论了Pascal的基本数据类型。Pascal语言还有一个非常重要的特征:它允许编程者
自定义数据类型,称为“用户自定义数据类型”,这在下一部分进行讨论。
下一页: 用户自定义数据类型
--
# ■■■■■■■■■■〓▄▃▂▁愿你圣诞快樂 ︸︸||︸
. . . . o o o o o
_____ o _______
____==== ]OO|_n_n__][. |Go to|
[________]_|__|________)< | 滢滢| 天 外 来 客
oo oo 'oo OOOO-| oo\\_ ~~~|~~~
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 四: 用户自定义数据类型
发信站: 哈工大紫丁香 (2002年12月29日09:39:18 星期天), 站内信件
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程序
(可从www.marcocantu.com下载)查看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中的集合类型属性
另一个基于集合类型的属性是字体。字体类型值可以是粗体、斜体、带下画线、带删除线
等,一种字型可以既是斜体又是粗体,也可以没有属性,或者带有全部的属性。因此用集
合类型来表示它。你可以象下面代码那样,在程序中给集合赋值:
Font.Style := []; // no style
Font.Style := [fsBold]; // bold style only
Font.Style := [fsBold, fsItalic]; // two styles
你也能对一个集合进行许多不同方式的操作,包括把两个相同类型的集合变量相加(或更
准确地说,计算两个集合变量的并集):
Font.Style := OldStyle + [fsUnderline]; // two sets
此外,你可以通过OrdType 查阅Delphi 控件库中定义的集合类型取值列表。OrdType 放在
本书源代码的TOOLS 目录中。
数组类型
数组类型定义了一组指定类型的元素序列,在方括号中填入下标值就可访问数组中的元素
。定义数组时,方括号也用来指定可能的下标值。例如,下面的代码中定义了一个有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语言的),并且Delp
hi 控件库也在往这方向靠拢。
使用数组时,你总要用标准函数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
BirthDay.Year := 1997;
BirthDay.Month := 2;
BirthDay.Day := 14;
类和对象可以看作是记录类型的扩展。Delphi 库趋向于用类替代记录类型,不过Windows
API中定义了许多记录类型。
记录类型中允许包含variant 域,它表示多个域能公用同一内存区,而且域可以是不同类
型(这相应于C语言中的联合union)。换句话说,你可以通过variant 域或说是一组域访
问记录中同一个内存位置,但是各个值仍需区别对待。variant类型主要用来存贮相似但又
不同的数据,进行与类型映射(typecasting)相似的类型转换(自从typecasting 引入Pas
cal,已很少用到这种方法了)。虽然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 TFormGPF.BtnGpfClick(Sender: TObject);
var
P: ^Integer;
begin
P := nil;
ShowMessage (IntToStr (P^));
end;
通过运行例GPF,或者看图4.4,你可以看到上述这种结果。
图 4.4: 访问nil指针引起的系统错误
将上面程序加以修改,访问数据就安全了。现在将一个已存在的局部变量赋给指针,指针
使用就安全了,虽然如此,我还是加上了一个安全检查语句:
procedure TFormGPF.BtnSafeClick(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 对象模型,理解指针是很重要的,因为Delph
i 对象模型在幕后使用了指针。
注意:虽然在Delphi中不常使用指针,但是你经常会用一个极为相似的结构--引用(refe
rences)。每个对象实例实际上是一个隐含的指针,或说是对其实际数据的引用,利用引
用,你能象用其他数据类型一样使用对象变量。
文件类型
另一个Pascal特定的类型构造器是文件类型(file)。文件类型代表物理磁盘文件,无疑
是Pascal语言的一个特殊类型。按下面的方式,你可以定义一个新的数据类型:
type
IntFile = file of Integer;
然后,你就能打开一个与这个结构相应的物理文件、向文件中写入整数、或者从文件中读
取当前的值。
Pascal 文件类型的使用很直观,而且Delphi 中也定义了一些控件用于文件保存和装载,
以及对数据流和数据库的支持。
结束语
我们讨论了自定义数据类型,完成了对Pascal 数据类型体系的介绍,为下一部分“语句”
作好了准备。语句用于操作我们所定义的变量。
下一页: 语句
--
老茄子躲在一边偷着乐 :-)
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 五: 语句
发信站: 哈工大紫丁香 (2002年12月29日09:40:05 星期天), 站内信件
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 TForm1.Button1Click(Sender: TObject);
begin
// simple if statement
if CheckBox1.Checked then
ShowMessage ('CheckBox1 is checked')
end;
当点击button1,如果第一个复选框中有复选标记,那么这个程序将显示一条消息(见图5
.1)。我用了ShowMessage 函数,因为它是Delphi中最简单的短信息显示函数。
图 5.1: 例IfTest显示的信息
如果点击按钮后没有反应,表明复选框未被选中。对于这种情况,最好能交代得更清楚些
,为此在第二个按钮的代码中,我用了if-then-else 语句:
procedure TForm1.Button2Click(Sender: TObject);
begin
// if-then-else statement
if CheckBox2.Checked then
ShowMessage ('CheckBox2 is checked')
else
ShowMessage ('CheckBox2 is NOT checked');
end;
要注意的是,不能在第一句之后、else 关键词之前加分号,否则编译器将告知语法错误。
实际上,if-then-else 语句是单纯的一条语句,因此不能在语句中间加分号。
if 语句可以很复杂,句子中的条件部分可以是一系列条件(用and、 or 、 not等布尔操
作符联接起来),if语句又可以嵌套另一个if语句,见例IfTest中其它两个按钮的示范代
码:
procedure TForm1.Button3Click(Sender: TObject);
begin
// statement with a double condition
if CheckBox1.Checked and CheckBox2.Checked then
ShowMessage ('Both check boxes are checked')
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
// compound if statement
if CheckBox1.Checked then
if CheckBox2.Checked 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语句 或 rep
eat 语句,而不是for循环语句。
注意:for循环计数器不必非是数字,它可以是任何有序类型的值,例如一个字符或一个枚
举类型值。
while语句和repeat语句
while-do 循环语句和 repeat-until 语句的不同点在于repeat 循环语句的代码至少要执
行一次。从下面的简例很容易理解这一点:
while (I <= 100) and (J <= 100) do
begin
// use I and J to compute something...
I := I + 1;
J := J + 1;
end;
repeat
// use I and J to compute something...
I := I + 1;
J := J + 1;
until (I > 100) or (J > 100);
从上可见即使 I 或 J 的初始值大于100,repeat-until循环中的代码也仍会执行一次。
注意:两种循环另一个关键的不同点是,repeat-until 循环的条件是反向的条件,只要不
满足这个条件,循环就执行;当条件满足时,循环终止。这正好与while-do 循环相反,w
hile-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 TForm1.BtnForClick(Sender: TObject);
var
I: Integer;
begin
ListBox1.Items.Clear;
for I := 1 to 20 do
Listbox1.Items.Add ('String ' + IntToStr (I));
end;
第二个button的事件代码稍微复杂点。本例中让while 循环基于一个随机增长的计数器。
为实现它,我调用了Randomize 过程, 用它来重置随机数发生器,还调用了Random 函数,
其取值范围为100, 即函数返回0至99之间的随机数,随机数序列控制while 循环的执行次
数。
procedure TForm1.BtnWhileClick(Sender: TObject);
var
I: Integer;
begin
ListBox1.Items.Clear;
Randomize;
I := 0;
while I < 1000 do
begin
I := I + Random (100);
Listbox1.Items.Add ('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
BirthDay.Year := 1997;
BirthDay.Month := 2;
BirthDay.Day := 14;
可以用with语句改进后半部分代码,如下:
begin
with BirthDay do
begin
Year := 1995;
Month := 2;
Day := 14;
end;
在Delphi程序中,这种方法能用于访问控件和类变量。现在通过with语句访问列表框的条
目,我们重写上面循环例子的最后部分:
procedure TForm1.WhileButtonClick(Sender: TObject);
var
I: Integer;
begin
with ListBox1.Items 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语句通常能简化你的代码,尤其对嵌套域。例如,你要改变窗
体画笔的宽度和颜色,你可以写代码如下:
Form1.Canvas.Pen.Width := 2;
Form1.Canvas.Pen.Color := clRed;
但如果用With语句代码会更简单:
with Form1.Canvas.Pen 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属性, 又由于执行的代码是针对窗体对象的(
我们正在写窗体的方法),所以窗体对象即成为默认的访问对象。如果这样写:
Button1.Width := 200;
Button1.Caption := 'New Caption';
Button1.Color := clRed; // error!
编译器会给出一个错误。通常,由于with语句在当前的块中定义了新的标识符,省略了原
有的标识符,可能引起在同一块内错误地访问另一个标识符(就象上面的这段代码)。即
使存在种种缺陷,我还是建议你习惯于使用with语句,因为with语句确实是非常便利,并
且有时也会使代码更容易读懂。
然而,你应该避免使用多个with语句,如:
with ListBox1, Button1 do...
这样会使后面的代码非常难读,因为,对该块中定义的每个属性,你都要根据相应的属性
以及控件的次序,才能推出所访问的控件。
注意:说到可读性,要知道Pascal 没有endif 或endcase 语句。如果if语句有一个begin
-end 块,那么end标志语句结束;另外,case语句也总是以一个end结束。所有这些end语
句,常常是一个接一个,使代码难以理解, 只有通过缩排跟踪,才能追出一个end所对应
的语句。解决这个问题的一个通用办法, 也是使代码更可读的办法,是在end后面加注释
,如下例:
if ... then
...
end; // if
结束语
我描述了怎样编写条件语句和循环语句的代码。程序通常被分成例程、过程或函数,而不
是把所有语句列成长长的列表。这是下一部分的主题,下一部分也将介绍一些Pascal的高
级内容。
下一页: 过程与函数
--
︿○╯
(︿
〈 x . . ﹍﹍
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 六: 过程与函数
发信站: 哈工大紫丁香 (2002年12月29日09:40:44 星期天), 站内信件
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 TForm1.Button1Click (Sender: TObject);
begin
Hello;
end;
procedure TForm1.Button2Click (Sender: TObject);
var
X, Y: Integer;
begin
X := Double (StrToInt (Edit1.Text));
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:';
Label1.Caption := Format ('Total: %d', [N]);
Label2.Caption := Format ('Int: %d, Float: %f', [N, 12.4]);
Label3.Caption := Format ('%s %d', [S, N * 2]);
从上可见,传递的参数可以是常量值、变量值或一个表达式。声明这类函数很简单,但是
怎样编写函数代码呢?怎样知道参数类型呢?对类型可变的开放数组,其数组元素与TVar
Rec 类型元素兼容。
注意:不要把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 TForm1.Button4Click(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 con
ventions 主题下,你能找到有关Delphi调用协定的总结内容。
什么是方法?
如果你使用过Delphi 或读过Delphi 手册,大概已经听说过“方法”这个术语。方法是一
种特殊的函数或过程,它与类这一数据类型相对应。在Delphi 中,每处理一个事件,都需
要定义一个方法,该方法通常是个过程。不过一般“方法”是指与类相关的函数和过程。
你已经在本章和前几章中看到了几个方法。下面是Delphi 自动添加到窗体源代码中的一个
空方法:
procedure TForm1.Button1Click(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也可能调用DoubleHell
o。当然,必须设置条件终止这个递归,避免栈的溢出。上面的代码可以在例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 的另一个独特功能是可定义过程类型。过程类型属于语言的高级功能,De
lphi 程序员不会经常用到它。因为后面章节要讨论相关的内容(尤其是“方法指针” De
lphi用得特别多),这里不妨先了解一下。如果你是初学者,可以先跳过这部分,当学到
一定程度后再回过头阅读这部分。
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 TForm1.DoubleRadioButtonClick(Sender: TObject);
begin
IP := DoubleTheValue;
end;
当用户按Apply 按钮,程序就执行过程变量保存的过程:
procedure TForm1.ApplyButtonClick(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你可看到,当开始输入一个常量字符串时,D
elphi只显示第一个参数为字符串的两个ShowMsg例程参数列表,滤掉了第一个参数为整数
的例程。
图 6.4: 窗口中代码参数提示条显示的重载例程参数
重载例程必须用overload关键字明确标示,你不能在同一单元中重载没有overload标示的
例程,否则会出现错误信息: "Previous declaration of '<name>' was not marked wi
th the 'overload' directive."。不过你可以重载在其他单元中声明的例程,这是为了与
以前的Delphi版本兼容,以前的Delphi版本允许不同的单元重用相同的例程名。无论如何
,这是例程重载的特殊情况不是其特殊功能,而且不小心会出现问题。
例如在一个单元中添加以下代码:
procedure MessageDlg (str: string); overload;
begin
Dialogs.MessageDlg (str, mtInformation, [mbOK], 0);
end;
这段代码并没有真正重载原始的MessageDlg 例程,实际上如果键入:
MessageDlg ('Hello');
你将得到一个有意思的错误消息,告诉你缺少参数。调用本地例程而不是VCL的唯一途径是
明确标示例程所在单元,这有悖于例程重载的思想:
OverDefF.MessageDlg ('Hello');
确省参数
Delphi 4 中添加了一个新功能,即允许你给函数的参数设定确省值,这样调用函数时该参
数可以加上,也可以省略。下例把应用程序全程对象的MessageBox 方法重新包装了一下,
用PChar 替代字符串,并设定两个确省值:
procedure MessBox (Msg: string;
Caption: string = 'Warning';
Flags: LongInt = mb_OK or mb_IconHand);
begin
Application.MessageBox (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的一些编程元素。
下一页: 字符串操作
--
圣诞节快到了,祝福你!!!!!!
/\~~~~~~~~~~~~~\ ^*^ ☆ $$ .☆
./ \~~~▓~▓ ~~~~\ ◆ 圣诞快樂 * $◢◣$ *
/ ^^ \ ══════\.◆ * * * $◢★◣$ *
..▎[] ▎田 田 ▎ |┃◆ . * $◢■■◣$ *
&&▎ ▎ ▎'|'▎ @ * $◢■■■◣$ *
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 七: 字符串操作
发信站: 哈工大紫丁香 (2002年12月29日09:41:21 星期天), 站内信件
PASCAL
精要 七、
字符串操作
Delphi 中字符串的操作很简单,但幕后情况却相当复杂。Pascal 传统的字符串操作方法
与Windows 不同,Windows吸取了C语言的字符串操作方法。32位Delphi中增加了长字符串
类型,该类型功能强大,是Delphi 确省的字符串类型。
字符串类型
在Borland公司的Turbo Pascal和16位Delphi中,传统的字符串类型是一个字符序列,序列
的头部是一个长度字节,指示当前字符串的长度。由于只用一个字节来表示字符串的长度
,所以字符串不能超过255个字符。这一长度限制为字符串操作带来不便,因为每个字符串
必须定长(确省最大值为255),当然你也可以声明更短的字符串以节约存储空间。
字符串类型与数组类型相似。实际上一个字符串差不多就是一个字符类型的数组,因为用
[]符号,你就能访问字符串中的字符,这一事实充分说明了上述观点。
为克服传统Pascal 字符串的局限性,32位Delphi增加了对长字符串的支持。这样共有三种
字符串类型:
ShortString 短字符串类型也就是前面所述的传统 Pascal 字符串类型。这类字符串最多
只能有255个字符,与16位Delphi中的字符串相同。短字符串中的每个字符都属于ANSICha
r 类型(标准字符类型)。
ANSIString长字符串类型就是新增的可变长字符串类型。这类字符串的内存动态分配,引
用计数,并使用了更新前拷贝(copy--on-write)技术。这类字符串长度没有限制(可以存
储多达20亿个字符!),其字符类型也是ANSIChar 类型。
WideString 长字符串类型与ANSIString 类型相似,只是它基于WideChar 字符类型,Wid
eChar 字符为双字节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 函数中,用常量参数传递字符串至关重要。用拷贝方式(值参)传递会引起
副作用,因为函数执行过程中会产生一个对字符串的额外引用;与此相反,通过引用(va
r)或常量(const)参数传递不会产生这种情况。由于本例不希望字符串被修改,因此选
用常量参数。
为获取字符串内存地址(有利于识别串的实际内容也有助于观察两个不同的串变量是否引
用了同一内存区),我通过类型映射把字符串类型强行转换为整型。字符串实际上是引用
,也就是指针:字符串变量保存的是字符串的实际内存地址。
为了提取引用计数信息,我利用了一个鲜为人知的事实:即字符串长度和引用计数信息实
际上保存在字符串中, 位于实际内容和字符串变量所指的内存位置之前,其负偏移量对字
符串长度来说是-4(用Length 函数很容易得到这个值),对引用记数来说是-8。
不过必须记住,以上关于偏移量的内部信息在未来的Delphi版本中可能会变,没有写入正
式Delphi文档的特性很难保证将来不变。
通过运行这个例子,你会看到两个串内容相同、内存位置相同、引用记数为2,如图7.1中
列表框上部所示。现在,如果你改变其中一个字符串的值,那么更新后字符串的内存地址
将会改变。这是copy-on-write技术的结果。
图 7.1: 例StrRef显示两个串的内部状态,包括当前引用计数
第二个按钮(Change)的OnClick 事件代码如下,结果如图7.1列表框第二部分所示:
procedure TFormStrRef.BtnChangeClick(Sender: TObject);
begin
Str1 [2] := 'a';
ListBox1.Items.Add ('Str1 [2] := ''a''');
ListBox1.Items.Add ('Str1 - ' + StringStatus (Str1));
ListBox1.Items.Add ('Str2 - ' + StringStatus (Str2));
end;
注意,BtnChangeClick 只能在执行完BtnAssignClick 后才能执行。为此,程序启动后第
二个按钮不能用(按钮的Enabled 属性设成False);第一个方法结束后激活第二个按钮。
你可以自由地扩展这个例子,用StringStatus 函数探究其它情况下长字符串的特性。
Delphi 字符串与 Windows PChar字符串
长字符串为零终止串,这意味着长字符串完全与Windows使用的C语言零终止串兼容,这给
长字符串使用带来了便利。一个零终止串是一个字符序列,该序列以一个零字节(或null)
结尾。零终止串在Delphi中可用下标从零开始的字符数组表示,C语言就是用这种数组类型
定义字符串,因此零终止字符数组在Windows API 函数(基于C语言)中很常见。由于Pas
cal长字符串与C语言的零终止字符串完全兼容,因此当需要把字符串传递给Windows API
函数时,你可以直接把长字符串映射为PChar 类型。
下例把一个窗体的标题拷贝给PChar 字符串(用API 函数GetWindowText),然后再把它拷
贝给按钮的Caption 属性,代码如下:
procedure TForm1.Button1Click (Sender: TObject);
var
S1: String;
begin
SetLength (S1, 100);
GetWindowText (Handle, PChar (S1), Length (S1));
Button1.Caption := S1;
end;
你可以在例LongStr 中找到这段代码。注意:代码中用SetLength函数为字符串分配内存,
假如内存分配失败,那么程序就会崩溃;如果你直接用PChar 类型传递值(而不是象以以
上代码那样接受一个值),那么代码会很简单,因为不需要定义临时字符串,也不需要初
始化串。下面代码把一个Label(标签)控件的Caption 属性作为参数传递给了API函数,只
需要简单地把属性值映射为PChar类型:
SetWindowText (Handle, PChar (Label1.Caption));
当需要把WideString 映射为Windows兼容类型时,你必须用PWideChar 代替PChar进行转换
,WideString常用于OLE和 COM 程序。
刚才展现了长字符串的优点,现在谈谈它的弊端。当你把长字符串转换为PChar 类型时可
能会引发一些问题,问题根本在于:转换以后字符串及其内容将由你来负责,Delphi 不再
管了。现在把上面Button1Click代码稍作修改:
procedure TForm1.Button2Click(Sender: TObject);
var
S1: String;
begin
SetLength (S1, 100);
GetWindowText (Handle, PChar (S1), Length (S1));
S1 := S1 + ' is the title'; // this won't work
Button1.Caption := S1;
end;
程序编译通过,但执行结果会令你惊讶,因为按钮的标题并没变,所加的常量字符串没有
添加到按钮标题中。问题原因是Windows写字符串时(在GetWindowText API调用中),Wi
ndows 没有正确设置Pascal 长字符串的长度。Delphi 仍可以输出该字符串,并能通过零
终止符判断字符串何时结束,但是如果你在零终止符后添加更多的字符,那么这些字符将
被忽略。
怎么解决这个问题呢?解决方法是告诉系统把GetWindowText API函数返回的字符串再转换
成Pascal字符串。然而,如果你用以下代码:
S1 := String (S1);
Delphi 系统将不予理睬,因为把一种类型转换为它自己的类型是无用的操作。为获得正确
的Pascal 长字符串,需要你把字符串重新映射为一个PChar 字符串,然后让Delphi 再把
它转回到字符串:
S1 := String (PChar (S1));
实际上,你可以跳过字符串转换(S1 := PChar (S1));, 因为在Delphi中Pchar转换到st
ring是自动执行的,最终代码如下:
procedure TForm1.Button3Click(Sender: TObject);
var
S1: String;
begin
SetLength (S1, 100);
GetWindowText (Handle, PChar (S1), Length (S1));
S1 := String (PChar (S1));
S1 := S1 + ' is the title';
Button3.Caption := 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) 将浮点值转换为现金数量表示的字符串,转换结果取决于地域设置,详见Delph
i帮助文件的Currency and date/time formatting variables主题
领会以上内容最好的办法是你亲自进行字符串格式化试验。为了简便起见,我写了FmtTes
t 程序,它能将整数和浮点数转换为格式化字符串。从图7.2可见,程序窗体分为左右两部
分,左边对应整型数字转换,右边对应浮点数转换。
各部分的第一个编辑框显示需要格式化为字符串的数值。第一个编辑框下方有一个按钮,
用来执行格式化操作并在消息框中显示结果;紧接着第二个编辑框用于输入格式化类型串
。你也可以单击ListBox 控件中的任一行,选择预定义的格式化类型串,也可以自行输入
,每输入一个新的格式化类型串,该类型串就会被添加到列表框中(注意,关闭程序就失
去了添加的类型)。
图 7.2: 程序 FmtTest 的浮点值输出
本例只简单使用了不同的控制文本来产生输出,下面列出了其中一个Show 按钮事件代码:
procedure TFormFmtTest.BtnIntClick(Sender: TObject);
begin
ShowMessage (Format (EditFmtInt.Text,
[StrToInt (EditInt.Text)]));
// if the item is not there, add it
if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then
ListBoxInt.Items.Add (EditFmtInt.Text);
end;
这段代码主要用EditFmtInt 编辑框的文本和EditInt 控件的值进行了格式化操作。如果格
式化类型串没有在列表框中列出,那么输入的串会被添加到列表框中;如果用户在列表框
中进行点击,代码会把点击的串移到编辑框中:
procedure TFormFmtTest.ListBoxIntClick(Sender: TObject);
begin
EditFmtInt.Text := ListBoxInt.Items [
ListBoxInt.ItemIndex];
end;
结束语
字符串是一种很常用的数据类型,尽管在很多情况下不理解字符串怎样工作也能安全使用
它们,不过通过本章,了解了字符串的内部运行机制之后,你就能更充分地利用字符串类
型的强大功能。
Delphi用特殊的动态方式处理字符串内存,正如动态数组一样,这将在下一章进行讨论。
下一页: 内存
--
# ■■■■■■■■■■〓▄▃▂▁愿你圣诞快樂 ︸︸||︸
. . . . o o o o o
_____ o _______
____==== ]OO|_n_n__][. |Go to|
[________]_|__|________)< | 滢滢| 天 外 来 客
oo oo 'oo OOOO-| oo\\_ ~~~|~~~
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 八: 内存
发信站: 哈工大紫丁香 (2002年12月29日09:41:49 星期天), 站内信件
PASCAL
精要 八、
内存
作者的话:本章内容涉及内存处理,讨论各种内存区,并介绍动态数组。目前暂时只有动
态数组部分。
Delphi 4 的动态数组
传统的Pascal 语言其数组大小是预先确定的,当你用数组结构声明数据类型时,你必须指
定数组元素的个数。专业程序员也许知道些许动态数组的实现技术,一般是采用指针,用
手工分配并释放所需的内存。
Delphi 4中增加了非常简单的动态数组实现方法,实现过程效仿我前面讲过的动态长字符
串。与长字符串一样,动态数组的内存动态分配并且引用记数,不过动态数组不支持 cop
y-on-write 技术。这不是个大问题,因为你可以把变量值设置为nil释放数组内存。
这样你就可以声明一个不指定元素个数的数组,并用SetLength 过程给数组分配一个特定
大小的内存,SetLength 过程还可以改变数组大小而不影响其内容,除此外还有一些字符
串过程也可用于数组,如Copy 函数。
以下摘录的代码突出了一点,这就是:定义数组后必须先为它分配内存,然后才能开始使
用:
procedure TForm1.Button1Click(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 TForm1.FormCreate(Sender: TObject);
begin
// allocate
SetLength (Array1, 100);
end;
这样就把数组所有值设置为0。完成这段代码你马上就能读写数组元素的值,而不用害怕内
存出错,当然条件是你没有试图访问超过数组上界的元素。为了更好地初始化,程序中添
加了一个按钮,执行数组元素赋值操作:
procedure TForm1.btnFillClick(Sender: TObject);
var
I: Integer;
begin
for I := Low (Array1) to High (Array1) do
Array1 [I] := I;
end;
Grow 按钮用于修改数组大小,但并不影响数组内容。单击Grow 按钮后,你可以用Get va
lue按钮进行检验:
procedure TForm1.btnGrowClick(Sender: TObject);
begin
// grow keeping existing values
SetLength (Array1, 200);
end;
procedure TForm1.btnGetClick(Sender: TObject);
begin
// extract
Caption := IntToStr (Array1 [99]);
end;
Alias 按钮的OnClick 事件代码稍复杂些,程序通过 := 算子把一个数组拷贝给另一个数
组,从而有效地创建了一个别名(一个新变量,但引用内存中同一数组)。从中可见,如
果你改变了其中一个数组,那么另一个同样也会改变,因为它们指向同一个内存区:
procedure TForm1.btnAliasClick(Sender: TObject);
begin
// alias
Array2 := Array1;
// change one (both change)
Array2 [99] := 1000;
// show the other
Caption := IntToStr (Array1 [99]);
在btnAliasClick 事件中增加了两部分操作内容。第一部分是数组等同测试,不过并不是
测试实际的数组元素,而是测试数组所引用的内存区,检测变量是不是内存中同一数组的
两个别名:
procedure TForm1.btnAliasClick(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 编程
--
圣诞节快到了,祝福你!!!!!!
/\~~~~~~~~~~~~~\ ^*^ ☆ $$ .☆
./ \~~~▓~▓ ~~~~\ ◆ 圣诞快樂 * $◢◣$ *
/ ^^ \ ══════\.◆ * * * $◢★◣$ *
..▎[] ▎田 田 ▎ |┃◆ . * $◢■■◣$ *
&&▎ ▎ ▎'|'▎ @ * $◢■■■◣$ *
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 九: Windows编程
发信站: 哈工大紫丁香 (2002年12月29日09:42:20 星期天), 站内信件
PASCAL
精要 九、
Windows 编程
Delphi 利用Object Pascal 和可视控件库(VCL)对底层的Windows API 进行了完美的封
装,所以很少需要使用基础Pascal 语言来建立Windows应用程序,也无需直接调用Window
s API 函数。尽管如此,如果遇到特殊情况,VCL 又不支持,Delphi程序员还得直接面对
Windows编程。不过只有在极其特殊的情况下,例如:基于不寻常API 调用的Delphi新控件
开发, 你才需要这样做,这里我不想讨论这方面内容,我只想让大家看一下与操作系统交
互的几个Delphi元素以及Delphi程序员能从中获益的Windows编程技术。
Windows 句柄
Delphi从Windows 引入了不少数据类型,其中句柄最重要。这种数据类型名为THandle,该
类型在Windows 单元中定义:
type
THandle = LongWord;
句柄数据类型通过数字实现,但并不当数字用。在Windows 中,句柄是一个系统内部数据
结构的引用。例如,当你操作一个窗口,或说是一个Delphi 窗体时,系统会给你一个该窗
口的句柄,系统会通知你:你正在操作142号窗口,就此,你的应用程序就能要求系统对1
42号窗口进行操作——移动窗口、改变窗口大小、把窗口极小化为图标,等等。实际上许
多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 属性,获取窗体对应的窗口句柄。调用Int
ToStr ,把句柄数值转换为一个字符串,然后再把它添加到窗体标题中,如图9.1:
procedure TFormWHandle.FormCreate(Sender: TObject);
begin
Caption := Caption + ' ' + IntToStr (Handle);
end;
因为FormCreate 是窗体类的方法,它可直接访问同类的其他属性和方法。因此,在这个过
程中我们能够直接访问窗体的Caption 属性和Handle 属性。
图 9.1: 例 WHandle 显示窗体句柄,每次运行程序得到的句柄值不同
如果你多此次执行该程序,通常会获得不同的句柄值。这个值实际上是由Windows 操作系
统确定并返回给应用程序的。(句柄从来不是由程序决定的,而且句柄没有预定义值,句
柄是由系统决定的,每执行一次程序,产生一个新值。)
当你单击按钮,程序将调用Windows API 函数SetWindowText,它会根据第一个传递参数改
变窗口的标题。更准确地说,所用的API 函数其第一个参数是需要修改窗体的句柄:
procedure TFormWHandle.BtnCallAPIClick(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 'gdi32.dll' name 'LineTo';
这段声明表示函数LineTo 的代码同名保存在GDI32.DLL 动态链接库中(最重要的Windows
系统库之一)。实际应用时,外部声明中的函数名与DLL中的函数名可以不同。
一般你不需要象刚才所例举的那样写声明,因为Windows 单元和一些Delphi 系统单元中已
包含了这些声明。只有在调用自定义DLL,或调用Delphi 中未定义的Windows 函数时,你
才能需要写外部声明。
注意:在16位Delphi中,外部声明使用不带扩展名的库名,后面跟name指令(如上所示)
或是一个index指令,后面跟DLL中函数的序号。尽管Win32 仍然允许通过序号访问DLL函数
,但是微软公司已经声明未来将不支持这种访问方式,这一改变反映了系统库访问方式的
改变。还要注意的是:目前Delphi的Windows 单元已取代了16位Delphi的WinProcs 和WinT
ypes 单元。
回调函数
从第六章已经了解到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 要求程序员遵循stdca
ll 调用协定。缺省情况下,Delphi使用另一种更高效的调用协定,其关键字为register。
下面是一个与定义兼容的回调函数,此函数把窗口的标题读到字符串中,然后添加到给定
窗体的一个列表框中:
function GetTitle (Hwnd: THandle; Param: Pointer): Boolean; stdcall;
var
Text: string;
begin
SetLength (Text, 100);
GetWindowText (Hwnd, PChar (Text), 100);
FormCallBack.ListBox1.Items.Add (
IntToStr (Hwnd) + ': ' + Text);
Result := True;
end;
窗体有一个几乎覆盖整个窗体的列表框,窗体顶部有一个小面板,面板上有一个按钮。当
按下按钮时,EnumWindows API函数被调用,并且GetTitle 函数作为参数传递给它:
procedure TFormCallback.BtnTitlesClick(Sender: TObject);
var
EWProc: EnumWindowsProc;
begin
ListBox1.Items.Clear;
EWProc := GetTitle;
EnumWindows (@EWProc, 0);
end;
你可以直接调用GetTitle函数,不必先把值保存到过程类型临时变量中,上例这么做是为
了使回调过程更清楚。程序运行结果确实很有意思,正如你在图9.2中看到的那样,结果显
示了系统中正在运行的所有主窗口,其中大部分是隐藏的,你通常看不到,许多实际上没
有标题。
图 9.2: 例CallBack输出结果--当前所有主窗体,其中包括可见及隐藏的窗体
最小的Windows 程序
为了完整介绍Windows 编程及Pascal 语言,现在我展示一个简单但完整的应用程序,建立
该程序没有使用VCL库。这个程序只是简单地采用命令行参数(保存在系统全程变量cmdLi
ne中),并利用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显示了资源管理器及相应的输出。
图9.3: 把一个文件拖放到执行文件上,给例StrParam提供命令行参数
结束语
在这一章中,我们对Windows 编程的底层内容进行了介绍,讨论了句柄和简单的Windows
程序。对于常规的Windows 编程任务,通常只需使用Delphi 提供的可视开发工具及VCL可
视控件库。但是这超出了本书讨论的范围,因为本书讨论的是Pascal 语言。
下一章介绍variant类型。对Pascal 数据类型系统来说,它是一个非常特殊的外来物,引
入它是为了提供完全的OLE 支持。
下一页: Variant 类型
--
老茄子躲在一边偷着乐 :-)
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 十: Variant类型
发信站: 哈工大紫丁香 (2002年12月29日09:42:56 星期天), 站内信件
PASCAL
精要 十、
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,它是上面代码的扩展。窗体上有三个编辑框,一对按钮,第一个按钮的On
Click 事件代码如下:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
begin
V := 10;
Edit1.Text := V;
V := 'Hello, World';
Edit2.Text := V;
V := 45.55;
Edit3.Text := V;
end;
很有趣是不是?你可以把一个值为字符串的variant 变量赋给编辑框Text 属性,还可以把
值为整数或浮点数的variant 变量赋给Text属性。正如你在图10.1中所看到的,一切正常
。
(图10.1)按Assign按钮后,例VariTest的输出结果
图 10.1: 例 VariTest 的 Assign 按钮 Click 事件输出结果
更糟糕的是:你还可以用variant变量计算数值,从第二个按钮的Click事件代码就可看到这
一点:
procedure TForm1.Button2Click(Sender: TObject);
var
V: Variant;
N: Integer;
begin
V := Edit1.Text;
N := Integer(V) * 2;
V := N;
Edit1.Text := V;
end;
至少这种代码带有一定危险性,如果第一个编辑框包含了一个数字,那么一切运行正常;
如果不是,将会引发异常。这里再重申一遍,如果不到万不得以,不要随便使用Variant
类型,还是应坚持使用传统的Pascal 数据类型和类型检查方法。在Delphi 和 VCL中,va
riant变量主要是用于 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 TForm1.Button1Click(Sender: TObject);
var
time1, time2: TDateTime;
n1, n2: Variant;
begin
time1 := Now;
n1 := 0;
n2 := 0;
ProgressBar1.Position := 0;
while n1 < 5000000 do
begin
n2 := n2 + n1;
Inc (n1);
if (n1 mod 50000) = 0 then
begin
ProgressBar1.Position := n1 div 50000;
Application.ProcessMessages;
end;
end;
// we must use the result
Total := n2;
time2 := Now;
Label1.Caption := 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 编程,但用来写一些潦潦草草的程序倒也便
利,因为不用考虑数据类型,不过正如以上所述,这样做会影响程序执行速度。
通过前面各部分我们已经介绍了绝大部分的语言特征,下面将讨论程序的总体框架和单元
模块。
下一页: 程序与单元
--
圣诞节快到了,祝福你!!!!!!
/\~~~~~~~~~~~~~\ ^*^ ☆ $$ .☆
./ \~~~▓~▓ ~~~~\ ◆ 圣诞快樂 * $◢◣$ *
/ ^^ \ ══════\.◆ * * * $◢★◣$ *
..▎[] ▎田 田 ▎ |┃◆ . * $◢■■◣$ *
&&▎ ▎ ▎'|'▎ @ * $◢■■■◣$ *
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 十一: 程序与单元
发信站: 哈工大紫丁香 (2002年12月29日09:43:38 星期天), 站内信件
PASCAL
精要 十一、
程序和单元
Delphi 应用程序中的单元,或说程序模块可谓老道精深。实际上,单元是程序模块化的基
础,类是继它之后才有的。在Delphi 应用程序中,每个窗体都有一个相对应的单元。用相
应的工具按钮, 或File > New Form 菜单命令,在工程中添加一个新窗体,实际上是增加
了一个新单元,也就是建立了该新窗体的类。
单元
虽然所有窗体都在单元中定义,但反之则不然。除窗体外,单元中还可以定义一系列能访
问的例程。选择File > New菜单命令,然后在Object Repository的New 页中选择Unit 图
标,随即当前工程中就会加入一个空白单元。单元代码分区存放,空白单元的代码如下:
unit Unit1;
interface
implementation
end.
单元的概念比较简单,单元名与文件名相同,而且必须唯一。单元包括界面区(interfac
e)及实现区(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),但是必须用同一版本的De
lphi进行编译。
在单元的界面区中可以声明许多不同的元素,包括过程、函数、全程变量及数据类型。在
Delphi 应用程序中,数据类型可能用得最频繁。每创建一个窗体,Delphi 会在单元中自
动建立一个新的数据类型--类(class)。在Delphi 单元中不仅能定义窗体;还能象传统单
元一样,只包含过程及函数;还可以定义与窗体和任何可视控件无关的类。
单元的工作空间
Pascal单元是封装性和可视性的关键,它很可能比类中的 private 和 public 关键字还要
重要。(实际上,private关键字与类单元的工作空间有关)。一个标识符(如一个变量、过
程、函数或数据类型)的工作空间是指能访问标识符的代码段。基本原则是:标识符在它工
作空间内才有意义,也就是说,只在其声明的代码块中才有意义,在工作空间外你不能访
问这些标识符。例如:
局部变量:如果你在例程或方法代码块内声明一个变量,那么单元外部不能访问这个变量
。该标识符的工作空间就是定义标识符的整个例程,其中包括嵌套例程(除非嵌套例程内
有一个同名标识符覆盖了外部定义)。当调用到例程时,其变量压入栈内存中,例程一结
束,栈中的内存就自动释放。
全程隐藏变量:如果你在单元的实现部分声明一个标识符,那么在单元外你不能使用它,
但是能在单元内任一代码块及过程中使用它。程序一启动就会为全程隐藏变量分配内存,
程序终止内存释放,你可以在单元初始化区给它赋初值。
全程变量:如果你在单元的界面部分声明标识符,那么该标识符的工作空间就扩大了,任
何Use它的单元都能访问它。这类变量的内存分配及生命周期与上类变量相同,唯一不同的
是其可见性。
只要程序单元的uses子句中列出某一单元名,那么所列单元界面区中声明的任何标识符该
程序都能访问。窗体类的变量就是这样声明的,所以你可以在其他窗体代码中访问这个窗
体以及它的公共域、方法、属性和组件。当然把什么都声明为全局标识这种编程习惯并不
好。除了明显的内存消耗问题外,使用全程变量使代码维护和更新变得困难。一句话,你
应该尽量少用全程变量。
单元用作命名空间
uses 语句是访问其他单元工作空间的标准技术,通过该语句你能访问其它单元的定义内容
。如果恰巧两个单元声明的标识符同名,也就是说你可能有两个同名的类或例程,遇到这
种情况,你可以用单元名作前缀定义类型或过程名,由此进行区分。例如用Totals.Compu
teTotal访问Totals 单元中的ComputeTotal 过程。不过这种情况最好不要经常遇到,因此
强烈建议不要在同一程序中用同一名字表示两个不同的东西。
然而,如果查阅VCL库和Windows 文件,你会发现一些Delphi 函数和Delphi 可用的Windo
ws API 函数同名,不过参数往往不同,下面以Beep 过程为例说明这个问题。
新建一个Delphi 程序,添加一个按钮,然后写入以下代码:
procedure TForm1.Button1Click(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 TForm1.Button1Click(Sender: TObject);
begin
SysUtils.Beep;
end;
不管uses子句中单元顺序如何排列,以上代码都能编译通过。在Delphi中很少有其他命名
冲突的情况,因为Delphi 代码通常放在类的方法中,如果不同类中有两个同名的方法不会
产生任何冲突,只是使用全程例程会产生冲突问题。
单元和程序
Delphi 应用程序源代码文件可分成两类,它们是一个或多个单元文件及一个程序文件,单
元文件可以看成是次一级的文件,它被应用程序的主体——程序文件——引用。理论上如
此,实际上程序文件通常是自动产生的,作用有限,程序启动并运行主窗体时才会用到它
。程序文件的代码,或说Delphi 工程文件(DPR),可用手工进行编辑,也可通过工程管
理器及其与应用程序、窗体相关的选项进行编辑。
程序文件的结构通常比单元文件的结构简单得多。下面是一个程序文件的源代码:
program Project1;
uses
Forms,
Unit1 in Unit1.PAS?{Form1DateForm};
begin
Application.Initialize;
Application.CreateForm (TForm1, Form1);
Application.Run;
end.
从上可见,文件中只有一个uses区和应用程序的主体代码,主体代码包含在begin 和 end
关键字之间。程序的uses子句特别重要,因为需要通过它来管理应用程序的编译和连接。
返回首页
--
老茄子躲在一边偷着乐 :-)
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 附录: 术语表
发信站: 哈工大紫丁香 (2002年12月29日09:44:11 星期天), 站内信件
PASCAL
精要 附录:
术语表
这里列出了用到的一些技术术语,在别的地方你也能找到它们,不过我想还是把它们集中
一处,以便查找。
堆(内存)
堆表示程序可用的内存区,也叫动态内存区。堆内存的分配与释放次序是随机的,这就是
说,如果你按次序分配三块内存,那么到时并不按分配时的次序释放内存。 堆管理器会负
责所有操作,你只需简单地使用GetMem 函数请求新内存或调用constructor 建立对象,
Delphi 会返回一个新的内存块(随意重用已经丢弃的内存块)。
堆是应用程序可用的三种内存区之一, 其它两种分别是全局内存区(存放全程变量) 和栈
。与堆相反,全程变量内存在程序启动时就分配,然后一直保留到程序终止才释放;栈的
内容请详见术语表。
Delphi 使用堆为对象、字符串、动态数组及特殊的动态内存请求(GetMem)内存分配。
Windows 应用程序的地址空间最大允许有 2 GigaByte, 其中大部分能被堆使用。
栈(内存)
栈表示程序可用的内存区,栈内存动态分配,并按特定次序分配、释放。栈内存按后进先
出次序(LIFO)分配,这表示最后分配的内存区先被释放。栈内存一般在例程中使用(过程、
函数及方法调用)。 当你调用例程时,例程参数及返回值是放在栈中的(除非使用Delphi缺
省调用方式,对调用过程进行优化)。此外,例程中声明的变量(在begin语句前的 var 块
中)也存放在栈中,所以当例程终止时,这些变量会被自动清除(在返回调用点之前以LIFO
次序释放)。
栈是应用程序可用的三种内存区之一,其它两种分别是全局内存区和堆。堆的内容请详见
术语表。
Delphi 使用栈存放例程参数及其返回值(除非你使用Delphi缺省的 register 调用协定)、
局部例程变量、Windows API 函数调用等等。
Windows 应用程序可以预留大量的栈内存,在 Delphi 中你可以通过工程选项的 linker
页设置, 不过一般采用缺省设置就可以了。 如果你收到一个栈溢出错误信息,这可能是因
为你的函数进入了死循环自调用,而不是栈空间太小。
其它
Dynamic
Static
Virtual
memory leak
painting
literal
array
API
class reference
class method
parent
owner
self
--
# ■■■■■■■■■■〓▄▃▂▁愿你圣诞快樂 ︸︸||︸
. . . . o o o o o
_____ o _______
____==== ]OO|_n_n__][. |Go to|
[________]_|__|________)< | 滢滢| 天 外 来 客
oo oo 'oo OOOO-| oo\\_ ~~~|~~~
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
发信人: coolren (茄子), 信区: BorlandDev
标 题: 终于贴完了,大家给点掌声吧 :-)
发信站: 哈工大紫丁香 (2002年12月29日09:46:34 星期天), 站内信件
校内的可以上我的机器上下载,html格式的看起来比较的方便(我最讨厌pdf格式的了)
--
老茄子躲在一边偷着乐 :-)
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 210.46.79.154]
※ 修改:·bbbbbbbbbbbb 於 12月29日09:52:25 修改本文·[FROM: 218.7.27.189]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:847.073毫秒