深入Delphi (二)
单元文件/编译器条件标识符
by machine
对比起工程文件,单元文件明显多了一点东西:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
private
{ 私有声明 }
public
{ 公有声明 }
end;
var Form1: TForm1;
implementation
{$R *.DFM}
initialization
{ 在这里进行初始化 }
finalization
{ 在这里进行销解化 }
end.
首先是program换成了unit,然后还有interface和implementation两个关键字把整个单元分为了两部分,即接口和实现部分。
接口部分相当于C中的头文件部分,如果一个单元被uses了,那其实只是被包括了接口的部分。在这一部分中可以定义函数原型,但函数(包括类中的函数)的具体实现部分只能出现在implementation部分里面。与C类似,接口部分和实现部分都可以出现uses语句,以实现全局和局部的限制。接口部分的uses必须紧接着interface关键字出现,实现部分的uses也必须紧接着implementation关键字,就像它们是合在一起似的。
至于实现部分,其实如果把工程文件里面的program一行改成implementation,看起来就成了单元里面的实现部分,实际上也是如此,因为首先,工程文件不能被uses,因此工程总是局部的;另外,实现部分里面也可以出现工程中的begin...end.块。
实现部分的begin...end.块实际上就起到了初始化的作用,但这主要是保留了以前Pascal的语法,Delphi建议使用新增的initialization和finalization来初始化和销解化。在实现部分,begin...end.块、初始化块和销解化块都不是必须出现的,但整个单元必须以end.结尾。
关键字initialization标志着初始化块的开始,当一个编译后的工程运行时,每个单元的初始化块被首先执行,至于那个单元首先被初始化,则取决于被uses的顺序,先被uses的就先被初始化。初始化块和begin...end.块不能同时出现,因此,建议使用初始化块,以便也能使用销解化块的功能。
销解化块只能伴随初始化会出现,但初始化块可以单独出现。销解化块在程序结束前被自动执行,其执行顺序与该单元被uses的顺序相反。要注意的是,初始化块可能在完全被执行之前程序就因某种错误而被中止了,但Delphi确保证销解化块被执行,因此销解块中的代码必须考虑未完全初始化的情况。
然后是对uses语句的详细说明。总结前面所说,uses总共可以在三处地方出现。在单元的两处地方的uses语句里,被uses的单元必须是在工程的搜索路径中,而工程文件中的uses语句则可以直接指明文件的路径:
uses
Forms,
Main,
Extra in '..\EXTRA\EXTRA.PAS';
在单元文件中,如果在接口部分引用了另外一个单元,则实现部分也可以调用该单元,但反之在实现部分引用的单元则不能在接口部分被调用。在编程的时候应该尽量在一个单元的实现部分引用其他单元,而减少在接口部分的引用,以避免所谓的“循环引用”问题。比如说,单元A在接口部分引用了单元B,单元B以同样的方法引用了单元C,最后单元C又在接口部分引用了单元A,则编译时会出现循环引用的错误。解决方法是把全局的声明放到集中的单元中,尽量把引用放到实现部分。
有时候会遇到一个问题,如果一个变量,或者常量、函数,又或者是数据类型,总之,一样相同名称的东西同时在两个被引用的单元中,那么调用的时候到底会调用到哪一个呢?答案是uses语句的单元列表中最后一个,而不是首先出现的那个!这是要注意的。如果不能确定将会调用那个版本,还可以在所调用的东西前面加上“单元名称+句号”的前缀,以强制Delphi调用相应的单元版本。
在单元和工程中还有一类用于控制编译器的特殊命令(Compiler Directive)。这种命令包含在大括号或者“(*”和“*)”的组合中,看起来就像注释,但用“$”这个符号开头。比如例子中的$R就是告诉编译器你要包含一些资源文件到工程中。其中有一组命令,$DEFINE、$UNDEF、$IFDEF、$IFNDEF、$ELSE和$ENDIF,看起来很像C中的宏定义命令,但Delphi中的这种定义只起到开关的作用,而不能定义宏替换,也就是说可以定义一个开关名称,使其处于开状态,而如果没有定义这个开关,则认为其处于关状态,但不能像C中那样定义这个开关为一段程序代码,并在编译是自动用代码替换宏,Delphi没有提供这样的宏功能。
要是对编译器指令不了解,那么请继续看下去,否则就可以直接跳到下一章了。好,首先,什么叫做编译呢?编译就是把你编写的源代码,转译为CPU能执行的指令。通常情况下,编译器把每个单元文件单独编译,然后连接器负责把编译后的单元文件连接为可执行文件(exe文件)。之所以要这样做,是因为不同的操作系统,其可执行文件的格式是不一样的,比如DOS、Windows、Linux这些操作系统。但只要CPU是兼容的型号,则可以使用同样的编译文件格式,因为转译过程是一样的。比如C的编译文件OBJ文件,即使是在DOS下编译的,仍然可以被连接为Windows的EXE,编译器只对CPU感兴趣,而对要输出的执行文件的格式不感兴趣。
我个人觉得对要学一样东西如果对其了解得越多,学起来就更能融会贯通,所以有时候可能会扯得很远。还有,要学好一样东西,耐心是必不可少的哦!所以一时弄不明白不要泄气,说不定过一段时间之后就突然明白了呢,我以前也试过这样子的。好了,回到刚才Delphi的DEFINE问题。初学者很容易搞不明白这样的定义和常量/变量声明的作用有什么不同:
{$DEFINE Debug}
begin
{$IFDEF Debug}
Writeln('Debug is on');
{$ENDIF}
end;
然后对比一下这个例子:
const Debug = True;
begin
if Debug then begin
Writeln('Debug is on');
end;
end;
两个例子运行起来结果是一模一样的。但事实上编译出来的程序是不一样的。在第二个例子中,首先,程序被编译,然后运行,CPU首先判断Debug的值是否不为零,如果是,则继续执行调用Writeln,没什么特别之处。但第一个例子的情况就不一样了,在编译时,编译器发现了$DEFINE Debug这条指令,于是它在内部设立标识,记录了Debug这个定义(DEFINE),然后继续编译过程,之后,编译器又发现了$IFDEF Debug这条指令,编译器查看自己的内部标识表,发现Debug是已经定义的,因此它继续进行$IFDEF和$ENDIF之间的源代码的编译,否则的话,它将忽略这段源代码,就好像他们根本不存在的样子!所以,第一段例子编译出来的程序与以下例子的是一模一样的:
begin
Writeln('Debug is on');
end;
这样,源代码在编译的时候编译器就已经作了判断,编译出相应的程序,而不是程序在运行的时候,CPU再做出判断,调用相应的代码。这样一来,程序的运行效率明显提高了,C就是大量使用这种特性的例子,因此C的执行效率比较高,同理,C的编译速度却是非常的慢,因为编译器要维护非常大的标识列表(别忘了C的宏定义比Delphi中的复杂,而且C中到处都是这样的宏定义),而且要不断地搜索列表并根据对应定义来修改程序源代码,最后才能进行CPU指令翻译。以后会讲到如何在Delphi中使用其它语言编写的代码,包括汇编和C等,这样一来,程序的事件处理部分可以用Delphi来完成,而在需求速度的地方可以使用汇编或者C来写,开发速度就能够大大提高,而程序运行效率仍然可以维持在高水平上。
使用DEFINE指令还能做到其他的一些事情,比如向上面的例子那样,定义一个调试(Debug)的编译器条件标识符(Conditional Symbol),然后在代码中插入一些只想在调试时才想执行的代码,比如记录程序每调用一个函数的返回值之类的,用$IFDEF来控制,是非常方便的,如果等到程序要发布,不想要再编译那些调试用的代码,只需注释掉Debug的定义就可以了,而不需要把每个调试用的代码都注释掉。在某些情况下,无法使用Delphi的调试器,比如正在使用Direct Draw,或者调试一个同时被多个程序使用的DLL那样,就只能把要调试的内容记录一个文件中,用查看文件记录的方式来调试,这样子本来就很烦了,如果要维护这样的程序,最好使用DEFINE,否则来回改动程序只会使自己更烦。
$DEFINE的作用还可以继续引申开去,凡是程序有多个代码版本的情况,都可以使用$DEFINE的方便功能。好了,这一章就说到这里吧。请继续留意下一章的出现。