第十二章 异常处理与程序调试(一)
在应用程序开发中如何检测、处理程序的运行错误是一个很重要的问题。在 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)节。
在异常保护的情况下,当异常发生时,系统会自动弹出一个消息框用于显示异常的消息。退出当前模块后异常类自动清除。