Delphi基础语法的学习笔记和注意事项总结
以下是我在自学Delphi的时候,对一些注意点的简单总结,并没有什么系统性可言,只是一个学习时顺手记下的笔记,主要为了当时加深对知识的印象,并没有希望能在以后的复习和使用Delphi中有什么多大的参考作用。
缺少Delphi的各种数据类型的介绍……知识点。
1. Delphi编译器自动对全局变量符初值。当程序开始时,所有的整型变量赋值为0,浮点数赋值为0.0,指针为null,字符串为空等等,因此,在源代码中不必对全局变量赋0初值
2. Object Pascal允许在const和var声明的时候,用编译期间的函数,这些函数包括Ord()、Chr()、Trunc()、Round()、High()、Low()、Sizeof(),例如下面的所有代码都是合法的
type A = array[1..2] of Integer; const w: Word = Sizeof(Byte); var i: Integer = 8; j: SmallInt = Ord(‘a’); L: Longint = Trunc(3.1415926); x: ShortInt = Round(2,71828); B1: Byte = High(A); B2: Byte = Low(A); C: char = Chr(46); //注意在定义变量的类型并赋值时,使用 = 而不是 :=
3. 如果要移植Delphi1.0的16位代码,请记住,无论是Integer还是Cardinal类型都已经从16位扩展到32位。更准确的说,在Deiphi2和Delphi3中,Cardinal被看做是31位的无符号证书,在Delphi4以后,Cardinal才真正成为32位的无符号数
4. 警告:在Delphi1、2和3中,Real是6字节的浮点数,这是Pascal特有的数据类型,和其他的语言不兼容。在Delphi4中,Real是Double类型的别名,6字节的浮点数仍然有,但现在是Real48.通过编译开关 {$REALCOMPATIBILITY ON} 可以是Real仍代表6字节的浮点数
5. 虽然AnsiString在外表上跟以前的字符串类型几乎相同,单它是动态分配的并有自动回收功能,正是因为这个功能AnsiString有时被称为是生存期自管理类型。Object Pascal能根据需要为字符串分配空间,所以不用像在C/C++中所担心的为中间结果分配缓冲区。另外,AnsiString字符串总是以null字符结束的,这使得AnsiString字符串能与Win32 API中的字符串兼容。实际上,AnsiString类型是一个指向堆栈中的字符串结构的指针
6. Borland没有将长字符串类型的内部结构写到文档里,并倒流了在Delphi后续版本中修改长字符串内部格式的权利以下的介绍主要是帮助你理解AnsiString类型是怎么工作的,并且要避免直接依赖于AnsiString结构的代码。
程序员如果在Delphi 1.0中避免了使用字符串的内部结构,则把代码移植到Delphi 2.0没有任何问题。而依赖于字符串内部结构写代码的程序在移植到Delphi 2.0时必须修改。
AnsiString字符串类型有引用技术的功能,这表示几个字符串都能指向相同的物理地址。因此,复制字符串因为仅仅是复制了指针而不是复制实际的字符串而变得非常快
当两个或更多的AnsiString类型共享一个指向相同物理地址的引用时,Delphi内存管理使用了copy-on-write技术,一个字符串要等到修改结束,才释放一个引用并分配一个物理字符串,例子:
var S1, S2: string begin S1 := ‘And now for something…;’ //给S1赋值,S1的引用计数为1 S2 := S1; //现在S2和S1指向同一个字符串,S1的引用计数为2 S2 := S2 + ‘completely different’; //S2现在改变了,所以它被复制到自己的物理空间,并且S1的引用计数减1 end;
7. 除了AnsiString以外,Delphi还提供了其他几种生存期自管理类型,这些类型包括:WideString、Variant、OleVariant、interface、dispinterface和动态数组
生存期自管理类型,又被称为自动回收类型,是指那些在被使用时就占用一定的特殊资源,而在它离开作用域时自动释放资源的类型。当然不同的类型使用不同的资源,例如AnsiString类型在被使用时就为字符串占用内存,当它超出作用域时就释放被字符串占用的内存。
8. 再练习将一个字符串转换为PChar类型时要小心,因为字符串在超出其作用范围时有自动回收的功能,因此当进行P := PChar(Stri)的赋值时,P的作用域(生存期)应当大于Str的作用域
9. 不要存放比分配给字符串的空间长度更长的字符,如果声明一个变量是string[8],并试图对这个变量赋值为’a_pretty_darn_long_string’,这个字符串将被截取为仅有8个字符,就会丢失数据。
10. 跟AnsiString类型字符串不一样,ShortString跟以null结尾的字符串不兼容,正因为如此,用ShortString调用Win32函数时,要做一些工作。
Win32 API函数需要以null结尾的字符串,不要把ShortString字符串传递给API函数,因为编译器将报错,长字符串可以传递给Win32 API函数。
下面这个S h o r t S t r i n g A s P C h a r ( )函数是在S T R U T I L S . PA S单元中定义的。func function ShortStringAsPChar(var S:ShortString):PChar;
这函数能使一个字符串以n u l l结尾,这样就能传递给需要P C h a r类型参数的Win32 API函数,如果字符串超过2 5 4个字符,多出的部分将被截掉
11. 记住在以后Delphi版本中一个字符的长度要从一个字节变成两个字节,因此,不能家丁一个字符的长度为一个字节,Sizeof() 就保证了不管字符长度是多少都能正确分配存
12. 有时候变量的类型在编译期间是不能确定的,而Variant能够在运行期间动态地改变类型,这就是引入Variant类型的目的,例子,下面的代码在编译期间和运行期间都是正确的
var V: Variant; begin V :=’Delphi is Great!’; //Variant此时是一个字符串 V :=1; //Variant此时是一个整数 V :=123.2; //Variant此时是一个浮点数 V :=true; //Variant此时是一个布尔值 V :=GreateOleObject(‘word.Basic’); //Variant此时是一个OLE对象 end;
Variant能支持所有简单的数据类型,例如整型、浮点型、字符串、布尔型、日期和时间、货币以及OLE自动化对象等。注意Variant不能表达Object Pascal对象。Variant可以表达不均匀的数组(数组的长度是可变的,它的数据元素能表达前面介绍过的任何一种类型,也可以包括另一个Variant数组)
13. 作为一条普遍的规则,请不要直接访问TVarData的数据域。如果确实需要直接访问,必须清楚正在干什么。
14. OleVariant与Variant很相像,两者的差别在于OleVariant仅支持自动化相兼容的类型。目前,不能跟自动化兼容的VType是VarString,即AnsiString类型。当试图把AnsiString字符串赋值给一个OleVariant变量时,AnsiString自动转化为OLEBSTR类型并作为varOleStr存储在Variant变量中。
15. Object Pascal允许你建立任何类型变量的数组(除了文件类型),下面的例子声明了一个数组,这个数组有八个整型数:
var A: Array[0..7] of Integer
它相当于C语言中的
int A[8];
Object Pascal的数组有一个不同于其他语言的特性,他们的下标不必以某个数为基准。像下面的例子,能从28开始定义一个有3个元素的数组
var A: Array[28..30] of Integer;
因为Object Pascal的下标不是必须从0或1开始的,在for循环中使用数组时一定要小心。在编译器中有两个内置的函数High()和Low(),它们分别返回一个数组变量或数组类型的上边界和下边界,在for循环中使用这两个语句能使程序更加稳定和易于维护,例子:
var A: Array[28..30] of Integer; I: Integer; begin for i:= Low(A) to High(A) do //不要在循环中硬编码 A[i] := i; end;
16. 定义多维数组,用逗号分开,例子
var
A: array[1..2, 1..2] of Integer;
为了访问多维数组,在方括号里用逗号隔开每一维
I := A[1, 2];
17. 利用引用对动态数组进行操作,在语义上类似于AnsiString类型,而不像普通的数组。这里有一个小实验:在下面的代码运行完成后A1[0]的值是多少?
var A1, A2 : array of Integer; begin SetLength(A1, 4); A2 := A1; A1[0] := 1; A2[0] := 26;
正确答案是26,因为赋值语句A2 := A1并不是创建新的数组,仅仅是将A1数组的引用赋给A2,因此对A2数组的任何操作都影响到A1,如果想用A1的完全拷贝赋值给A2,用标准过程Copy: A2 := Copy(A1); 当这行代码运行结束, A 1和A 2是两个独立的数组,但它们的初始值是相同的,改变其中的一个不会影响到另一个.
18. 在Object Pascal中用户自定义的结构被称为记录。它相当于C语言中的struct
{Pascal} Type MyRec = record i: Integer; d: Double; end; /*C*/ Typedef struct{ int i; double d; }MyRec;
下面的例子演示一个可变记录,其中Double、Integer和char共同占用相同的内存
type TVariantRecord = record; NullStrField : Pchar; IntField : Integer; case Integer of 0: (D: Double); 1:(I : Integer); 2:(C: char); end;
注意,Object Pascal规则声明:一个记录的可变部分不能是生存期自管理的类型。
上面的在C语言中的定义可以是
struct TUnionStruct{\
char * StrField;
int IntField;
union{
double D;
int I;
char c;
};
};
19. 一个集合最多只能有255个元素。另外,只有有序的类型才能跟关键字set of
集合在内部以位的形式存储它的元素,这使得在速度和内存利用上更有效。集合如果少于32个元素,它就存储在CPU的寄存器中,这样效率就更高了,为了用集合类型得到更高的效率。记住,集合的基本类型的元素数目要小于32。
20. 在集合中,尽可能使用Include()和Exclude()来增删元素,尽可能的少用+、-运算符。因为Include()和Exclude()仅需要一条机器指令,而+和-需要13+6n(n是喝的按位的长度)条机器指令
21. 常用的判断集合中是否有某几个元素的例子
用*运算符来计算两个集合的交集,表达式S e t 1 * S e t 2的结果是产生的集合的元素在S e t 1和S e t 2集合中都存在,下面的例子用来判断在一个给定的集合中是否有某几个元素:
if {'a', 'b', 'c'}*CharSet={'a', 'b', 'c'} then
/ /继续程序
22. Object Pascal中对象的定义大致如下
Type TChildObject = class(TparentObject); SomeVar :Integer; procedure SomeProc; end; 虽然Delphi的对象和C++中的对象不完全一致,但上面的代码大致和在C++中的如下定义相同: Class TChildObject : public TParentObject{ int SomeVar; void SomeProc(); };
要定义一个方法,类似于定义函数和过程,只是要加上对象名字和小圆点:
procedure TChildObject.SomeProc; begin {过程体} end;
Object Pascal中的小圆点,在功能上类似于C++的 :: 运算符。
23. 注意,Object Pascal的对象和C+的对象在内存中的布局不一样。因此,在Delphi中不能用C++的对象。
一个例外的情况是,在Borland C++ Builder中创建的新类,用_declspec(delphiclass)指令可以直接映射为Object Pascal类。但这样的类与普通的C++对象不兼容。
24. 一个指针变量指示了内存的位置,Pascal通用指针类型的名称是Pointer。Pointer有时又被成为无类型指针,因为他只指向内存地址,单边一起并不管指针指向的数据,这一点与Pascal严谨的风格似乎不相称,所以建议你在大部分情况下用有类型的指针。
对于有类型指针来说,编译器能准确地跟踪指针所指向内容的数据类型,这样用指针变量,编译器就能跟踪正在进行的工作。例子
Type Pint = ^Integer; //PInt现在是一个指向Integer的指针 Foo = record GobbledyGook: string; Snarf: Real; end; PFoo = ^Foo; //PFoo是一个指向Foo类型的指针 var P: Pointer; //一个无类型指针 P2: PFoo; //PFoo的实例
记住:一个指针变量仅仅是存出一个内存的地址,为指针所指向的内容分配空间是程序员要干的工作。
25. 要访问一个指针所指向的内容,在指针变量的名字后面跟上 ^ 运算符,这种方法成为对指针去内容,例子
Program PtrTest; Type MyRec = record I : Integer; S : string R : Real; end; PMyRec = ^MyRec; var Rec : PMyRec; begin New(Rec); //为Rec分配内存 Rec^.I := 10; //为Rec中的域赋值 Rec^.S := ‘And now for something completely different.’; Rec^.R := 3.14; {Rec现在满了} Dispose(Rec); //不要忘记释放空间 end;
26. 当编译器不知道要分配多少内存时,就要用到GetMem()和AllocMem(),在对PChar和Pointer类型分配内存时,编译器不可能提前告诉你要分配多少,因为它们有长度可变特性。要注意,不要试图操作分配空间以外的数据,因为这是导致“Access Violation”错误的最常见的原因。用FreeMem()来释放由getMem()和AllocMem()分配的内存。顺便说一下,AllocMem()要比GetMem()安全,因为AllocMem()总是把分配给他的内存初始化为零。
27. C程序员在学习Object Pascal时感到头疼的是,Object Pascal对指针类型的检查非常严格,例如,下面的代码中变量a和变量b并不兼容:
var a: ^Integer; b: ^Integer;
相反,在C中它们兼容
int *a;
int *b;
Object Pascal认为每一个指针类型是相异的,为了把a的值赋给b,你必须建立一个新的类型,示例如下:
Type PtrInteger = ^Integer; //建立新类型 var a, b : PtrInteger; //现在a和b相兼容了
28. 注意,只有当两个变量的数据长度一样时,才能对变量进行强制类型转换
29. 如果在一条if语句中有多个条件,你需要用括号把这几个条件分别用括号括起来,例如: if (x=7) and (y=8) then
下面的写法将会导致编译器警告: if x=7 and y-8 then
if (condition) then begin ... end //注意这里不用;。因为为了保证这个if和下面的if else是一对 //如果使用了;。就表示if语句就在此结束 //那么下面的else if 语句就会报错,因为不符合语法 else if (condition2) then begin ... end //注意这里不用;。因为为了保证这个else if和下面的 else 是一对 else begin ... end; //注意这里用; 如果else只有一句就可以是这样的 else ...; 最后一定要有; 表示结束
特别注意自在if-else if-else里面的分号的问题
if (a>0) then edt1.Text := '>' //注意这里并不像上面的那个例程里面使用了begin和end,但是同样的道理, //在if的下面的单条执行语句里面最后不能加分号(如果if下面有多条执行语句, //那么就要用begin和end括起来,这时候begin和end之间的每条语句都要用分号结束, //但是end后就不能用分号) else if(a<0) then edt1.Text := '<' //else if 的规则同 if 的规则 else edt1.Text := '='; //最后的else语句,如果else只有一条执行语句,那么这条执行语句必须以分号结尾。 //如果有多条执行语句,必须放到begin和end里面,这是不光每条执行语句需要以分号结尾, //end也要以分号结尾!
30. Pascal中的case语句就像是C/C++中的switch语句,下面的代码是Pascal的case语句
case SomeIntegerVariable of 101 : DoSomething; 202 : begin DoSomethinf; DoSomeThingElse end; 303 : DoSomething; else DoTheDefault; end;
注意,case语句的选择因子必须是有序类型,而不能是非有序的类型如字符串作为选择因子。上面类似于C中的代码:
switch(SomeIntegerVariable){ case 101 : DoSomething; break; case 202 : DoSomething; DoSomethingElse; break; case 303 : DoSomething; break; default : DoTheDefault; }
当变量等译值1和值2时,都执行语句1,那么可以这样写
case (表达式) of 值1,值2 : 语句1;
31. 警告:在Delphi1.0中,允许对控制变量赋值;而从Delphi2.0开始,不再允许对控制变量赋值,因为32位编译器对循环进行了优化。
32. 循环
for循环
循环体可以是简单语句,也可以是复合语句,若是复合语句,需要用begin...end括起来
在循环体中可以使用continue和break语句,他们也通常位于if语句之后
var I, X : Integer; begin X := 0; for I := 1 to 10 do //to是递增 inc(X, I); end;
var I, X : Integer; begin X := 0; for I := 10 downto 0 do //downto是递减 X := X + I; end;
while循环
Program FileIt; {$APPTYPE COMSOLE} var f : TextFile; //一个文本文件 S : string; begin AssignFile(f, ‘foo.txt’); Reset(f); while not EOF(f) do begin readln(f, S); writeln(S); end; CloseFile(f); end;
repeat…until循环(首先执行循环体,执行后在判断循环条件,所以循环体至少执行一次)
循环体可以是简单语句,也可以是复合语句,对于复合语句,不需要用begin...end括起来
注意:在“循环条件”为False的时候执行循环,为True的时候退出循环,这一点要和while语句进行区别
var x : Integer begin X := 1; repeat inc(x); until x>100; end;
33. 过程和函数。一个过程是一段程序代码,它在被调用时能执行某种特殊功能并能返回到调用它的地方。函数和过程类似,不同的是函数在返回到调用它的地方要返回一个值。Pascal中的过程相当于C/c++中的函数返回void,而Pascal中的函数相当于C/C++函数返回一个值。例子如下
Program FuncProc {$ApPTYE COMSOLE} procedure BiggerThanTen(i : Integer); {write something to the screen if I is greater than 10} begin if i >10 then writeln(‘Funky.’); end; function IsPositive(i : Integer): Boolean; {Return True if i is 0 or positive, False if i is negative} begin if i <0 then Result :=False; else Result := True; end; var Num : Integer; begin Num := 23; BiggerThanTen(Num); if IsPositive(Num) then writeln(Num, ‘is positive.’); else writeln(Num, ‘is negative’); end;
注意,在IsPositive()函数中的本地变量Result需要特别注意。每一个Object Pascal函数都有一个隐含的本地变量成为Result,它包含函数的返回值,注意这里和C/C++不一样,把一个值赋给Result,函数并不会结束。
34. 值参数。将参数以值的形式传递是默认的传递方式,一个参数以值的形式传递意味着创建这个变量的本地副本,过程很函数对副本进行运算,所以不会改变实参,看下面的例子:procedure Foo(s : string);
当用这种方式调用一个过程时,一个字符串的副本就被创建,Foo()将对副本s进行运算,这表示对这个副本的任何修改不会影响原来的变量。
35. 引用参数。Pascal允许通过引用把变量传递给函数和过程。通过引用传递的参数有时被称为是变量参数,通过引用传递意味着接受变量的函数和过程能够改变变量的值。为了通过引用传递变量,在过程或函数的参数表中用关键字var:
Procedure ChageMe(var x : longint); begin x := 3; {x在调用过程中变了} end;
不同于复制x,关键字var使得变量的地址呗复制,因此变量值就能被直接改变。
36. 常量参数。如果不想使传递给函数或过程的参数被改变,就用const关键字来声明它。关键字const不仅保护了变量的值不被修改,而且对于传递给函数或过程的字符串和记录来说能产生更优化的代码,下面的代码就是一个过程声明接受一个字符串常量参数: procedure Goon(const s : string);
37. 开放数组。开放数组参数能对过程和函数传递不确定数组,既可以传递相同类型的开放数组也可以传递不同类型的常量数组,下面的代码声明了一个函数,这个函数接受一个类型是Integer的开放数组参数:
var i, Rez : Integer; const j = 23; begin i := 8; Rez := AddEmup([I, 50, j, 89]);
为了在函数和过程中用开放数组,应该用High()、Low()和Sizeof()等函数来获取关于开放数组的信息。下面的代码是AddEmUp()函数的实现,它返回传递给参数A的所有元素的总和:
function AddEmUp(A: array of Integer) : Integer’ var i : Integer; begin Reault := 0; for i:= Low(A) to Hign(A) do inc(Result, A[i]); end;
Object Pascal还支持常量数组,这样就能把不同类型的数据放在一个数组中传递给函数和过程,下面就是它的语法:
procedure WhatHaveIGot(A: array of const);
可以使用下面的语法调用上述函数:
WhatHaveIGot([‘Tabasco’, 90, 5, 6, @WhatHaveIGot, 3.14159, True, ‘s’]);
38. 作用域是指一个过程、函数和变量能被编译器识别的范围,例如,一个全局常量的作用域是整个程序,而一些过程中的局部变量的作用域就是那些过程。
39. 单元(unit)。单元是组成Pascal程序的单独的源代码模块,单元有函数和过程组成,这些函数和过程能被主程序调用。一个单元至少要有以下三个部分组成:
*一个unit语句,每一个单元都必须在开头有这样一条语句,以标识单元的名称,单元的名称必须和文件名相匹配。例如,如果有一个文件名为FooBar,则unit语句可能是:
unit FooBar;
*interface部分。在unit语句后的源代码必须是interface语句。在这条语句和implementation语句之间是能被程序和其他单元所共享的信息。一个单元的interface部分是声明类型、常量、变量、过程和函数的地方,这些都能被主程序和其他单元调用。这里只能声明,而不能有过程体和函数体。Interface语句应当只有一个单词且在一行:
interface
*implementation部分。它在interface部分的后面。虽然单元的implementation包含过程和函数的源代码,但它同时也允许在此声明不被其他单元所调用的任何数据类型、常量和变量。Inplementation是定义在interface中声明的过程和函数的地方,inplementation语句只有一个单词并且在一行上:
implementation
一个单元能可选的包含其他两个部分:
*initialization部分。在单元中它放在文件结尾前,它包含了用来初始化单元的代码,它在主程序运行前运行并只运行一次。
*finalization部分。在单元中它放在initialization和end之间。finalization部分是在Delphi2.0引进的,在Delphi1.0中这部分用函数AddExitProc()增加一个推出过程来实现的,如果要把Delphi1.0程序移植过来,应该把退出过程中的代码移植到这部分来。
注意。如果几个单元都有initialization/finalization部分,则它们的执行顺序和单元在主程序的uses子句中的出现顺序一致。不要使initialization/finalization部分的代码依赖于它们的执行顺序,因为这样的话,主程序的uses子句只要有小小的修改,就会导致程序无法通过编译。
40. uses子句。Uses子句在一个程序或单元中用来列出想要包含近来的单元。例如,如果有一个程序名字为FooProg,它要用到在两个UnitA和UnitB中的函数和类型,正确的uses声明应该是:
Program FooProg; uses UnitA, UnitB; 单元能有两个uses子句,一个在interface部分,一个在implementation部分。例 Unit FooBar; interface use BarFoo; {在这里进行全局声明} implementation use BarFly; {在这里进行局部声明} initialization {在这里进行单元的初始化} finalization {在这里进行单元的退出操作} end
41. 循环单元引用。你经常会碰到这样的情况,在UnitA中调用UnitB,并在UnitB中调用UnitA,这就被称为循环单元引用。循环单元引用的出现表明了程序设计有缺陷,应该在程序中避免使用循环单元引用。比较好的解决方法是把UnitA和UnitB共有的代码转移到第三个单元中。然而,有时确实需要用到循环单元引用,这时就必须把一个uses子句转到implementation部分,而把两一个留在interface部分,这样就能解决问题。
42. 包。Delphi的包能把应用程序的部分代码放到一个单独的模块中,它能被多个应用程序所共享。如果你有用Delphi1.0和Delphi2.0开发的代码,利用包技术,再不修改任何代码的情况下就能把他们利用起来。
可以把包看作是若干个单元集中在一起类似于DLL的形式存储的模块(Borland Pack、age Library或BPL文件)。应用程序在运行的时候链接上这些包中的单元而不是在编译/链接时,因为这些单元的代码存在于BPL文件而不是存在于EXE或DLL中,所以EXE和DLL的长度将变得更短。
包的语法。包通常是用报编辑器创建的。要启动包编辑器,可以使用File | New | Package菜单项。包编辑器产生一个Delphi包源文件( DPK ),它将被编译进一个包。DPK的语法相当简单,其格式如下:
package PackageName requires Package1, Package2, …; //列在requires子句的包是这个包需要调用的其他包 contains Unit1, in ‘Unit1.pas’, Unit2, in ‘Unit2.pas’, …; //在contains下列出的单元是这个包所包含的单元,在contains子句下列出的单元将被编译进这个包,注意在这里列出的单元不能同时被列在requires子句中的包所包含。另外,有这些单元所引用的单元也会间接地包含到暴力,除非它们已经被列在requires子句中 end