DELPHI基础教程 第十二章 异常处理与程序调试

第十二章 异常处理与程序调试

  在应用程序开发中如何检测、处理程序的运行错误是一个很重要的问题。在 Delphi 的集成开发环境( IDE )中提供了一个完善的内置调试器,可以帮助你发现大部分程序错误。但并不是所有的错误都可以被发现,而且当程序涉及到与外设的数据交换或操作外设,如要求用户输入、读写磁盘等时,错误的发生是程序无法控制的,如输入非法字符、磁盘不能读写等。这些情况不仅会导致应用程序异常中止而且可能引起系统的崩溃。针对这些问题,Delphi同时提供了一套强大的异常处理机制。巧妙地利用它,可以使你的程序更为强健,使用更为友好。

  虽然Delphi为应用程序提供了一套缺省的自动异常处理机制,即当前模块发生错误后退出当前模块并给出错误信息,而并不立即引起应用程序的中止。但当应用程序执行的过程性很强时,仅仅利用这种方法是不够的,而且很容易导致程序执行的不可预测性。 

12.1 Delphi异常处理机制与异常类 

  Delphi异常处理机制建立在保护块(Protected Blocks)的概念上。所谓保护块是用保留字try和end封装的一段代码。保护块的作用是当应用程序发生错误时自动创建一个相应的异常类(Exception)。程序可以捕获并处理这个异常类,以确保程序的正常结束以及资源的释放和数据不受破坏。如果程序不进行处理,则系统会自动提供一个消息框。

  异常类是Delphi异常处理机制的核心,也是Delphi异常处理的主要特色。下面我们对异常类的概念和体系进行详细的介绍。

  Delphi提供的所有异常类都是类Exception的子类。用户也可以从Exception派生一个自定义的异常类。

  Exception类的定义如下,对于不常用的成员没有列出。  

{SysUtils 单元中}

Exception = class(TObject)

private

FMessage: PString;

FHelpContext: Longint;

function GetMessage: String;

procedure SetMessage(const Value: String);

public

constructor Create(const Msg: String);

constructor CreateFmt(const Msg: String; const Args: array of const);. . .

destructor Destroy; override;

property HelpContext: Longint

property Message: String;

property MessagePtr: PString;

end; 

Exception的一系列构造函数中最重要的参数是显示的错误信息。而数据成员中最重要的也是可被引用的消息字符串(message,messagePtr)。 这些信息分别对自定义一个异常类和处理一个异常类有重要作用。

  Delphi提供了一个很庞大的异常类体系,这些异常类几乎涉及到编程的各个方面。从大的方面我们可以把异常类分为运行时间库异常、对象异常、部件异常三类。下面我们分别进行介绍。 

12.1.1 运行时间库异常类(RTL Exception) 

  运行时间库异常可以分为七类,它们都定义在SysUtils库单元中。 

12.1.1.1 I/O异常 

  I/O异常类EInOutError是在程序运行中试图对文件或外设进行操作失败后产生的,它从Exception派生后增加了一个公有数据成员ErrorCode,用于保存所发生错误的代码。这一成员可用于在发生I/O异常后针对不同情况采取不同的对策。

  当设置编译指示{$I- } 时,不产生I/O异常类而是把错误代码返回到预定义变量IOResult中。 

12.1.1.2 堆异常 

  堆异常是在动态内存分配中产生的,包括两个类EOutOfMemory和EInvalidPointer。

表12.1  堆异常类及其产生原因

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

异常类 引发原因

─────────────────────────────────

EOutOfMemory 没有足够的空间用于满足所要求的内存分配

EInvalidPointer 非法指针。一般是由于程序试图去释放一个业已释放的指针而引起的

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

12.1.1.3  整数异常 

  整数异常都是从一个EIntError类派生的,但程序运行中引发的总是它的子类:EDivByZero,ERangeError,EIntOverFlow。 

   表12.2  整数异常及其产生原因

━━━━━━━━━━━━━━━━━━━━━

异常类 引发原因

─────────────────────

EDivByZero 试图被零除

ERangeError 整数表达式越界

EIntOverFlow 整数操作溢出

━━━━━━━━━━━━━━━━━━━━━━ 

  ERangeError当一个整数表达式的值超过为一个特定整数类型分配的范围时引发。比如下面一段代码将引发一个ERangeError异常。 

var

SmallNumber: ShortInt;

X , Y: Integer;

begin

X := 100;

Y := 75;

SmallNumber := X * Y;

end;

  特定整数类型包括ShortInt、Byte以及与整数兼容的枚举类型、布尔类型等。例如:  

type

THazard = ( Safety , Marginal , Critical , Catastrophic );

var

Haz: THazard;

Item: Integer;

begin

Item:= 4;

Haz:= THazard ( Item );

end; 

由于枚举数越界而引发一个ERangeError异常。

  数组元素越界也会引发一个ERangeError异常,如: 

var

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

i: Integer;

begin

for i := 1 to 11 do

Values[i] := i;

end;

ERangeError异常只有当类型检查打开时才会引发。这可以在代码中包含{$R+} 编译指示或设置IDE Option|Project的Range_Checking Option选择框。

  EIntOverFlow异常类在Integer、Word、Longint三种整数类型越界时引发。如:

var

I : Integer;

a,b,c : Word;

begin

a := 10;

b := 20;

c := 1;

for I := 0 to 100 do

begin

c := a*b*c;

end;

end;

引发一个EIntOverFlow异常。

EIntOverFlow异常类只有在编译选择框Option|Project|Over_Flow_Check Option选中时才产生。当关闭溢出检查,则溢出后变量保留该类整数的最大范围值。

整数类型的范围如下表。 

   表12.3 整数类型的范围

━━━━━━━━━━━━━━━━━━━━━━━━━━━

类型 范围 格式

  ───────────────────────────

Shortint -128 .. 127 有符号8位

Integer -32768 .. 32767 有符号16位

Longint -2147483648 .. 2147483647 有符号32位

Byte 0 .. 255 无符号8位

Word 0 .. 65535 无符号16位

━━━━━━━━━━━━━━━━━━━━━━━━━━━  

12.1.1.4 浮点异常 

  浮点异常是在进行实数操作时产生的,它们都从一个EMathError类派生,但与整数异常相同,程序运行中引发的总是它的子类EInvalidOp、EZeroDivide、EOverFlow、EUnderFlow。 

   表12.4 浮点异常类及其引发原因

━━━━━━━━━━━━━━━━━━━━━━━━

异常类 引发原因

────────────────────────

EInvalidOp 处理器碰到一个未定义的指令

EZeroDivide 试图被零除

EOverFlow 浮点上溢

EUnderFlow 浮点下溢

━━━━━━━━━━━━━━━━━━━━━━━━ 

  EInvalidOp最常见的引发原因是没有协处理器的机器遇到一个协处理器指令。由于在缺省情况下Delphi总是把浮点运算编译为协处理器指令,因而在386以下微机上常常会碰到这个错误。此时只需要在单元的接口部分设置全局编译指示{$N-},选择利用运行时间库进行浮点运算,问题就可以解决了。  

  各种类型的浮点数(Real、Single、Double、Extended)越界引起同样的溢出异常。这同整数异常类是不同的。 

12.1.1.5 类型匹配异常

  类型匹配异常EInvalidCast当试图用As 操作符把一个对象与另一类对象匹配失败后引发。 

12.1.1.6 类型转换异常

  类型转换异常EConvertError当试图用转换函数把数据从一种形式转换为另一种形式时引发,特别是当把一个字符串转换为数值时引发。下面程序中的两条执行语句都将引发一个EConvertError异常。

var

rl : Real;

int: Integer;

begin

rl := StrToFloat(\' $140.48\');

int := StrToInt(\' 1,402 \');

end; 

要注意并不是所有的类型转换函数都会引发EConvertError异常。比如函数Val当它无法完成字符串到数值的转换时只把错误代码返回。利用这一点我们在(6.2)节中实现了输入的类型和范围检查。 

12.1.1.7 硬件异常

  硬件异常发生的情况有两种:或者是处理器检测到一个它不能处理的错误,或者是程序产生一个中断试图中止程序的执行。硬件异常不能编译进动态链接库(DLLs)中,而只能在标准的应用中使用。

  硬件异常都是EProcessor异常类的子类。但运行时间并不会引发一个EProcessor 异常。 

   表12.5  硬件异常类及其产生原因

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

异常类 引发原因

─────────────────────────────────

Efault 基本异常类。是其它异常类的父类

EGPFault 一般保护错。通常由一个未 初始化的指针或对象引起

EStackFault 非法访问处理器的栈段

EPageFault Windows内存管理器不能正确使用交换文件

EInvalidOpCode 处理器碰到一个未定义的指令。这通常意味着处理器

试图去操作非法数据或未初始化的内存

EBreakPoint 应用程序产生一个断点中断

ESingleStep 应用程序产生一个单步中断

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

  EFault、EGPFault 往往意味着致命的错误。而EBreakPoint、ESingleStep被Delphi IDE的内置调试器处理。事实上前边的五种硬件异常的响应和处理对开发者来说都是十分棘手的问题。 

12.1.2 对象异常类 

  所谓对象异常是指非部件的对象引发的异常。Delphi定义的对象异常包括流异常、打印异常、图形异常、字符串链表异常等。 

12.1.2.1 流异常类 

  流异常类包括EStreamError、EFCreateError、 EFOpenError、EFilerError、EReadError、EWriteError、EClassNotFound。它们的结构关系如下: 

EStreamError

|---------- EFCreateError

|---------- EFOpenError

|---------- EFilerError

|--------- EReadError

|--------- EWriteError

|--------- EClassNotFound

    图12.1 流异常结构图 

流异常在Classes库单元中定义。

  流异常引发的原因如表12.6。

表12.6  流异常类及其产生原因

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

异常类 引发原因

─────────────────────────────────

EStreamError 利用LoadFromStream方法读一个流发生错误

EFCreateError 创建文件时发生错误

EFOpenError 打开文件时发生错误

EFilerError 试图再次登录一个存在的对象

EReadError ReadBuffer方法不能读取特定数目的字节

EWriteError WriteBuffer方法不能写特定数目的字节

EClassNotFound 窗口上的部件被从窗口的类型定义中删除

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

12.1.2.2 打印异常类 

  打印异常类EPrinter当打印发生错误时引发。它在printers库单元中定义。例如你的应用程序试图向一个不存在的打印机打印或由于某种原因打印工作无法送到打印机时,就会产生一个打印异常。 

12.1.2.3 图形异常类 

  图形异常类定义在Graphic 库单元中,包括EInvalidGraphic和EInvalidGraphicOperation两类。

  EInvalidGraphic当应用程序试图从一个并不包含合法的位图、图标、元文件或用户自定义图形类型的文件中装入图形时引发。例如下面的代码: 

  Image1.Picture.LoadFromFile(\'Readme.txt\'); 

  由于Readme.txt并不包含一个合法的图形,因而将引发一个EInvalidGraphic异常。

  EInvalidGraphicOperation当试图对一个图形进行非法操作时引发。例如试图改变一个图标的大小。 

var

AnIcon: TIcon;

begin

AnIcon := TIcon.Create;

AnIcon.LoadFromFile(\'C:\WINDOWS\DIRECTRY.ICO\');

AnIcon.Width := 100; { 引发一个图形异常 }

...

12.1.2.4 字符串链表异常 

  字符串链表异常EStringListError、EListError在用户对字符串链表进行非法操作时引发。由于许多部件(如TListBox,TMemo,TTabSet,…)都有一个TStrings类的重要属性,因而字符串链表异常在部件操作编程中非常有用。

  EStringListError异常一般在字符串链表越界时产生。例如对如下初始化的列表框:  

ListBox1.Items.Add(\'First item\');

ListBox1.Items.Add(\'Second item\');

ListBox1.Items.Add(\'Third item\');

  则以下操作都会引起EStringListError异常: 

ListBox1.Item[3] := \' Not Exist\';

str := ListBox1.Item [3];

  EListError异常一般在如下两种情况下引发:

  1.当字符串链表的Duplicates属性设置为dupError时,应用程序试图加入一个重复的字符串;

  2.试图往一个排序的字符串链表中插入一个字符串。 

12.1.3 部件异常类 

12.1.3.1 通用部件异常类 

  通用部件异常类常用的有三个:EInvalidOperation、EComponentError、EOutOfResource。其中EInvalidOperation、EOutOfResource在Controls单元中定义;EComponentError在Classes单元中定义。

  1.非法操作异常 EInvalidOperation

  EInvalidOperation 引发的原因可能有:

  ● 应用程序试图对一个Parent属性为nil的部件进行一些需要Windows句柄的操作

  ● 试图对一个窗口进行拖放操作

  ● 操作违反了部件属性间内置的相互关系等 

  例如,ScrollBar、Gauge等部件要求Max属性大于等于Min属性,因而下面的语句: 

  ScrollBar1.Max := ScrollBar1.Min-1;

  将引发一个EInvalidOperation异常。 

  2.部件异常EComponentError

引发该异常的原因可能有:

  ● 在Register过程之外试图登录一个部件(常用于自定义部件开发中)

  ● 应用程序在运行中改变了一个部件的名称并使该部件与另一个部件重名

  ● 一个部件的名称改变为一个Object Pascal非法的标识符

  ● 动态生成一个部件与已存在的另一部件重名 

3.资源耗尽异常EOutOfResource

当应用程序试图创建一个Windows句柄而Windows 却没有多余的句柄分配时引发该异常。 

12.1.3.2 专用部件异常类 

  许多部件都定义了相应的部件异常类。但并不是有关部件的任何错误都会引发相应的异常类。许多情况下它们将引发一个运行时间异常或对象异常。

  下面列出几个典型的部件异常类。

  1.EMenuError

非法的菜单操作,例如试图删除一个不存在的菜单项。这一异常类在Menus库单元中定义。

  2.EInvalidGridOpertion

  非法的网格操作,比如试图引用一个不存在的网格单元。这一异常类在Grids库单元中定义。

  3.EDDEError

  DDE异常。比如应用程序找不到特定的服务器或会话,或者一个联接意外中止。这一异常类在DDEMan库单元中定义。

  4.EDatabaseError,EReportError

  数据库异常(EDatabaseError)和报表异常(EReportError) 在进行数据库和报表操作出现错误时引发。有关数据库的问题请读者参阅本书第二编。 

12.1.4 小结 

  在这一节中重点介绍了Delphi提供的异常类体系。我们力求给读者一个清晰、全面的印象,使读者能在自己的程序开发中实际使用它们。为便于理解我们也提供了一些简单的说明性示例。虽然在具体的使用中读者还可能会碰到许多问题,但意识到应该用异常类来增强程序的健壮性却是程序设计水平走上新台阶的标志。 

12.2 异常保护 

  确保回收分配的资源是程序健壮性的一个关键。但缺省情况下异常发生时程序会在出错点自动退出当前模块,因此需要一种特殊的机制来确保即使在异常发生的情况下释放资源的语句仍能被执行。而Delphi的异常处理正提供了这种机制。 

12.2.1 需要保护的资源 

  一般说来需要保护的资源包括:

  ● 文件

  ● 内存

  ● Windows资源

  ● 对象 

  比如下面一段程序就会造成1K内存资源的丢失。 

var

APointer : Pointer ;

AInt , ADiv: Integer ;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

AInt := 10 div ADiv ;

FreeMem ( Apointer , 1024 );

end; 

由于程序从异常发生点退出从而FreeMem永远没有执行的机会。 

12.2.2 产生一个资源保护块 

  Delphi提供了一个保留字finally,用于实现资源的保护: 

  {分配资源}

  try

{资源使用情况}

finally

{释放资源}

  end; 

try…finally…end就形成了一个资源保护块。finally后面的语句是在任何情况下,不论程序是否发生异常,都会执行的。

  对于(12.2.1)中的例子如下代码即可确保所分配内存资源的释放: 

var

APointer : Pointer ;

AInt , ADiv : Integer;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

try

AInt := 10 div ADiv ;

finally

FreeMem ( Apointer , 1024 );

end;

end; 

下面的例子摘自(6.4)节,是在文件拷贝中实现文件资源的保护: 

procedure CopyFile(const FileName, DestName: TFileName);

var

CopyBuffer: Pointer;

TimeStamp, BytesCopied: Longint;

Source, Dest: Integer;

Destination: TFileName;

const

ChunkSize: Longint = 8192;

begin

Destination := ExpandFileName(DestName);

if HasAttr(Destination, faDirectory) then

Destination := Destination + \'\\' + ExtractFileName(FileName);

TimeStamp := FileAge(FileName);

GetMem(CopyBuffer, ChunkSize);

try

Source := FileOpen(FileName, fmShareDenyWrite);

if Source < 0 then

raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));

try

Dest := FileCreate(Destination);

if Dest < 0 then

raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));

try

repeat

BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize);

if BytesCopied > 0 then

FileWrite(Dest, CopyBuffer^, BytesCopied);

until BytesCopied < ChunkSize;

finally

FileClose(Dest);

end;

finally

FileClose(Source);

end;

finally

FreeMem(CopyBuffer, ChunkSize);

end;

end;

程序的具体解释见 (6.4)节。

  在异常保护的情况下,当异常发生时,系统会自动弹出一个消息框用于显示异常的消息。退出当前模块后异常类自动清除。

 

12.3 异常响应 

  异常响应为开发者提供了一个按自己的需要进行异常处理的机制。try …except …end形成了一个异常响应保护块。与finally不同的是:正常情况下except 后面的语句并不被执行,而当异常发生时程序自动跳到except,进入异常响应处理模块。当异常被响应后异常类自动清除。

  下面的例子表示了文件打开、删除过程中发生异常时的处理情况: 

uses Dialogs;

var

F: Textfile;

begin

OpenDialog1.Title := \'Delete File\';

if OpenDialog1.Execute then

begin

AssignFile(F, OpenDialog1.FileName);

try

Reset(F);

if MessageDlg(\'Erase \' +OpenDialog1.FileName + \'?\',

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

begin

System.CloseFile(F);

Erase(F);

end;

except

on EInOutError do

MessageDlg(\'File I/O error.\', mtError, [mbOk], 0);

on EAccessDenied do

MessageDlg(\'File access denied.\', mtError, [mbOk], 0);

end;

end;

end.

  保留字on…do用于判断异常类型。必须注意的是:except后面的语句必须包含在某一个on…do模块中,而不能单独存在。这又是同finally不同的一个地方。 

12.3.1 使用异常实例 

  上面所使用的异常响应方法可总结为如下的形式: 

  on ExceptionType do

{响应某一类的异常} 

  这种方法唯一使用的信息是异常的类型。一般情况下这已能满足我们的需要。但我们却无法获取异常实例中包含的信息,比如异常消息、错误代码等。假设我们需要对它们进行处理,那么就必须使用异常实例。

  为了使用异常实例,需要为特定响应模块提供一个临时变量来保存它: 

  on EInstance : ExceptionType do  … 

  在当前响应模块中我们可以象使用一个普通对象那样来引用它的数据成员。但在当前响应模块之外不被承认。

  下面的代码用于获取异常消息并按自己的方式显示它: 

{窗口中包括一个ScrollBar部件,一个Button部件} 

procedure TErrorForm.Button1Click(Sender: TObject);

begin

try

ScrollBar1.Max := ScrollBar1.Min-1;

except

on E: EInvalidOperation do

MessageDlg(\'Ignoring Exception:\'+E.Message,

mtInformation,[mbOK],0);

end;

end; 

12.3.2 提供缺省响应 

  在异常响应模块中,一般我们只对希望响应的特定异常进行处理。如果一个异常发生而响应模块并没有包含对它的处理代码,则退出当前响应模块,异常类仍被保留。

  为了保证任何异常发生后都能在当前响应模块中被清除,可以定义缺省响应: 

try

{程序正常功能}

except

on ESomething do

{响应特定异常}

else

{提供缺省响应}

end; 

由于else可以响应任何异常,包括我们一无所知的异常,因此在缺省响应中最好只包括诸如显示一个消息框之类的处理,而不要改变程序的运行状态或数据。 

12.3.3 响应一族异常 

  诸如

    on ExceptionType do

的异常响应语句不仅可响应本类异常,而且可以响应子类异常。对于象EIntError、EMathError等系统不会引发的异常,它们将只响应其子类异常。而对于象

   on Exception do

这样的语句将会对任何异常进行响应。

  下面一段代码对整数越界异常进行单独处理,而对其它整数异常进行统一处理: 

  try

{整数运算}

except

on ERangeError do

{越界处理}

on EIntError do

{其它整数异常处理}

end; 

由于异常在处理后即被清除,因而上面的代码可保证不会使ERangeError异常被多次处理。假如颠倒两条响应语句的顺序,则ERangeError异常响应将永远没有被执行的机会。 

  由于异常在处理后即被清除,因而当希望对异常进行多次处理时就需要使用保留字raise来重引发一个当前异常。

  下面的代码同时使用了异常响应和异常保护。异常响应用于设置变量的值,异常保护用于释放资源。当异常响应结束时利用raise重引发一个当前异常。 

var

APointer: Pointer ;

AInt , ADiv: Integer;

begin

ADiv := 0;

GetMem ( APointer , 1024 );

try

try

AInt := 10 div ADiv ;

except

on EDivByZero do

begin

AInt := 0 ;

raise;

end;

end;

finally

FreeMem ( APointer , 1024 );

end;

end;

  上面一段代码体现了异常处理的嵌套。异常保护、异常响应可以单独嵌套也可以如上例所示的那样相互嵌套。 

12.3.5 自定义异常类的应用 

  利用Delphi的异常类机制我们可以定义自己的异常类来处理程序执行中的异常情况。同标准异常不同的是:这种异常情况并不是相对于系统的正常运行,而是应用程序的预设定状态。比如输入一个非法的口令、输入数据值超出设定范围、计算结果偏离预计值等等。

  使用自定义异常需要:

  1.自己定义一个异常对象类;

  2.自己引发一个异常。 

12.3.5.1 定义异常对象类 

  异常是对象,所以定义一类新的异常同定义一个新的对象类型并无太大区别。由于缺省异常处理只处理从Exception或Exception子类继承的对象,因而自定义异常类应该作为Exception或其它标准异常类的子类。这样,假如在一个模块中引发了一个新定义的异常,而这个模块并没有包含对应的异常响应,则缺省异常处理机制将响应该异常,显示一个包含异常类名称和错误信息的消息框。

  下面是一个异常类的定义: 

  type

EMyException = Class(Exception) ; 

12.3.5.2 自引发异常 

  引发一个异常,调用保留字raise,后边跟一个异常类的实例。

  假如定义: 

type

EPasswordInvalid = Class(Exception); 

则在程序中如下的语句将引发一个EPasswordInvalid异常: 

 If Password <> CorrectPassword then

raise EPasswordInvalid.Create(\'Incorrect Password entered\');

  异常产生时把System库单元中定义的变量ErrorAddr的值置为应用程序产生异常处的地址。在你的异常处理过程中可以引用ErrorAddr的值。

  在自己引发一个异常时,同样可以为ErrorAddr分配一个值。

  为异常分配一个错误地址需要使用保留字at,使用格式如下: 

  raise EInstance at Address_Expession; 

12.3.5.3 自定义异常的应用举例  

下面我们给出一个利用自定义异常编程的完整实例。

两个标签框(Label1、Label2)标示对应编辑框的功能。编辑框PassWord和InputEdit用于输入口令和数字。程序启动时Label2、InputEdit不可见。当在PassWord中输入正确的口令时,Label2、InputBox出现在屏幕上。此时Label1、PassWord隐藏。

设计时,令Label2、InputEdit的Visible属性为False。通过设置PassWord的PassWordChar可以确定输入口令时回显在屏幕上的字符。

自定义异常EInvalidPassWord和EInvalidInput分别用于表示输入的口令非法和数字非法。它们都是自定义异常EInValidation的子类。而EInValidation直接从Exception异常类派生。

下面是三个异常类的定义。 

type

EInValidation = class(Exception)

public

ErrorCode: Integer;

constructor Create(Const Msg: String;ErrorNum: Integer);

end;

EInvalidPassWord = class(EInValidation)

public

constructor Create;

end;

EInvalidInput = class(EInValidation)

public

constructor Create(ErrorNum: Integer);

end; 

EInValidation增加了一个公有成员ErrorCode来保存错误代码。错误代码的增加提供了很大的编程灵活性。对于异常类,可以根据错误代码提供不同的错误信息;对于使用者可以通过截取错误代码,在try...except模块之外来处理异常。

从以上定义可以发现:EInvalidPassWord和EInvalidInput的构造函数参数表中没有表示错误信息的参数。事实上,它们保存在构造函数内部。下面是三个自定义异常类构造函数的实现代码。 

constructor EInValidation.Create(Const Msg: String; ErrorNum: Integer);

begin

inherited Create(Msg);

ErrorCode := ErrorNum;

end;

constructor EInValidPassWord.Create;

begin

inherited Create(\'Invalid Password Entered\',0);

end;

constructor EInValidInput.Create(ErrorNum: Integer);

var

Msg: String;

begin

case ErrorNum of

1:

Msg := \'Can not convert String to Number\';

2:

Msg := \'Number is out of Range\';

else

Msg := \'Input is Invalid\';

end;

inherited Create(Msg,ErrorNum);

end; 

对于EInvalidInput,ErrorCode=1表示输入的不是纯数字序列,而ErrorCode=2表示输入数值越界。

口令检查是用户在PassWord中输入口令并按下回车键后开始的。实现代码在PassWord的OnKeyPress事件处理过程中: 

procedure TForm1.PassWordKeyPress(Sender: TObject; var Key: Char);

const

CurrentPassWord = \'Delphi\';

begin

if Key = #13 then

begin

try

if PassWord.text <> CurrentPassWord then

raise EInvalidPassWord.Create;

Label2.Visible := True;

InputEdit.Visible := True;

InputEdit.SetFocus;

PassWord.Visible := False;

Label1.Visible := False;

except

on EInvalidPassWord do

begin

PassWord.text := \'\';

raise;

end;

end;

Key:=#0;

end;

end; 

同样,在InputEdit的OnKryPress事件处理过程中实现了输入数字的合法性检查: 

procedure TForm1.InputEditKeyPress(Sender: TObject; var Key: Char);

var

Res: Real;

Code: Integer;

begin

if Key = #13 then

begin

try

val(InputEdit.text,Res,Code);

if Code <> 0 then

raise EInValidInput.create(1);

if (Res > 1) or (Res < 0) then

raise EInValidInput.create(2);

MessageDlg(\'Correct Input\', mtInformation,[mbOk], 0);

Key := #0;

except

on E:EInValidInput do

begin

InputEdit.text := \'\';

MessageDlg(E.Message, mtWarning,[mbOk], 0);

end;

end;

end;

end; 

由于异常响应后即被清除,所以要显示异常信息,需要另外的手段。在以上两段程序中我们采用了两种不同的方法:在口令合法性检查中,利用异常重引发由系统进行缺省响应;在输入数字合法性检查中,通过异常实例来获取异常信息并由自己来显示它。

以上所举的是一个非常简单的例子,但从中已可以发现:使用自定义异常编程,为程序设计带来了很大的灵活性。 

12.3.6 利用异常响应编程 

  利用异常处理机制不仅能使程序更加健壮,而且也提供了一种使程序更加简捷、明了的途径。事实上,使用自定义异常类就是一种利用异常响应编程的方式。这里我们再讨论几个利用标准异常类编程的例子。

  比如为了防止零作除数,可以在进行除法运算前使用if…then…else语句。但如果有一系列这样的语句则繁琐程度是令人难以忍受的。这时候我们可能倾向于使用EDivByZero异常。例如如下一段程序就远比用if…then…else实现简捷明了。 

function Calcu(x,y,z,a,b,c:Integer):Real;

begin

try

Result := x/a+y/b+z/c ;

except

on EDivByZero do

Result := 0;

end;

end;

在(6.2.3)记录文件的打开与创建中就是利用异常响应来实现文件的打开或创建。 

procedure TRecFileForm.OpenButtonClick(Sender: TObject);

begin

if OpenDialog1.Execute then

FileName := OpenDialog1.FileName

else

exit;

AssignFile(MethodFile,Filename);

try

Reset(MethodFile);

FileOpened := True;

except

on EInOutError do

begin

try

if FileExists(FileName) = False then

begin

ReWrite(MethodFile);

FileOpened := True;

end

else

begin

FileOpened := False;

MessageDlg(\'文件不能打开\',mtWarning,[mbOK],0);

end;

except

on EInOutError do

begin

FileOpened := False;

MessageDlg(\'文件不能创建\',mtWarning,[mbOK],0);

end;

end;

end;

end;

if FileOpened = False then exit;

Count := FileSize(MethodFile);

if Count > 0 then

ChangeGrid;

RecFileForm.Caption := FormCaption+\' -- \'+FileName;

NewButton.Enabled := False;

OpenButton.Enabled := False;

CloseButton.Enabled := True;

end; 

总之,利用异常响应编程的中心思想是虽然存在预防异常发生的确定方法,但却对异常的产生并不进行事前预防,而是进行事后处理,并以此来简化程序的逻辑结构。 

12.4 程序调试简介 

  Delphi提供了一个功能强大的内置调试器(Integrated Debugger), 因而对程序的调试不用离开集成开发环境(IDE)就可以进行。

  程序错误基本可以分为两类,即运行时间错和逻辑错。所谓运行时间错是指程序能正常编译但在运行时出错。逻辑错是指程序设计和实现上的错误。程序语句是合法的,并顺利执行了,但执行结果却不是所希望的。

  对于这两类错误,调试器都可以帮助你快速定位错误,并通过对程序运行的跟踪和对变量值的监视帮助你寻找错误的真正原因和解决错误的途径。

  程序调试的主要内容可以概括为如下的几方面:

  1.调试的准备和开始;

  2.控制程序的执行;

  3.断点的使用;

  4.检查数据的值。

  程序调试只有用户实际上机操作才能真正掌握。在这一节中我们主要对调试中的主要问题和一些关键点进行介绍。至于一些很细小的问题相信读者可以在上机实际应用中掌握,因而没有列出。

 

12.4.1 调试的准备和开始 

  在程序开发过程中程序编码和调试是一个持续的循环过程,只有在你对程序进行了彻底的测试后才能交付最终用户使用。为了保证调试的彻底性,在调试前应制定一个详细的调试计划。一般说来应该把程序划分为几个相对独立的部分,分别进行调试,以利于错误的迅速定位,确保每一部分程序都按设计的要求运行。

  调试计划准备好后就可以开始程序的调试。

  开始一个调试过程包括:

  1.编译时产生调试信息;

  2.从Delphi里运行你的程序。

  在程序调试过程中,程序的执行完全在你的控制之中。你可以在任何位置暂停程序的执行去检查变量和数据结构的值,去显示函数调用序列,去修改程序中变量的值以便观察不同值对程序行为的影响。 

12.4.1.1 产生调试信息 

  要使用内部调试器必须选中Option| Environment菜单References页的Integrated Debugging检查框。缺省情况下该框被选中。

  在开始调试前需要使用Symbols Debug Information(调试符号信息)编译工程文件。调试符号信息包含了一个符号表,能够使调试器在程序的源代码与编译器产生的机器代码间建立联系。这样在程序执行中可以同时查看对应的源代码。

  Delphi 在缺省情况下自动产生调试符号信息。在集成开发环境中的开关选项是Option|project菜单Compiler Options页的Debug Information and Local Symbols检查框。

  当产生的调试符号信息供内部调试器使用时,编译器把调试符号表储存在每个相应的.dcu文件中。

  如果希望在集成环境外使用Turbo Debugger,则需要把调试信息储存在最终的 .exe文件中。为此需要选定Option|Project菜单Linker页的Include TDW Debug Info检查框。

  由于储存调试信息大大增加了执行文件的大小,因而调试完成后应重新生成一个不包含调试信息的执行文件。 

12.4.1.2 运行程序 

  通过调试器(包括内置调试器)运行程序,当程序处于等待状态时,调试器可以获得控制,利用调试器的功能来检查当前程序的状态。通过合理布置屏幕显示,使应用程序运行窗口和Code Editor(代码编辑器)互不重叠,可以让用户在它们间方便地切换以观察代码执行的效果。

  如果希望使用命令行参数来调试程序,则可以通过Run|Parameters 菜单打开运行参数对话框进行设置。 

12.4.2 程序运行的控制 

  程序运行控制的方法和使用如下表。 

   表12.7  程序运行控制的方法和使用途径

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方法 使用途径

───────────────────────────────

运行到光标位置 ● Code Editor加速菜单的Run to Cursor项

(Run to Cursor) ● Run主菜单的Run to Cursor项

● F4

跟踪(Trace Into) ● Run主菜单的Trace Into项

● Trace Into加速按钮

● F7

步进(Step Over) ● Run主菜单的Step Over项

● Step Over加速按钮

● F8

运行到断点 设置断点并按正常方式运行

暂停程序执行 Run主菜单的Program Pause项

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

  跟踪和步进都是一种单步执行方式。但“步”的含义不同。对跟踪而言它一次执行一条简单程序语句。当碰到包含调试信息的函数或过程调用时则跳入该函数或过程,并执行其第一条可执行语句。对步进而言它一次执行一条当前模块的可执行语句,而不管该语句是否是函数或过程调用。

  运行到光标位置和运行到断点都是程序正常运行到某一确定的源代码位置,而后进入调试状态。但相对于运行到光标位置而言,运行到断点更为灵活。因为断点一次可设置多个,同时也可以对断点设置一定的条件。只有满足该条件程序运行才会中止。

 

12.4.3 断点的使用 

12.4.3.1 设置断点 

  设置断点首先在Code Editor中选定你想设置断点的代码行,而后进行如下的任一种操作:

  ● 单击选定代码行左边的空白

  ● 按F5

  ● 选择Code Editor加速菜单的Toggle BreakPoint项

  ● 选择Run|Add Breadpoint打开断点编辑对话框(Edit BreakPoint Dialog Box),而后选择New去确认一个新的断点设置或选择Modify去对一个存在的断点进行修改

  ● 从BreakPoint List加速菜单中选择Add BreakPoint项 

  断点必须位于可执行代码行上,凡设置在注释、空白行、变量说明上的都是无效的。另外,断点既可以在设计状态下设置也可以在运行调试状态下设置。 

12.4.3.2 断点的操作 

  断点列表窗口(BreakPoint List Window)列出了所有断点所在的源文件名、行号、条件以及已通过的次数。如果一个断点非法或失去功能,则在列表窗口中变灰。

  断点列表窗口可以通过选择View|BreakPoint菜单打开。

断点列表窗口是断点操作的基础。

  1.显示和编辑断点处的代码

  利用断点列表窗口可以快速找到断点在源代码中的位置。

  首先选定断点而后从加速菜单中选择View Source或Edit Source。此时Code Editor更新,显示该断点位置处的代码。如果选择的是View Source,则断点列表窗口仍保持活动;如果选择的是Edit Source,则Code Editor获得输入焦点,可以在断点位置修改源代码。

  2.断点功能的丧失和恢复

  使断点失去功能可以使断点从当前程序运行中隐藏起来。假如你定义了一个断点当前并不需要,但可能在以后使用,则这一功能是很有用的。

  断点列表窗口加速菜单的Disable BreakPoint和Disable All BreakPoints项可以使当前选中断点或所有断点失去功能。

  加速菜单中的Enable BreakPoint和Enable All BreakPoint 可以使相应断点恢复功能。

  3.断点的删除

  断点删除可以从Code Editor或断点列表窗口中进行。

  从Code Editor:

  ● 把光标停到包含断点的行并按F5(或选择加速菜单的Toggle BreakPoint)

  ● 单击包含断点行左边的终止符 

  从断点列表窗口:

  ● 选中欲删除的断点并选择加速菜单的Delete BreakPoint项

  ● 删除当前所有断点,则选择加速菜单的Delete All BreakPoints项 

12.4.3.3 修改断点属性 

  断点列表窗口双击选定断点或从加速菜单中选择Edit BreakPoint项,可以打开断点编辑对话框,用于显示和修改断点的属性。

利用断点编辑对话框可以改变断点的位置,设置断点条件。

  断点条件包括两种:布尔表示式和通过次数。

  Condition编辑框用于设置布尔表达式条件。如果表达式值为真(或非零)则程序运行在断点处中止;否则调试器将忽略该断点。

  Pass Count编辑框用于设置通过次数条件,即只有当程序运行在该断点处通过设定次数时程序运行才在该断点处中止。这往往用于对循环体内语句的调试。

  有一点应引起注意的是:当Condition和Pass Count同时设置时,Pass Count是指满足条件的通过次数。

  对如下一段程序: 

var

i,Re,s: Integer ;

begin

s := 1;

Re := 0;

for i:=1 to 100 do

Re:=Re+s*i ;

end; 

在 Re := Re + s*i; 一行设置一断点。

若条件设置为: 

  Condition :  i = 3

Pass Count:   4

  则当程序中止时检测i 的值为7。 

12.4.3.4 断点和程序执行点颜色的设置 

  选择Option|Environment进入环境设置对话框而后选择Editor Colors页标签。此时即可对有关项按自己的希望设置背景和前景色。 

12.4.4 监视数据的值 

  内置调试器提供了如下的工具用于监视程序中数据的值:

  ● 监视列表窗口

  ● 计算/修改对话框

  ● 调栈窗口 

12.4.4.1 监视表达式 

  监视列表窗口(Watch List Window)显示程序运行中当前监视表达式的值。

  选择View|Watches可以打开监视列表窗口。

从Code Editor中添加一个监视表达式最方便的方法是:

  1.选中要监视的表达式;

  2.从Code Editor加速菜单中选择Add Watch把表达式添加到监视列表窗口。

  也可以利用下面的方法产生一个监视表达式:

  1.用下列方法之一打开监视属性对话框(Watch Properties Dialog Box):

●主菜单中选择Run|Add Watch

●在光标处从Code Editor加速菜单中选择Add Watch

  ●按Ctrl-F5

  ●双击监视列表窗口中的一个监视表达式

  ●从监视列表窗口选定一个表达式而后从加速菜单中选择Edit

  2.在监视属性对话框的Expression下拉列表框中输入或选择一个被监视的表达式;

  3.设定表达式的显示格式和使能状态。

  与断点类似,利用加速菜单也可以使监视表达式功能丧失、恢复或删除监视表达式。 

12.4.4.2 计算/修改表达式 

 选择Run|Evaluate /Modify可打开计算/修改对话框。当单击Evaluate按钮时,Expression编辑框中表达式的值显示在Result域中。

  Expression中可以输入或选择任何合法的表达式(包括对象的属性),但不包括;

  1.包含有当前执行点不能引用的局部或静态变量的表达式;

  2.函数或过程调用。

  Expression中的表达式可以带特定的格式字符用于规定其显示格式。 格式字符及其功能如下表。 

   表12.8  格式字符及其功能

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

格式字符 功 能

─────────────────────────────────

$,H,X 以十六进制格式显示标量

D 以十进制格式显示标量

C 把ASCII码在0..31的特殊字等显示为ASCII码图形

Fn 用n个有效数字显示浮点数

M 以十六进制方式显示一变量的内存转储值

P 以段和偏移量格式显示指针。两部分皆为四位十六进制值

R 显示记录、对象的域名和值(例如 X:5,Y:2)

S 用ASCII码显示字符串(包括特殊字符)。用于修改内存转储值

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  修改表达式的值常用于验证错误解决方案的正确性。在Expression编辑框中输入或选定欲修改的表达式,单击Evaluate按钮观察表达式的当前值。而后在New Value编辑框中输入或选中一个新值,并单击Modify按钮确认并更新数据项。这种修改只影响特定的程序运行。

  修改表达式的值(特别是指针变量和数组下标)可能会引起无法预计的后果。因而使用中要特别小心。 

12.4.4.3 显示函数调用 

  选择View|Call Stack可以显示调栈窗口(Call Stack Window)。调栈窗口的顶端列出了应用程序最近的函数调用。

利用调栈窗口可以退出当前跟踪的函数,可以利用加速菜单项显示或编辑位于特定函数调用处的源代码

12.5 其它调试工具 

  Delphi的内置调试器虽然功能很强大,但并不能胜任所有的任务。同时由于内置调试器在执行中引起程序环境的细微变化,所以可能影响错误的发生方式。为此我们需要使用其它调试工具来完成我们的任务。这些调试工具包括Turbo Debugger、WinSight、WinSpector和Browser。Browser将在下一节中专门进行介绍。 

12.5.1 Turbo Debugger 

Turbo Debugger是Borland公司推出的第三代语言调试器,它虽然还没有推出完全支持Delphi的新版本,但也基本能胜任一般Delphi程序的调试。

  Turbo Debugger在字符模式下执行,但它是一个真正的Windows程序,它仅使用基于字符的界面。由于Turbo Debugger是一个准备控制其它程序的特殊程序,因此不可以使用通常Windows任务切换功能如Alt+Tab。

Turbo Debugger的操作大部分与内置调试器相同或类似。利用File|Open菜单装入要调试的文件就可以开始一个调试过程。

  利用Turbo Debugger必须把调试符号信息储存在可执行文件中。具体操作见(12. 4.1.1)中介绍。

  Turbo Debugger与内置调试器相比,有许多新的功能。

  首先它提供了许多在低级硬件信息方面的控制。可以完全访问CPU的寄存器、标志及系统内存。用户可以跟踪远指针到内存位置并直接检查它们的内容。Turbo Debugger可以跟踪进到代码中,即使得不到源代码也可以。

  Turbo Debugger支持许多Windows的特殊功能。它可以跟踪Windows消息,让用户查看程序的局部堆和全局堆,并可以浏览包括DLLs在内的组成程序的所有代码单元列表。

  另外Turbo Debugger支持远程执行能力。可以通过串口链接或通过支持NetBIOS的网络配置Turbo Debugger控制另外一台机器。在这种模式下,一台机器显示调试器屏幕,另一台机器显示被跟踪的程序。这允许在一个屏上单步执行程序并在另一屏上监视结果。 

12.5.2 WinSight

  WinSight 是一个用于查看Windows 对象并跟踪消息的发送和接收的调试工具。WinSight的图标可以在Delphi程序组中找到。

WinSight界面分为两部分,上面为对象树窗口,下面为消息跟踪窗口。如图12.9所示。

  在实际应用中,用户可能只是对其中的一部分消息感兴趣,而又不希望它们淹灭在无用信息之中。为此,用户可以打开Message菜单并选择Selected Windows。按住Shift键,单击对象树窗口中感兴趣的对象,所选定对象的任何消息都显示在消息跟踪窗口中。

  如果用户只想跟踪某些消息类,则打开Message菜单并选择Options ,使用如图12.10所示的检查框过滤消息。 

12.6.6 小结 

  本章介绍的内容,核心是如何增强程序的健壮性并提高开发效率。为此我们首先考察了Delphi的异常处理机制,而后介绍了几种程序调试工具,在您即将结束基础篇的学习时,这些内容是您步入开发大型应用程序的高级程序员行列的必备武器。