Delphi异常处理与调试

来源:互联网 发布:js创建对象的几种方式 编辑:程序博客网 时间:2024/05/16 04:04

Delphi异常处理与调试

 

3.1 Delphi异常处理
3.1.1 异常处理的意义
所谓异常,可以理解为一种特殊的事件。当这种特殊的事件发生时,程序正常的执行流程将被打断。异常处理机制能够确保在发生异常的情况下应用程序不会中止运行,也不会丢失数据或资源。
Object Pascal定义了大量的异常处理对象,使应用程序几乎能够处理所有的异常情况,并且对异常处理的语法作了简化。异常处理不再仅仅是高级程序员的“专利”。
没有人能保证程序代码绝对不出错。有时候,即使程序本身没有错,但与程序打交道的软硬件设备出错,也会使程序出现异常。引起异常的设备称为“异常源”,包括操作系统、设备驱动程序、数据库驱动程序(诸如BDE、SQL Links、ODBC、ADO)、动态链接库、常驻内存的防病毒软件、Delphi自身的组件库和运行期库等。
由此可见,异常几乎是不可避免的。过去,在没有异常处理机制的情况下,程序员不得不小心翼翼地检查每一次函数调用的返回值,或者通过额外的代码捕获可能的错误。
关键是,通过检查函数的返回值或设置错误陷阱只能捕获可预见的错误。如果发生没有预见到的错误,或者函数调用本身就已失败,则程序正常的流程将被打乱。
还有一个问题就是,程序中过多的错误检查代码也破坏了程序的可读性。事实上,这些代码大都是闲置的,因为程序中到处发生异常的可能性毕竟不是很大。
使用Object  Pascal语言的异常处理机制,就能解决上述弊端。既能使程序的安全性得到保证,又不至于使程序代码过分琐碎。我们先看一段没有采用异常处理机制的代码:
var
    AChar,AString:ShortString;
begin
    AString:=’Welcome  to  Delphi’;
    AChar:=Copy(AString,20,1);
    if AChar<>#0 then       // #0是空字符,不是空格
    begin
    if AChar<>’!’ then  Insert(AChar,AString,20);
    Exit;
    end;
end;
在上述代码中,虽然检查了Copy()的返回值,但如果Copy()本身调用失败,则程序就执行不到Exit语句。下面改用异常处理机制来编写这段代码:
var
  AChar,Astring:ShortString;
begin
  try
    AString:=’Welcome to Delphi’;
    AChar:=Copy(Astring,20,1);
    if AChar<>’!’  then Insert(AChar,AString,20);
  except
    Exit;
  end;
End;
采用了异常处理机制后,可以先假设程序不可能出错,按正常的顺序写代码。当代码在运行过程中出现异常时,将跳转到except部分,执行Exit。
异常处理机制有两种结构:一种是try...except结构,另一种是try...finally结构,这两种结构在用法上有很大的区别。
3.1.2 错误类型
一般来说,无论在编程的时候如何仔细,程序总会有错误。错误分为四种类型:设计期错误、编译期错误、运行期错误、逻辑错误。
1.设计期错误
这种错误类型发生在设计期,通常是因为给组件的某个属性输入了非法的值。例如,在设计数据库应用程序时指定了一个没有定义的数据库别名。
这种类型的错误比较容易被发现和纠正,因为Delphi能够对属性的值进行合法性检查。一旦发现这种错误,Delphi将弹出一个警告窗口,提示你纠正错误。
2.编译期错误
编译期错误也叫语法错误,当程序代码违反了Object  Pascal的语法规则时将发生这种错误。
如果程序代码中有语法错误,编译就不能通过,代码编辑器的状态栏将给出错误信息提示,并在代码编辑器中突出显示有语法错误的行。
比较常见的语法错误是数据类型不匹配,特别是调用RTL、VCL或Windows的API时容易发生参数不匹配的错误。为了避免这类错误,建议使用Delphi的在线帮助,找到RTL、VCL或API的声明,仔细对照它们的参数。不小心输错变量名或命令语句拼写错误也是经常造成编译期错误的原因。
编译器检查语法错误的功能是可以自定义的。你可以使用“Project”菜单上的“Options”命令打开“Project Options”对话框,然后设置编译器的语法检查选项。
3.运行期错误
程序虽然通过了编译,但在运行时失败了,这种错误称为运行期错误。例如,程序试图打开一个不存在的文件,或者在运算时出现了被零除。
运行期错误分两种情况,一种是运行时随运行异常环境或用户输入的错误参数而造成运行错误,这种情况的“错误”往往无法在交付前通过调试排除,而通常由“异常处理”来解决。另一种是不随环境和参数变化都会产生的运行错误,这种情况下,如果不能确认错误发生在什么地方,可以使用Delphi的内部集成调试器帮助你找到错误所在。例如,可以通过单步执行命令让程序一条语句一条语句地执行,或者通过一个观察窗口来监视某个变量的变化情况。
4.逻辑错误
逻辑错误是指程序通过了编译,也能执行,但执行的结果与预期的不同。
逻辑错误有时比较难找,因为编译器无法识别这种错误。此时,需要用内部集成调试器,通过控制程序的运行以及监视程序的输出,来把错误逐步定位在一个较小的范围内。
5.怎样尽可能地减少错误
虽然错误是很难避免的,但好的编程习惯能够尽可能地减少错误。下面是我们的建议:
(1) 程序应尽可能地模块化
程序分解为模块后,由于每个模块所要完成的任务相对简单了,所以发生错误的可能也就减少了。模块化还简化了程序的维护。
(2) 养成良好的代码书写习惯
注释能够增加代码的可读性,方便维护和修改。缩进能够使程序的语法结构更加清晰。
(3) 不要忘记检查参数的值
在函数内部,首先要检查传递过来的参数值是否合法,是否在一个可接受的范围内。
(4) 不要忘记检查函数的返回值
函数的返回值往往表示函数调用是否成功,以此决定下面的程序流程。如果贸然执行下一步,有可能会出现意想不到的结果。
3.2 Delphi异常类
异常就是不正常或者是不可预料的情况,它干扰正常的程序流。在应用程序运行过程中有许多情况可能导致程序异常,如需要打开的文件不存在,不能向操作系统获得申请的内存,数组元素的索引超过数组的长度,进行除法运算时除数为零,在进行网络访问时,因为某种原因不能和通信对方建立连接等等,均会产生异常现象。
异常类是Delphi异常处理机制的核心,也是Delphi异常处理的主要特色。下面我们对异常类的概念和体系进行详细的介绍。
Delphi提供的所有异常类都是类Exception的子类。用户也可以从Exception派生一个自定义的异常类。
Exception的一系列构造函数中最重要的参数是显示的错误信息。而数据成员中最重要的也是可被引用的消息字符串(message,messagePtr)。这些信息分别对自定义一个异常类和处理一个异常类有重要作用。
Delphi提供了一个很庞大的异常类体系,这些异常类几乎涉及到编程的各个方面。从大的方面我们可以把异常类分为运行库异常、对象异常、组件异常三类。下面我们分别进行介绍。
3.2.1 运行库异常类(RTL Exception)
运行库异常可以分为七类,它们都定义在SysUtils库单元中。
1.I/O异常
I/O异常类EInOutError是在程序运行中试图对文件或外设进行操作失败后产生的,它从Exception派生后增加了一个公有数据成员ErrorCode,用于保存所发生错误的代码。这一成员可用于在发生I/0异常后针对不同情况采取不同的对策。
当设置编译指示{$I-}时,不产生I/0异常类而是把错误代码返回到预定义变量IOResult中。
2.堆异常
堆异常是在动态内存分配中产生的,包括两个类EOutOfMemory和EInvalidPointer,如表3-1所示。
表3-1  堆异常类及其产生原因
异常类 引发条件
EOutOfMemory 没有足够的空间用于满足所要求的内存分配
EInvalidPointer 非法指针。一般是由于程序试图去释放一个已释放的指针而引起的
3.整数异常
整数异常都是从一个EIntError类派生的,但程序运行中引发的总是它的子类:EDivByZero,ERangeError,EIntOverFlow,如表3-2所示。
表3-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:=5;
    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选择框。注意,Delphi不对长字符串做范围检查。
EIntOverFlow异常类在Integer、Word、Longint三种整数类型越界时引发。如下面的代码将引发一个EIntOverFlow异常:
var
    I:Integer;
    a,b,c:Word;
begin
    a:=10;
    b:=20;
    c:=1;
    for I:=0 to 100 do
      c:=a*b*c;
end;
EIntOverFlow异常类只有在编译选择框Option|Project|Over_Flow_Check Option选中时才产生。当关闭溢出检查,则溢出后变量的值是丢弃溢出部分后的剩余值。
4.浮点异常
浮点异常是在进行实数操作时产生的,它们都从一个EMathError类派生,但与整数异常相同,程序运行中引发的总是它的子类EInvalidOp、EZeroDivide、EOverFlow、EunderFlow (表3-3)。
表3-3  浮点异常类及其引发条件
异常类 引发条件
EInvalidOp 处理器碰到一个未定义的指令
EZeroDivide 试图被零除
EOverFlow 浮点上溢
EUnderFlow 浮点下溢
EInvalidOp最常见的引发条件是没有协处理器的机器遇到一个协处理器指令。由于在缺省情况下Delphi总是把浮点运算编译为协处理器指令,因而在386以下微机上常常会碰到这个错误。此时只需要在单元的接口部分设置全局编译指示{$N-},选择利用运行库进行浮点运算,问题就可以解决了。
各种类型的浮点数(Real、Single、Double、Extended)越界引起同样的溢出异常。
5.类型匹配异常
类型匹配异常EInvalidCast当试图用As操作符把一个对象与另一类对象匹配失败后引发。
6.类型转换异常
类型转换异常EConvertError当试图用转换函数把数据从一种形式转换为另一种形式时引发,特别是当把一个字符串转换为数值时引发。下面程序中的两条执行语句都将引发一个EConvertError异常。
var
    r1:Real;
    int:Integer;
begin
    r1:=StrToFloat(’$140.48’);
    int:=StrToInt(’1,402’);
end;
要注意,并不是所有的类型转换函数都会引发EConvertError异常。比如函数Val当它无法完成字符串到数值的转换时只把错误代码返回。利用这一点我们实现了输入的类型和范围检查。
7.硬件异常
硬件异常发生的情况有两种:或者是处理器检测到一个它不能处理的错误,或者是程序产生一个中断试图中止程序的执行。硬件异常不能编译进动态链接库(DLLs)中,而只能在标准的应用中使用(表3-4)。
硬件异常都是EProcessor异常类的子类。但运行时并不会引发一个EProcessor异常。
表3-4  硬件异常类及其产生原因
异常类 引发条件
EFault 基本异常类,是其它异常类的父类
EGPFault 一般保护错,通常由一个未初始化的指针或对象引起
EStackFault 非法访问处理器的栈段
EPageFault Windows内存管理器不能正确使用交换文件
EInvalidOpCode 处理器碰到一个未定义的指令,这通常意味着处理器试图去操作非法数据或未初始化的内存
EBreakPoint 应用程序产生一个断点中断
ESingleStep 应用程序产生一个单步中断
EFault、EGPFault往往意味着致命的错误。而EBreakPoint、ESingleStep被Delphi IDE的内置调试器处理。事实上前边的五种硬件异常的响应和处理对开发者来说都是十分棘手的问题。
3.2.2 对象异常类
所谓对象异常是指非组件的对象引发的异常。Delphi定义的对象异常包括流异常、打印异常、图形异常、字符串链表异常等。
1.流异常类
流异常类包括EStreamError、EFCreateError、EFOpenError、EFilerError、EReadError、EWriteError、EClassNotFound。
流异常在Classes库单元中定义。
流异常引发的原因如表3-5所示。
表3-5  流异常类及其产生原因
异常类 引发条件
EStreamError 利用LoadFromStream方法读一个流发生错误
EFCreateError 创建文件时发生错误
EFOpenError 打开文件时发生错误
EFilerError 试图再次登录一个存在的对象
EReadError ReadBuffer方法不能读取特定数目的字节
EWriteError WriteBuffer方法不能写特定数目的字节
EClassNotFound 窗口上的组件被从窗口的类型定义中删除
2.打印异常类
打印异常类EPrinter当打印发生错误时引发。它在printers库单元中定义。例如应用程序试图向一个不存在的打印机打印或由于某种原因打印工作无法送到打印机时,就会产生一个打印异常。
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;    {引发一个图形异常}
end;
4.字符串链表异常
字符串链表异常EStringListError、EListError在用户对字符串链表进行非法操作时引发。
由于许多组件(如TListBox,TMemo,TTabSet,…)都有一个TStrings类的重要属性,因而字符串链表异常在组件操作编程中非常有用。
EStringListError异常一般在字符串链表越界时产生。例如对如下初始化的列表框:
    ListBox1.Items.Add('Firstitem');
    ListBox1.Items.Add('Seconditem');
    ListBox1.Items.Add('Thirditem');
则以下操作都会引起EStringListError异常:
    ListBox1.Item[3]:=’NotExist’;
    Str:=ListBox1.Item[3];
EListError异常一般在如下两种情况下引发:
(1)当字符串链表的Duplicates属性设置为dupError时,应用程序试图加入一个重复的字符串。
(2)试图往一个排序的字符串链表中插入一个字符串。
3.2.3 组件异常类
1.通用组件异常类
通用组件异常类常用的有三个:EInvalidOperation、EComponentError、EOutOfResource。其中EInvalidOperation、EOutOfResource在Controls单元中定义;EComponentError在Classes单元中定义。
(1)非法操作异常EInvalidOperation
EInvalidOperation引发的原因可能有:
a. 应用程序试图对一个Parent属性为nil的组件进行一些需要Windows句柄的操作
b. 试图对一个窗口进行拖放操作
c. 操作违反了组件属性间内置的相互关系等
例如,ScrollBar、Gauge等组件要求Max属性大于等于Min属性,因而下面的语句:
ScrollBar1.Max:=ScrollBar1.Min-1;
将引发一个EInvalidOperation异常。
(2)组件异常EComponentError
引发该异常的原因可能有:
a. 在Register过程之外试图登录一个组件(常用于自定义组件开发中)
b. 应用程序在运行中改变了一个组件的名称并使该组件与另一个组件重名
c. 一个组件的名称改变为一个Object Pascal非法的标识符
d. 动态生成一个组件与已存在的另一组件重名
(3)资源耗尽异常EOutOfResource
当应用程序试图创建一个Windows句柄而Windows却没有多余的句柄分配时引发该异常。
2.专用组件异常类
许多组件都定义了相应的组件异常类。但并不是有关组件的任何错误都会引发相应的异常类。许多情况下它们将引发一个运行时异常或对象异常。
下面列出几个典型的组件异常类。
(1)EMenuError
非法的菜单操作,例如试图删除一个不存在的菜单项。这一异常类在Menus库单元中定义。
(2)EInvalidGridOpertion
非法的网格操作,比如试图引用一个不存在的网格单元。这一异常类在Grids库单元中定义。
(3)EDDEError
DDE异常。比如应用程序找不到特定的服务器或会话,或者一个连接意外中止。这一异常类在DDEMan库单元中定义。
(4)EDatabaseError,EReportError
数据库异常(EDatabaseError)和报表异常(EReportError)在进行数据库和报表操作出现错误时引发。
3.3 Delphi异常处理机制
Delphi异常处理机制建立在保护块(Protected Blocks)的概念上。所谓保护块是用保留字try和end封装的一段代码。保护块的作用是当应用程序发生错误时自动创建一个相应的异常类(Exception)。程序可以捕获并处理这个异常类,以确保程序的正常结束以及资源的释放和数据不受破坏。如果程序不进行处理,则系统会自动提供一个消息框。
3.3.1 异常响应与try-except语句
异常响应为开发者提供了一个按自己的需要进行异常处理的机制。try…except…end形成了一个异常响应保护块。正常情况下except后面的语句并不被执行,而当异常发生时程序自动跳到except,进入异常响应处理模块。当异常被响应后异常类自动清除。
try  except语句的一般格式如下:
  try                        //try保护代码块
    被保护语句
  except                     //异常处理块
    异常处理语句             //异常不发生,不处理
  end;

  try                        //try保护代码块
    被保护语句
  except                     //异常处理块
    on  <异常对象类型1>  do  <语句1>   //捕获指定类型的异常对象,进行处理
    on  <异常对象类型n>  do  <语句n>   //捕获指定类型的异常对象,进行处理
    else
    <语句n+1>               //缺省的异常处理代码
  end;
try语句块指出了需要进行异常保护的代码。如果在这部分有不正常的事件发生,则引发一个异常对象。except是异常处理部分,被保护部分引发的异常对象将执行<异常处理语句>或由这部分代码捕获并进行处理。
实例3-1 用try-except语句处理被0除情况:
procedure TForm1.Button1Click(Sender: TObject);
var a,b,c:real;
begin
   b:=strtofloat(edit1.Text);
   c:=strtofloat(edit2.Text);
   try
     a:=b /c;
     edit3.Text:=floattostr(a);
   except
     edit3.Text:='不能用0除';
   end;
end;
在正常情况下(即由try保护的部分没有异常事件发生时),应用程序执行完毕try部分代码后,跳过except部分代码,继续运行应用程序。在执行时内代码出现异常情况时,引发一个异常对象后,应用程序将跳过剩余的被保护部分未执行的代码,直接进入except部分进行异常处理,在except异常处理块中,由try保护块引发的异常对象也可以由以下语句来捕获:
on  <异常对象类型>  do  <语句>
实例3-2  两数相除过程中发生异常时的处理情况:
procedure TForm1.Button1Click(Sender: TObject);
var a,b,c:real;
begin
 try
   b:=strtofloat(edit1.Text);
   c:=strtofloat(edit2.Text);
   a:=b /c;
   edit3.Text:=floattostr(a);
 except
   on Ezerodivide do  edit3.Text:='不能用0除';
   on EMathError do  edit3.Text:='计算错误';
   else
     edit3.Text:='发生异常';
 end;
end;
保留字on...do用于判断异常类型。这样,根据不同的异常类型,执行不同的异常处理命令。如上例中,edit2中输入0,则edit3中将显示'不能用0除',若edit1和edit2同时输入0,则edit3中将显示'计算错误',edit1或edit2组建中输入非数字字符,则edit3中将显示'发生异常'。
注意:运行时必须先编译生成exe文件,然后在Windows中运行,否则在发生异常时仍将出现系统的错误信息。
3.3.2 异常保护与try-finally语句
确保回收分配的资源是程序健壮性的一个关键。但缺省情况下异常发生时程序会在出错点自动退出当前模块,因此需要一种特殊的机制来确保即使在异常发生的情况下释放资源的语句仍能被执行。而Delphi的异常处理正提供了这种机制。
1.需要保护的资源
一般说来需要保护的资源包括:文件、内存、Windows资源和对象。
比如下面一段程序就会造成1K内存资源的丢失。
var
   pPointer:Pointer;
   iInt,iDiv:Integer;
begin
   iDiv:=0;
   GetMem(pPointer,1024);           //分配1K的内存
   iInt:=10 div iDiv;               //这里将触发被零除的异常
   FreeMem(pPointer,1024);          //永远执行不到这儿
end;
由于程序从异常发生点退出,从而FreeMem永远没有执行的机会。
2.创建一个资源保护块
一个设计良好的应用程序必须关闭或释放操作系统为应用程序分配的各种资源。即使在应用程序运行中产生了不正常的事件,也要保证系统资源的释放和关闭。如应用程序运行过程中申请的内存资源,在应用程序结束前必须释放它,否则将会使内存丢失。特别需要注意的是,不仅用户代码可能产生异常,许多运行时函数也可能产生异常。一旦产生异常应用程序将被终止,这时可能出现分配的系统资源未被释放。使用try-finally语句可以有效地避免这种情况。
try finally语句的一般格式如下:
    try                        //try保护代码块
      被保护语句
    finally                     //异常处理块
      异常处理语句             //无论异常发生否,都必须处理
    end;
若用作创建一个资源保护块时,它的格式可写成:
    (分配系统资源)
    try
      (使用系统资源的语句)
    finanlly
      (释放系统资源)
    end;
try前面的语句中包含了申请或创建系统资源的语句。try部分包含了使用系统资源的代码语句。finally部分包含资源释放语句。在应用程序运行过程中,如果在try部分产生异常情况,将直接跳过随后的代码,直接执行finally部分的资源释放语句。如果不产生异常,则应用程序正常运行,最后执行finally部分中的资源释放语句。
注意:在try-finally语句中,当try部分产生异常后,应用程序直接执行Finally部分的资源释放语句。
实例3-3  用try-finally语句确保所分配内存资源的在异常情况下仍被释放:
var
  pPointer: Pointer;
  iInt,iDiv: Integer;
begin
  iDiv:=0;
  GetMem(pPointer,1024);          //分配1K的内存
  try
  iInt:=10 div iDiv;              //这将触发异常
  finally
  FreeMem(pPointer,1024);         //释放先前分配的内存
  end;
end;
try...finally结构与try...except结构在用法上主要有以下区别:
(1) 对于try...finally结构来说,不管try部分的代码是否触发异常,finally部分总是执行的。如果发生异常,就提前跳到finally部分。而对于try...except结构来说,只有当触发了异常后,才会执行except部分的代码。
(2) 在try...except结构中,当异常被处理后异常对象就被释放,除非重新触发异常。而在try...finally结构中,即使finally部分对异常作了处理,异常对象仍然存在。
(3) finally部分不能处理特定的异常,因为它没有try...except结构中的异常句柄,无法知道确切的异常类型。因此,在finally部分只能对异常作笼统的处理。
(4) 在try…finally结构中,如果在try部分调用标准命令Exit、Break或Continue,将导致程序的执行流程提前跳到finally部分。finally部分不允许调用上述三个命令。
3.3.3 异常的重引发和处理嵌套
由于异常在处理后即被清除,因而当希望对异常进行多次处理时就需要使用保留字raise 来重引发一个当前异常。
实例3-4 同时使用异常响应和异常保护来处理被0除和释放资源,当异常响应结束时利用raise重引发一个当前异常。
var
  pPointer:Pointer;
  iInt,iDiv:Integer;
begin
  iDiv:=0;
  GetMem(pPointer,1024);
  try
    try
    iInt:=10 div iDiv;
    except
    On EDivByZero do
      begin
      iInt:=0;
      raise;
      end;
    end;
  finally
  FreeMem(pPointer,1024);
  End;
End;
上面一段代码体现了异常处理的嵌套。异常保护、异常响应可以单独嵌套也可以如上例所示的那样相互嵌套。
3.3.4 定义自己的异常
利用Delphi的异常类机制我们可以定义自己的异常类来处理程序执行中的异常情况。同标准异常不同的是:这种异常情况并不是相对于系统的正常运行,而是应用程序的预设定状态。比如输入一个非法的口令、输入数据值超出设定范围、计算结果偏离预计值等等。
使用自定义异常需要:
(1)自己定义一个异常对象类;
(2)自己引发一个异常。
1.定义异常对象类
异常是对象,所以定义一类新的异常同定义一个新的对象类型并无太大区别。由于缺省异常处理只处理从Exception或Exception子类继承的对象,因而自定义异常类应该作为Exception或其它标准异常类的子类。这样,假如在一个模块中引发了一个新定义的异常,而这个模块并没有包含对应的异常响应,则缺省异常处理机制将响应该异常,显示一个包含异常类名称和错误信息的消息框。
下面是一个异常类的定义:
type
    EMyException=Class(Exception);
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;
下面通过例程来说明类型文件的使用。
实例3-5 编写一个利用自定义异常编程的完整实例。程序设计界面如图3-1所示。
(1).新建应用程序
启动Delphi,从[File]菜单中选取[New Application]命令,开始一个新的应用文件,应用程序中将有一个缺省窗体Form1。
(2).设置窗体属性
将窗体Form1的Caption属性设置为“自定义异常”;Font属性设置为宋体、五号;Height和Width属性分别设置为200、290。
 
图3-1  自定义异常实例(3).添加组件
往窗体Form1中添加两个Label组件-Label1和Label2,两个Edit组件-name属性分别设为PassWord和InputEdit,编辑框用于输入口令和代码,和两个Button组件-Button1和Button2。
(4).设置组件属性
表3-6  实例3-5组件属性
组件 Caption Visible PassWordChar
Label1 输入密码: 
Label2 输入数字(0,1): false
PassWord   *
InputEdit  false
窗体的布置如图3-1所示。
程序启动时Label2、InputEdit不可见。当在PassWord中输入正确的口令时,Label2、InputBox出现在屏幕上。此时Labell、PassWord隐藏。通过设置PassWord的PassWordChar为“*”,可以使输入口令时回显在屏幕上的字符为“*”。
(5).自定义异常
自定义异常EInvalidPassWord和EInvalidInput分别用于表示输入的口令非法和数字非法。它们都是自定义异常EInValidation的子类。而EInValidation直接从Exception异常类派生。
下面是三个异常类的定义:
  type
  TForm1 = class(TForm)
  ……
  private
    { Private declarations }
  public
    { Public declarations }
  end;    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('输入的密码不正确',0);
end;

constructor EInValidInput.Create(ErrorNum:Integer);
var
    Msg:String;
begin
    case ErrorNum of
    1:
    Msg:='无法把字符转换成数字';
    2:
    Msg:='数值超出范围';
    else
    Msg:='输入有错';
    end;
    inherited Create(Msg,ErrorNum);
end;
对于EInvalidInput,ErrorCode=1表示输入的不是纯数字序列,而ErrorCode=2表示输入数值越界。
(6).添加事件
为窗体Form1中的PassWord和InputEdit编辑框添加OnKeyPress事件,事件响应过程分别为PassWordKeyPress和InputEditKeyPress。
(7).编写事件代码
口令检查是用户在PassWord中输入口令并按下回车键后开始的。即在PassWordKeyPress中,要对口令进行核对操作,若不正确触发异常

EInvalidPassWord。
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的OnKeyPress事件处理过程中实现了输入数字的合法性检查:
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('输入正确',mtInformation,[mbOk],0);
      Key:=#0;
    except
      on E:EInValidInput do
         begin
         InputEdit.text:='';
         MessageDlg(E.Message,mtWarning,[mbOk],0);
         end;
    end;
  end;
end;
(8).保存程序
将窗体Form1的单元文件保存为“Unit03_5.pas”,将项目文件保存为“Project03_5.dpr”。
(9).运行程序
必须先编译生成Project03_3.exe文件,然后在Windows资源管理器中运行Project03_3.exe。运行时,不同的输入会触发不同的异常。
由于异常响应后即被清除,所以要显示异常信息,需要另外的手段。在以上两段程序中我们采用了两种不同的方法:在口令合法性检查中,利用异常重引发由系统进行缺省响应;在输入数字合法性检查中,通过异常实例来获取异常信息并由自己来显示它。
以上所举的是一个非常简单的例子,但从中已可以发现:使用自定义异常编程,为程序设计带来了很大的灵活性。
3.3.5 利用异常响应编程
利用异常处理机制不仅能使程序更加健壮,而且也提供了一种使程序更加简捷、明了的途径。事实上,使用自定义异常类就是一种利用异常响应编程的方式。这里我们再讨论几个利用标准异常类编程的例子。
比如为了防止零作除数,可以在进行除法运算前使用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 EdivByZerod do
     Result:=0;
    end;
end;
实例3-6  在文件的打开与创建中可以利用异常响应来实现文件的打开或创建。
Procedure TRecFileForm.OpenButtonClick(Sender:TObject);
begin
    if OpenDialog1.Execute then
      FileName:=OpenDialog1.FileName
    else
     exit;
    AssignFile(MethodFile,Filename);
    try
     Reset(MethodFile);
     FileOpened:=True;
    except
    OnEInOutError 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
         FileOpen:=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;
    OpenButtomEnabled:=False;
    CloseButton.Enabled:=True;
end;
总之,利用异常响应编程的中心思想是虽然存在预防异常发生的确定方法,但却对异常的产生并不进行事先预防,而是进行事后处理,并以此来简化程序的逻辑结构。
3.4 Delphi调试器
Delphi 在IDE内部集成了一个调试器,因而对程序的调试不用离开集成开发环境(IDE)就可以进行。该调试器能够控制程序的运行、监视程序的输出、检查和修改变量的值。
3.4.1 准备调试
在调试程序之前,必须保证程序代码已经没有语法错误,还要正确设置一些选项。为此,需要使用“Project”菜单上的“Options”命令打开“Project Options”对话框。翻到“Compiler”页,选中“Debug information”复选框(缺省就是选中)。这样,编译器将把所有的调试信息加到.DCU文件和.EXE文件中。
VCL的代码都是仔细调试过的,一般不会有错误。如果仍然不放心,想跟踪进入VCL的内部,则需要选中“Use Debug DCUs”复选框。
要说明的是,调试信息将使可执行文件增大,但不影响程序的性能和对内存的需求。尽管如此,调试结束后,最好要打开"Project Options”对话框,翻到“Compiler”页,清除“Debug infomation”复选框,再重新编译程序。这样,程序中就不包含任何调试信息。
要使用内部集成调试器来调试程序,还必须使用“Tools”菜单上的“Debugger Options”。命令打开“Debugger Options”对话框,选中“Integrated debugging”复选框(缺省就是选中)。否则,“Run”菜单上的调试命令将变灰。但要说明的是,内部集成的调试器可能会与某些软件冲突,从而引起应用程序运行异常。因此,在调试程序时最好把可能引起冲突的软件退出运行。
使用“Tools”菜单上的“Environment Options”命令打开“Environment Options”对话框,翻到“Preferences”页。如果选中“Hide designers on run”复选框,当程序运行时,Object  Inspector和Form设计器将关闭,以腾出屏幕上的空间。如果选中“Minimize on run”复选框,当程序运行时,IDE将最小化,以避免屏幕上内容太多太乱。不过,当程序暂停运行时,IDE的窗口会重新恢复成原先大小。
3.4.2 设置调试器的选项
1.设置调试器选项
要设置调试器的选项,可以使用“Tools”菜单上的“Debugger Options”命令,Delphi将打开“Debugger Options”对话框,如图3-2所示。
有General页、Event Log页、Language Exceptions页和OS Exceptions页,用来设置调试的一些环境、配置及方法等
 
图3-2  “Debugger Options”对话框2.编译指令
默认情况下,上述设置对整个项目的所有单元都有效。不过,也可以让这些设置只对部分单元有效,因为有些单元可能没有问题,不希望参加调试。
要使某个单元不包含调试信息,就在这些单元中加入适当的编译指令,程序示例如下:
    unit Unit1;
    {$DEBUGINFO OFF}
    interface

3.自定义调试器的颜色
在上册中已提到,代码编辑器可以用不同的颜色显示不同的语法成分,在使用调试器调试程序时也有这个功能。例如,通常断点用白底红色表示,当前执行点用白底蓝色表示。
要自定义调试器的颜色,可以使用“Tools”菜单上的“Editor Options”命令打开“Editor  Properties”对话框,翻到“Colors”页,在“Element”框中选择某种语法元素,然后设置它的前景颜色和背景颜色。
3.5 控制程序的运行
带调试信息编译了程序后,就可以调试程序了,调试器将接管对程序运行的控制,但程序的运行结果与在非调试状态下运行没有什么两样,包括建立窗口、接受用户输入、计算数值、响应事件、访问数据库等均照常进行。
可以在单步执行或跟踪执行时同步观察应用程序的输出。为了避免出现令人讨厌的闪烁,最好把应用程序的窗口和代码编辑器的窗口恰当布局,不要使它们相互覆盖。
3.5.1 单步执行
通过[Run]菜单上的[Step Over]命令,可以单步执行程序。所谓单步执行,就是一次只执行一行(一个指令),这样就可以知道哪一行或指令引起了运行期错误或逻辑错误。
[Step Over]命令将把整个过程或函数当作一行。如果把几条语句写在一行上,调试器将把这几条语句当作一条语句。这样,就无法单独调试其中的某一个语句。如果把一条长语句分成几行写,调试器仍然把这几行当作一行。
调试器每执行一行,当前执行点就自动移到下一个要执行的行上,但不一定是源代码的下一行。例如,若正在执行的是Goto语句,当前执行点将移到Goto语句跳转到的行上。
另外还有一种情况就是,如果开启了优化编译的选项,某些源代码行将被合并或越过,这时候,当前执行点不会移到这些行上。
为了清晰地看出当前执行点在哪儿,代码编辑器将用白底蓝色显示当前执行点,同时,在“装订区”显示一个绿色的箭头指向当前执行点,如图3-3所示。
 
3.5.2 跟踪执行
[Run]菜单上的[Trace Into]命令用于跟踪程序。与单步执行相似,这条命令一次也只执行一行。不同的是,执行到有函数调用的行时,这条命令将进入函数的内部。
如果程序链接了外部代码诸如动态链接库,只要动态链接库包含了符号调试信息,就可以跟踪进入这些外部代码。否则,调试器将把动态链接库当作一行处理。
在调试过程中,可以根据需要交替使用单步执行和跟踪执行。例如,对有疑问的调用命令使用跟踪执行,使控制进入被调用部分内部,调试该被调用部分。而对有把握的调用命令使用单步执行,从而直接跳过被调用部分的调试,这样能提高调试效率。
“Trace Into”命令也能够进入事件句柄的内部,就像进入一般的函数内部一样。要注意的是,OnPaint事件是当应用程序的窗口需要重画的时候触发的,当进入处理该事件的句柄内部时,代码编辑器的窗口将推到前端。也就是说,此时窗口需要重画了,这样又将触发OnPaint事件。而一旦进入处理OnPaint事件的句柄内部,代码编辑器的窗口又将被推到前端。如此反复,构成无限循环。要解决上述问题,必须把代码编辑器与应用程序的窗口在屏幕上重新排列,不要相互覆盖。
程序往往大量调用了VCL的方法,一般情况下,不要跟踪进入VCL的内部,因为VCL的源代码一般是不会出错的。如果怀疑VCL中可能出错,或者想进入VCL方法的内部看看方法是怎样实现的,你也可以进入VCL方法的内部。Delphi Enterprise和Delphi Professional附带了VCL的源代码,而且还提供了带调试信息的VCL库。
3.5.3 跳过一段代码
为了节省时间和提高工作效率,不必每次都从头开始单步或跟踪执行程序,可以一下子跳到有疑问的地方,然后再一行一行地执行程序。
[Run]菜单上的[Run to Cursor]命令可以实现这个功能。它能够先以非调试方式执行到光标所在的行,再接管对程序的控制,单步或跟踪执行以后的代码。
如果光标所在的行不包含调试信息,调试器将推出一个错误框显示“No code was  generated  for the current Line”。
如果不小心进入了例程的内部,想马上退出来,可以把光标移到该例程的最后一行,使用[Run to Cursor]命令,再使用[Step Over]命令,就可以返回到调用该例程的地方。
3.5.4 全速执行剩余的代码
如果不小心进入了一个例程,但又不想调试这个例程,或者确信该例程的代码没有问题,从而想尽快退出这个例程,你可以使用[Run]菜单上的[Run Until Return]命令。这个命令将全速执行该例程的代码,直到返回为止。
3.5.5 返回到执行点
在调试过程中,随时可以切换到IDE或其他程序中,可以进行各种操作。
如果要重新回到调试器的当前执行点,可以使用[Run]菜单上的[Show Execution Point]命令,光标将自动回到先前的执行点上。
如果包含执行点的源文件已关闭,调试器将重新打开这个源文件。如果执行点没有对应的源代码,Delphi将打开CPU窗口,显示相应的机器指令。
3.5.6 暂停运行
使用[Run]菜单上的[Program Pause]命令将使程序运行暂时停止,这样就可以检查程序在此状态下的输出或变量的值是否正确,检查完以后,可以继续对程序进行调试,或者修改变量的值再让程序继续执行,以便看程序对新的值会作出什么反应。
有时候,程序暂停后无法回到调试器中继续运行,这时候可以同时按—下Ctrl+Alt+SysRq键终止程序的运行,如果按一次不行,就多按几次。
3.5.7 重新开始运行
在调试过程中,可以使用[Run]菜单上的[Program Reset]命令中止程序的运行并释放所有占用的内存和资源,关闭所有打开的文件,清除所有的变量设置,然后重新运行程序。
这通常用于在调试过程中发现了错误并更改了源代码后需要重新编译和运行的情况。
“Program Reset”命令并不删除先前设置的断点和观察表达式,因为重新开始调试程序时可能还要用到这些设置。
“Program Reset”命令可能不能很“干净”地释放应用程序占用的所有资源,这样可能导致其他程序运行失败,碰到这种情形应当退出Delphi或者重新启动Windows。
3.5.8 命令行参数
如果要调试的程序需要传递参数,可以使用[Run]菜单上的[Parameters]命令打开[Run  Parameters]对话框,如图3-4所示。
 
图3-4  命令行参数在“Parameters”框内键入要传递的参数,也可以从以前键入过的参数中选择一个。
3.6 断点
所谓断点,就是在程序代码的某一行上设一个标记,程序执行到这里将暂停,由调试器接管对程序的控制。使用断点与使用[Run to Cursor]命令有点相似,都是执行到某一行后暂停。不同的是,程序中可以设多个断点并且能够给断点设置条件。
断点通常设在怀疑有问题的区域。在遇到断点之前,程序以全速运行。遇到断点之后,程序暂时停止运行,以后就可以单步或跟踪执行程序。
3.6.1 源代码断点
要在代码编辑器中设置源代码断点,有以下4种操作方式:
(1) 把光标移到要设为断点的行上,按下F5键;
(2) 用鼠标左键单击要设为断点的行的最左端;
(3) 用鼠标右键单击要设为断点的行,在弹出的菜单中选择“Debug”命令,再选择“Toggle Breakpoint”;
(4) 使用“Run”菜单上的“Add Breakpoint”命令,再选择“Source Breakpoint”,Delphi将打开“Add Source Breakpoint”对话框,如图3-5所示。
“File name”框用于输入断点所在的源文件名(包含路径)。
“Line number”框用于输入断点所在的行号。
“Condition”框用于设置断点有效的条件,通常是一个布尔表达式。布尔表达式中可以包含函数调用,只要该函数返回布尔值即可。
 
图3-5  “Add Source Breakpoint”对话框当程序执行到这个断点时,首先计算该布尔表达式的值。如果值为True,则断点有效,程序将暂停运行。如果值为False,则断点无效,程序将继续执行。
“Pass count”框用于指定经过断点多少次后断点有效。例如,在一个For循环中设一个断点,每次循环时都会遇到这个断点。但并非每次遇到断点时程序都会暂停,因为还需要经过一定次数后断点才有效。
“Group”框用于把断点分组。你可以在这个框内输入一个新的组名,也可以选择一个已有的组名。一旦若干个断点编成组,就可以分别使用“Disable Group”命令和“Enable Group”命令成组地被禁止或允许它们,还可以给一组断点指定一系列动作。
注意:设为断点的行必须是可执行的代码行。如果把断点设在注释行、空行、变量声明的行上,调试器将认为断点无效。
默认情况下,断点所在的行用白底红字显示,并且在装订区有一个填为红色的小圆圈。如图3-6所示。
当鼠标指向这个小圆圈时,将弹出一个提示窗口,显示断点的条件和经过次数。
 
图3-6  断点3.6.2 机器指令断点
Delphi允许针对某个机器指令设断点。当程序执行到这个指令时,就会暂停(必须执行到断点处设置)。要设置机器指令断点,有下列几种方式:
(1) 在CPU窗口中用鼠标左键单击某个指令的装订区;
(2) 在CPU窗口中选择一个指令,然后按F5键;
(3) 在CPU窗口中用鼠标右键单击某个指令,在弹出的菜单中选择“Toggle breakpoint”命令;
(4) 使用“Run”菜单上的“Add Breakpoint”命令,再选择“Address Breakpoint”,Delphi将打开“Add Address Breakpoint”对话框,如图3-7所示。
 
图3-7  “Add Address Breakpoint”对话框“Address”框用于指定断点(在这儿是机器指令)的地址。
“Condition”框用于设置断点有效的条件。
“Pass count”框用于指定经过断点多少次后断点有效。
“Group”框用于把断点分组。
当程序执行到这条指令时,就会暂停(如果满足有关条件的话)。
3.6.3 数据断点
Delphi能够监视指针错误。如果内存的某个地址被改写,程序就会暂停,由调试器接管控制权,就好像遇到断点一样。
要设置这样的断点,可以使用“Run”菜单上的“Add Breakpoint”命令,再选择“Data  Breakpoint”,Delphi将显示“Add Data Breakpoint”对话框,如图3-8所示。
 
图3-8  “Add Data Breakpoint”对话框“Address”框用于指定要监视的内存地址。可以键入一个变量名。
 “Length”框用于指定数据的长度(字节数)。如果“Address”框键入的是一个变量名的话,则“Length”框可以空着,因为Delphi会自动计算出该变量的长度。
“Condition”框用于设置断点有效的条件,通常是一个布尔表达式。
“Pass count”框用于指定经过断点多少次后断点有效。
“Group”框用于把断点分组。
要说明的是,当本次调试结束时,所有的数据断点都会被禁止。下次调试时,如果还要用到这些数据断点的话,需要使它们有效。
3.6.4 模块断点
要监视模块的第一次加载,可以使用“Run”菜单上的“Add Breakpoint”命令,再选择“Module Load Breakpoint”,Delphi将打开“Add Module”对话框,如图3-9所示。
 
图3-9“Add Module”对话框在“Module Name”框内指定一个要监视的模块,通常是DLL或BPL。也可以单击“Browser”按钮定位一个模块。以后,当这个模块第一次调入内存时,程序就会暂停,由调试器接管控制权,就好像遇到断点一样。
3.6.5 指定遇到断点时的行为
一般来说,当遇到断点时,程序将暂停。不过,在Delphi中,除了使程序暂停外,还可以指定其他行为。
要指定遇到断点时的行为,可以在设置断点时进行。使用“Run”菜单上的“Add  Breakpoint”命令,再选择一种断点类型,Delphi将打开“Add Breakpoint”对话框。然后,单击“Advanced”按钮,“Add Breakpoint”对话框将被展开,如图3-10所示。
 
图3-10  指定遇到断点时的行为如果选中“Break”复选框,当遇到断点时,程序将暂停。这是默认的行为。
如果选中“Ignore subsequent exceptions”复选框,以后将忽略当前进程触发的异常,也就是说,遇到异常时调试器不会停止。这个复选框与下面的“Handle subsequent exceptions”复选框成对使用,可以使一段代码忽略异常。
如果选中“Handle subsequent exceptions”复选框,以后将处理当前进程触发的异常,也就是说,当调试器遇到“Debugger Options”对话框中指定的异常时将停止。
“Log message”框让你指定一个消息。当遇到断点时,将记载这个消息。
“Eval expression”框让你指定一个表达式。如果下面的“Log result”复选框被选中的话,当遇到断点时,将计算这个表达式并记载计算结果。
“Enable group”框可以使一组断点有效。
“Disable group”框可以禁止—组断点。
如果—个断点被指定了多个行为,则当遇到断点时,将依次执行这些行为。
3.6.6 断点列表窗口
如果定义了很多断点,或者断点不在当前的编辑窗口中,可以通过断点列表窗口来查找断点并且在源代码中定位它。
使用“View”菜单上的“Debug Windows”命令,再选择“Breakpoints”,Delphi将打开断点列表窗口,如图3-11所示。
 
图3-11  断点列表对于源代码断点来说,断点列表窗口将显示断点所在的源文件名称、行号、条件和经过次数。
对于机器指令断点来说,断点列表窗口将显示断点所在的源文件名称、行号加一个16进制的偏移量,这个偏移量就是该指令距离源代码行机器指令起始点的字节数。不过,有时候,—条机器指令并不—定对应着一条源代码行,此时,就不显示行号。
对于数据断点,断点列表窗口将显示数据的名称或地址以及长度。
Delphi的断点列表窗口还有两栏,分别用于显示断点的行为和所属的组名。
要在源代码中定位断点所在的行,或者在CPU窗口中定位断点所在的机器指令,可以在断点列表窗口中用鼠标右键单击某个断点,在弹出的菜单中选择“View Source”或“Edit  Source”命令。如果选择“View Source”命令,光标将定位于要找的断点上,但断点列表窗口仍然是当前活动的窗口,以便继续在断点列表窗口中查找其他断点。如果选择的是“Edit  Source”命令,代码编辑器将成为当前活动的窗口,这样就可以编辑源代码。
3.6.7 删除断点
删除断点并不是删除断点所在的行或指令,只是取消断点的定义,程序执行到这儿不会暂停。
如果仅仅要删除一个断点,有下列几个操作方式:
(1) 在断点列表窗口中用鼠标右键单击断点,在弹出的菜单中选择“Delete”命令;
(2) 在断点列表窗口中选择一个断点,然后按Delete键或按Ctrl+D键;
(3) 在代码编辑器或CPU窗口中用鼠标右键单击断点,在弹出的菜单中选择“Debug”命令,再选择“Toggle Breakpoint”;
(4) 在代码编辑器或CPU窗口中把光标移到断点上,按下F5键;
(5) 在代码编辑器或CPU窗口中用鼠标左键单击断点的最左端。
如果要删除所有断点,可以在断点列表窗口中单击鼠标右键,在弹出的菜单中选择“Delete All”命令。
3.6.8 设置断点的属性
要设置断点的属性,需要打开“Breakpoint Properties”对话框。为此,你可以在断点列表窗口中用鼠标右键单击断点,在弹出的菜单中选择“Properties”命令;或者在代码编辑器或CPU窗口中用鼠标右键单击小圆圈,在弹出的菜单中选择“Breakpoint Properties”命令。
以源代码断点为例,“Source Breakpoint Properties”对话框如图3-12所示。
 
图3-12  修改断点的属性“Filename”框用于指定包含断点的文件名及其路径。
“Line number”框用于指定断点所在的行号。
“Condition”框用于设置断点有效的条件,通常是一个布尔表达式。
“Pass count”框用于指定经过断点多少次后断点有效。
“Group”框用于把断点分组。
3.6.9 禁止和允许断点
设置了断点以后,也可以暂时禁止这个断点,使这个断点暂时无效。程序执行到这里将不会停下来,但断点的属性继续保留,以后还可以用它。
要暂时禁止断点,可以在断点列表窗口中用鼠标右键单击断点,在弹出的菜单中不要选中“Enabled”命令。要重新允许这个断点,可以重新选中“Enabled”命令。
3.7 监视表达式的值
在单步执行或跟踪执行程序的时候,可以同步监视某个表达式的值。通过监视表达式的值,可以分析程序是执行到哪里才开始不正常的。
要监视表达式的值,使用[Run]菜单上的[Add Watch]命令或[Evaluate/Modify]命令,前者可以同时观察多个表达式的值,后者只能观察一个表达式的值但可以修改。也可以使用[Inspect]命令,这个命令适合于监视结构型数据的值。
3.7.1 观察窗口
要用观察窗口监视表达式的值,有下列3种操作方式:
(1) 直接使用[Run]菜单上的[Add Watch]命令;
(2) 在代码编辑器中把光标移到某个表达式上,然后按Ctrl+F5键,或者用鼠标右键单击某个表达式,在弹出的菜单中选择“Debug”下的“Add Watch At Cursor”命令;
(3) 使用“View”菜单上的“Debug Windows”命令,再选择“Watch”,然后在观察窗口中单击鼠标右键,在弹出的菜单中选择“Add Watch”命令。
实际上,上述三种操作都会打开“Watch Properties”对话框,如图3-13所示。
 
图3-13  设置表达式的观察属性如果“Watch Properties”对话框是用第一或第三种操作方式打开的,首先要在“Expression”框内输入要监视的表达式;如果对话框是用第二种方式打开的,光标所在的表达式已经自动出现在“Expression”框内。当然,不管哪种方式,都可以键入一个新的表达式或者从下拉列表中选择一个过去曾经键入过的表达式。
要注意的是,在“Expression”框内键入的表达式必须符合Object Pascal的语法规则。表达式中可以包含函数调用。
通过观察窗口可以监视某个对象的属性。从表面上看,属性就好像一个变量一样。实际上,对象的属性是通过对象内部的函数调用访问的,因此,直接监视对象的属性可能会产生意想不到的结果。解决上述问题的办法很简单,就是把属性的值赋给一个变量,通过监视这个变量间接地监视这个属性。不过,这又带来一个新的问题,就是编译器可能会认为这个赋值是没意义的,从而把它优化掉。事实上,这个变量的赋值确是没意义的,因为代码中没有任何地方再引用这个变量。要解决这个问题也有办法,就是把这个变量声明为全局变量,使编译器难以简单地判断出这个赋值没意义。
假设要监视的数组是这样声明的:
Var
    MyArray:array [1..1000]  of char;
可以看出,这个数组很大。如果在“Expression”框内键入MyArray,这意味着要监视它的每一个元素,而实际上关心的往往只是其中一部分元素,例如第900到909个元素。这时,应当在“Expression”框内键入MyArray[900],同时在“Repeat count”框内键入10,表示让观察窗口显示从MyArray[900]开始的10个元素。
如果要监视的表达式是浮点数,可以在“Digits”框内指定数据的有效位。
如果选中“Enabled”复选框,表示该表达式将加到观察窗口。如果不选中“Enabled”复选框,表示该表达式不加到观察窗口,但其观察属性不会丢失。
默认情况下,调试器根据表达式本来的数据类型选择一种最合适的格式来显示表达式的值,但也可以让表达式以另一种格式输出。例如,一个整型变量本来以十进制显示,你可以让它以十六进制显示。
设置好观察属性后,单击[OK]按钮,就把表达式加到观察窗口中,如图3-14所示。
 
图3-14  观察窗口在观察窗口中,如果要修改某个表达式的观察属性,可以用鼠标右键单击该表达式,在弹出的菜单中选择“Edit Watch”命令,Delphi将打开“Watch Properties”对话框。
如果要暂时禁止某个观察表达式,可以在观察窗口中用鼠标右键单击该表达式,在弹出的菜单中选择“Disable Watch”命令。如果要禁止所有的观察表达式,可以在观察窗口中单击鼠标右键,在弹出的菜单中选择“Disable All Watches”命令。
如果要重新允许某个观察表达式,可以在观察窗口中用鼠标右键单击该表达式,在弹出的菜单中选择“Enable Watch”命令。如果要允许所有的观察表达式,可以在观察窗口中单击鼠标右键,在弹出的菜单中选择“Enable All Watches”命令。
如果要永久删除某个观察表达式,可以在观察窗口中用鼠标右键单击该表达式,在弹出的菜单中选择“Delete Watch”命令;如果要删除所有的观察表达式,可以在观察窗口中单击鼠标右键,在弹出的菜单中选择“Delete All Watches”命令。
3.7.2 计算和修改表达式的值
要计算和修改一个表达式的值,可以使用“Run”菜单上的“Evaluate/Modify”命令,或在代码编辑器中用鼠标右键单击某个表达式,在弹出的菜单中选择“Debug”命令,再选择“Evaluate/Modify”,Delphi将打开“Evaluate/Modify”对话框,如图3-15所示。
 
图3-15 Evaluate/Modify对话框

在“Expression”框内键入一个表达式或从下拉列表框中选择一个已有的表达式,它可以是对象的属性,例如Buttonl.Height。
要计算表达式的值,可以单击“Evaluate”按钮,表达式的值将出现在“Result”框内。
要计算的表达式可以是任何符合Object Pascal语法规则的表达式,但不能包含函数调用或者当前执行点上没有定义的局部变量。
“Evaluate/Modify”对话框最重要的功能就是能修改表达式的值。这样,在调试过程中,可以用不同的值去观察程序的反应。
要修改表达式的值,可以在“New Value”框内键入一个新的值,然后单击“Modify”按钮。此时,表达式在内存中的值就被新的值替换。要注意的是,在“New Value”框内键入的值必须与表达式的类型兼容,否则有可能引起错误。
“Evaluate/Modify”对话框是非模态的,所以即使没有关闭该对话框,也可以把输入焦点移走,以便使用“Run”菜单上的“Step Over”命令继续执行程序,看看新的值对程序有什么影响。如果在执行过程中表达式的值又发生了变化,“Evaluate/Modify”对话框的“Result”框不会自动更新,需要单击“Evaluate”按钮手动更新表达式的值。
“Evaluate/Modify”对话框的“Result”框是一个多行的编辑框,也就是说,“Evaluate/Modify”对话框比观察窗口更适合于显示大型的数据如记录和对象。
3.7.3 计算提示
当处于调试暂停的状态时,在代码编辑器中只要把鼠标指向某个变量,在该变量的附近将弹出一个小窗口,窗口内显示该变量的当前值。不过,这个功能并不能完全替代观察窗口和“Evaluate/Modify”对话框。观察窗口的优势在于能同时显示多个表达式的值,而“Evaluate/Modify”对话框的优势在于能够修改表达式的值。另外,对于在开域语句中简写了的变量,或在退出当前例程前不再引用的变量,小窗口也不会弹出。
3.7.4 Inspector窗口
Inspector窗口非常适合于查看复合的数据,诸如对象、结构等。
要打开Inspector窗口,可以使用“Run”菜单上的“Inspect”命令,Delphi将打开“Inspect”对话框。键入一个要查看的表达式,然后单击[OK]按钮,Delphi将打开“Debug Inspector”窗口,显示该表达式当前的值,如图3-16所示。
   
图3-16 Inspector窗口              图3-17  显示局部变量的值

Inspector窗口的最大特点是,它能够根据所要查看的表达式的数据类型自动按最适合的格式显示。例如,在图3-16中,要查看的表达式是Forml.Button1。由于Button1是TButton类型的对象,所以Inspector窗口就把该对象的私有数据、方法和公开的属性分别列出来。如果要查看的是一个数组,Inspector窗口就会列出该数组的每一个元素的值。
你可能已经发现,Inspector窗口非常类似于IDE中的Object Inspector,事实上的确如此。不同的是,Inspector窗口不能列出对象所能响应的事件。
使用Inspector窗口要特别注意表达式的作用域问题。如果在代码编辑器中单击鼠标右键,在弹出的菜单中选择“Inspect”命令,而当前执行点并不在要查看的表达式的作用域范围内。这时候,表达式无定义,Inspector窗口就是空白的。
在Inspector窗口上可以修改元素的值。首先,单击该元素,如果出现一个省略号按钮,表示该元素的值是可以修改的。单击省略号按钮,键入一个新的值即可。
在Inspector窗口上单击鼠标右键,将弹出一个快捷菜单,菜单上的命令有:
“Range”命令用于在查看指针或数组的时候指定要显示的范围。其中,“Start”用于指定起始序号,“Count”用于指定要显示的元素个数。
“Change”命令用于修改元素的值,相当于单击省略号按钮。
“Show Inherited”命令如果被选中的话,Inspector窗口将显示基类的成员。
“Inspect”命令将再打开一个Inspector窗口,显示所选元素的值。
“Descend”命令类似于“Inspect”命令,但不打开一个新的Inspector窗口。
“New Expression”命令用于查看一个新的表达式。
“Type Cast”命令用于强制转换表达式的数据类型。
3.7.5 查看局部变量的值
如果要查看过程或函数的局部变量,可以使用“View”菜单上的“Debug Windows”命令,再选择“Local Variables”。你不必指定要查看哪个局部变量,因为Delphi会自动把当前函数的所有局部变量全部列出来。图3-17显示了一个事件中局部变量的值。
如果有必要的话,建议使这个窗口保持在打开状态。这样,调试程序时,就能看到局部变量是怎样变化的。
3.8 调试的有关窗口
这一节介绍几个与调试有关的窗口,这些窗口可以帮助你了解当前CPU内部的状态、线程的状态、映射到应用程序地址空间的模块、例程的调用顺序以及调试期间的事件。
3.8.1 CPU窗口
要打开CPU窗口,可以使用“View”菜单上的“Debug Windows”命令,再选择“CPU”,也可以直接按Alt+F2键。要说明的是,只有在调试暂停状态才能打开CPU窗口。
CPU窗口能够显示CPU内部当前的状态,包括寄存器、标志、栈和全局数据段等,如图3-18所示。
 
图3-18  CPU窗口

要完全看懂CPU窗口,需要对CPU内部的结构有一定的了解,最好还要有80x86汇编语言的编程经验。
CPU窗口分成5个窗格,每个窗格都可以独立滚动,这5个窗格的相对尺寸是可以改变的。在不同的窗格中单击鼠标右键,将弹出不同的快捷菜单。CPU窗口的左上角是“Disassembly”窗格。“Disassembly”窗格把编译器生成的机器代码反汇编成可读性较好的汇编指令。为了便于对照,“Disassembly”窗格能够同时显示Object  Pascal源代码和汇编指令。一行Object Pascal代码可能对应着几条汇编指令。
“Disassembly”窗格分为两栏,右边一栏显示汇编指令及其操作数,左边一栏显示汇编指令的地址。如果单击某个地址,“Disassembly”窗格的左上角将显示有效地址和该地址存储的值。“Disassembly”窗格的右上角将显示当前线程的ID。
在“Disassembly”窗格中也可以单步执行程序。“Disassembly”窗格用一个绿色小箭头指向当前执行点的位置。随着当前执行点的移动,绿色小箭头也相应地移动。
CPU窗口的左下角是“Memory Dump”窗格,用于显示应用程序的全局数据段中的数据。“Memo Dump”窗格分成三栏,显示内容很象以前DOS下的Debug工具的显示内容,左边的栏显示内存地址,中间的栏显示该地址当前的值,右边的栏是相应的ASCII值,不可打印的字符用小圆点表示。
要改变中间那一栏的显示格式,可以在“Memory Dump”窗格上单击鼠标右键,在弹出的菜单中选择“Display As”命令,然后再选择一种显示格式。
“Machine Stack”窗格位于CPU窗口的右下角,用于显示应用程序栈中的数据。这个窗格也分成3栏,左边的栏显示内存地址,中间的栏显示该地址当前的值,右边的栏是相应的ASCII值,不可打印的字符用小圆点表示。绿色小箭头指示栈顶的位置。
“Registers”窗格位于“Disassembly”窗格的右边,用于显示CPU中寄存器的值,包括8个32位的通用寄存器、6个16位的段寄存器、1个32位的程序计数器和1个32位的标志寄存器。如果最近一次单步或跟踪执行后有寄存器的值被改变,就用红色突出显示。
“Registers”窗格的右边是“Flags”窗格,用于显示CPU中15个标志位的状态,值为1表示标志被置位,值为0表示标志被清除。如果最近一次单步或跟踪执行后有标志位的状态被改变,就用红色突出显示该标志位的状态。
3.8.2 FPU窗口
FPU窗口用于显示CPU中浮点单元的内容或MMX信息。要打开FPU窗口,可以使用“View”菜单上的“Debug Windows”命令,再选择“FPU”。FPU

窗口如图3-19所示。
 
图3-19  FPU窗口

FPU窗口分为三个窗格:最左边的是寄存器窗格,显示了浮点寄存器栈的内容;中间是控制标志窗格,列出了所有的控制字;最右边是状态标志

窗格,列出了所有的状态字。
寄存器窗格的上方是指令指针(IPTR)的地址、操作码以及上次执行的浮点指令的地址。
3.8.3 线程状态窗口
使用“View”菜单上的“Debug Windows”命令,再选择“Threads”,Delphi将打开“Thread Status”窗口,以显示当前进程中所有活动的线

程,如图3-20所示。
 
图3-20  线程状态窗口

除非正在调试多线程的应用程序,否则,线程状态窗口一般只显示一个线程。线程状态窗口分为以下4栏:
(1) ThreadID:显示线程的识别号,它是由操作系统分配的。
(2) State:显示线程的状态,可能是Running,也可能是Stopped。当应用程序正在等待用户输入时,线程的状态就是Runnable。
(3) Status:显示线程处于Stopped状态的原因,BreakPoint表示遇到断点,Stepped表示单步或跟踪执行,Faulted表示出现异常,Unknown表

示这个线程不是当前线程。
(4) Location:显示本线程的当前执行点所在的源代码文件名和行号。
程序一般执行很快,可能来不及看到线程的执行情况,除非事先在程序中设置了断点或者自己中止了程序运行或者程序异常中止。
在调试多线程应用程序时,线程状态窗口将显示所有活动的线程,但其中只有一个是当前线程,通常它就是应用程序的主线程。要把其他线程

变为当前线程,可以用鼠标右键单击这个线程的“ThreadID”栏,在弹出的菜单中选择“Make Current”命令,此时,代码编辑器将用一个绿

色小箭头指向当前线程的当前执行点,这样就可以调试当前线程了。
3.8.4 Call Stack窗口
使用“View”菜单上的“Debug Windows”命令,再选择“Call Stack”,Delphi将打开“Call Stack”窗口,如图3-21所示。
 
图3-21  “Call Stack”窗口

“Call Stack”窗口非常有用,它能显示例程的调用顺序以及传递给例程的参数值。其中,显示在最上面的是最近调用的例程,图3-21中就是

TForm1.Button12Click()。
“Call Stack”窗口主要用于定位例程调用。例如,如果不小心跟踪进入了一个本不想跟踪的例程内部,要想返回到调用这个例程的地方,只

要在“Call Stack”窗口用鼠标右键单击调用这个例程的例程(通常是“Call Stack”窗口中第二个例程),在弹出的菜单中选择“Edit 

Source”命令,代码编辑器将被推到前端,光标定位在调用该例程的地方(如果有必要的话将首先打开包含此例程的源代码窗口)。然后,移动

光标跳过这个调用,再使用“Run”菜单上的“Run to Cursor”命令就可以继续调试了。如果在弹出的菜单中选择“View Source”命令,光标

将定位在要找的例程上,但“Call Stack”窗口仍然是当前活动的窗口,这样就可以继续在“Call Stack”窗口中查找其他例程。
3.8.5 模块窗口
使用“View”菜单上的“Debug Windows”命令,再选择“Modules”,Delphi将打开模块窗口。该窗口显示当前所有调入到内存中的模块,包

括应用程序本身、应用程序显式或隐式调用的DLL以及运行期包、操作系统调用的DLL,如图3-22所示。
 
图3-22  模块窗口

模块窗口分为三个窗格:Module(左上)、Source(左下)、Entry Point(右),其中:
“Module”窗格列出了所有模块的名称、入口地址以及路径。
如果在“Module”窗格中所选的模块包含调试信息,“Source”窗格将显示该模块中调用了哪些单元。如果所选的模块不包含调试信息,

“Source”窗格就是空的。
如果在“Module”窗格中所选的模块包含调试信息,“Entry Point”窗格将列出该模块中所有的全局符号,否则,“Entry Point”窗格只列

出进入该模块的入口。
3.8.6 事件记录窗口
使用“View”菜单上的“Debug Windows”命令,再选择“Event Log”,Delphi将打开“Event Log”窗口,如图3-23所示。
 
图3-23  事件记录窗口

事件记录窗口能够显示调试过程中遇到的各种事件,其中包括断点和异常、进程装载或终止、调用Output Debug String和Windows的消息。
在事件记录窗口上单击鼠标右键,将弹出一个快捷菜单,其中,“Clear Events”命令将把所有的事件记录清空,“Save Events to File”命

令能够把事件记录保存到一个文件中,“Add  Comments”命令能够给事件加上注解,“Properties”命令将打开“Debugger Event Log 

Properties”对话框,让你设置有关选项。
3.9 特殊程序调试*
3.9.1 调试动态链接库
Delphi可以用内部集成的调试器来调试动态链接库(DLL)。我们将在下一章学习DLL,DLL是不能单独执行的,只能由一个可执行程序调用它。
因此,要调试DLL,首先要指定一个可执行程序,方法是使用“Run”菜单上的“Parameters”命令打开“Run Parameters”对话框,如图3-24

所示。
 
图3-24  指定—个可执行程序

在“Host Application”框内键入一个可执行程序的路径,也可以单击“Browse”按钮定位一个可执行程序,然后单击“打开”按钮把这个可

执行程序调入内存运行。接下来,就可以像调试一般的应用程序那样调试DLL,包括单步执行、设断点、开观察窗口等。
也可以通过模块断点来调试DLL。首先,要设置一个模块断点。为此,可以使用“Run”菜单上的“Add Breakpoint”命令再选择“Module Load

Breakpoint”;也可以使用“View”菜单上的“Debug Windows”命令再选择“Modules”打开“Modules”窗口,在左上角的窗格中单击鼠标右

键,在弹出的菜单中选择“Add Module”命令。此时,Delphi将打开“Add Module”对话框。输入要调试的DLL的路径,也可以按“Browse”按

钮浏览。
以后,当应用程序调用这个DLL时,程序将暂停。
Delphi能够调试其他语言如C/C++、汇编语言编写的DLL,只要DLL中含有Borland符号调试信息。如果其他语言写的DLL中不包含Borland符号调

试信息,就不能在代码编辑器中看到DLL的源代码,而只能用CPU窗口来调试。
除了DLL外,Delphi的内部集成调试器还可以调试Active X控件和OLE自动化对象。与调试DLL一样,也要首先指定一个可执行程序。
3.9.2 远程调试
Delphi支持远程调试。所谓远程调试,是指要调试的EXE、DLL或包在另一台机器上。当然,要调试的EXE、DLL或包必须含有远程符号调试信息


要进行远程调试,本地需要有Delphi的IDE(DELPHI32.EXE),当然,需要适当配置。远程机器上不需要安装Delphi,但需要安装远程调试服务器

来支持远程调试。
本地和远程机器之间必须通过TCP/IP协议连接,端口是8000。
1.本地的配置
本地的IDE需要适当配置才能调试位于远程的EXE、DLL或包。
首先,要打开“Project Options”对话框,翻到“Linker”页,选中“Include remote debug  symbols”复选框。这样,EXE或DLL将包含远

程调试信息。
接着,使用“Run”菜单上的“Parameters”命令打开“Run Parameters”对话框,翻到“Remote”页,如图3-25所示。
 
图3-25  设置远程调试的参数

在“Remote Path”框内,输入一个要调试的EXE的名称和路径。如果要调试的是DLL或包,在“Remote Path”框内输入一个调用该DLL或包的可

执行程序的名称和路径,而不是DLL或包的名称和路径。
注意:远程调试服务器应当能够找到你指定的EXE及其符号。如果远程调试服务器是作为服务运行的(在Windows NT环境中),则无法访问网络共

享驱动器。
这种情况下,你要么把EXE及其符号复制到服务器的机器上,要么在编译时把输出路径设为服务器所在机器的某个目录。
在“Remote Host”框内输入远程机器的主机名或IP地址。如果输入的是主机名,本地机器必须通过标准的Internet域名解析来获得远程机器的

IP地址。
在“Parameters”框输入要传递给“Remote Path”框指定的EXE的参数。
如果选中“Debug project on remote machine”复选框,则以后所有的IDE命令将直接针对远程的项目。如果没有选中这个复选框,则必须单

击“Load”按钮启动远程的EXE,但不打开相关的项目,这样,“Project”菜单上的命令就不会影响到远程的项目。
2.远程机器的配置
远程机器上虽然不需要安装Delphi,但需要安装一个特殊的软件即远程调试服务器,以支持远程调试。
在Delphi的光盘中有一个RDEBUG目录,运行其中的SETUP程序,将把BORDBG60.EXE安装到C:/Program Files\Common Files/Borland

Shared/Debugger目录下。这个程序的作用就是通过TCP/IP协议来连接和解释IDE发来的调试命令。
如果远程机器的操作系统是Windows 2000 Server,则BORDBG60.EXE既可以作为一个程序运行,也可以作为一个服务运行。如果远程机器的操作

系统是Windows 95/98,则 BORDBG60.EXE只能作为程序运行,并且只能人工启动。
要把BORDBG60.EXE作为程序运行并进入监听状态(Windows 95/98),可以单击 Windows的“开始”按钮,选择“运行”命令,然后键入

“BORDBG60.EXE-listen”。
要把BORDBG60.EXE作为一个服务安装(只适用于Windows 2000 Server),可以单击Windows 2000 Server的“开始”按钮,选择“运行”命令,

然后键入“BORDBG60.EXE-install”。
要移走BORDBG60.EXE这个服务,可以单击Windows 2000 Server的“开始”按钮,选择“运行”命令,然后键入“BORDBG60.EXE-remove”。
在Windows 2000 Server环境下,如果BORDBG60.EXE是作为服务(Service)安装的(这可以从Windows 2000 Server的控制面板中看出来),

BORDBG60.EXE就能够自动启动。当然,正如前面所讲的,此时的BORDBG60.EXE无法访问网络共享驱动器。因此,必须把要调试的EXE及其符号信

息(扩展名是.RSM)复制到运行BORDBG60.EXE的机器上。
3.9.3 多进程调试
Delphi支持在Windows 2000下进行多进程调试。要调试的进程可以在本地机器上,也可以在远程机器上。对于后者,需要按上一节讲的那样进

行配置。
要选择一个进程并调试它,有下列几种方式:
一是借助于项目管理器把要调试的项目加到一个项目组中,然后使用“Project”菜单上的“Build All Projects”命令编译这些项目。这样,

就可以选择一个进程并调试它。
二是使用“Run”菜单上的“Parameters”命令,启动一个新的进程并调试它。
三是使用“Run”菜单上的“Attach to Process”命令,列出当前正在IDE之外运行的进程。选择一个进程,调试器将转而调试该进程。
四是单击“Debug”工具栏上“Run”按钮边上的箭头,选择一个要调试的进程。
使用“View”菜单上的“Debug Windows”命令,再选择“Threads”,将打开线程状态窗口,显示内存中的所有进程以及每个进程中的所有线

程,如图3-20所示。
绿色的箭头指示当前的进程,浅绿色的箭头指示非当前的进程。在同一个时刻,调试器的调试命令都是针对当前进程的。要使一个非当前的进

程变成当前进程,可以在这个非当前的进程上单击鼠标右键,在弹出的菜单中选择“Make Current”命令。采用类似的办法可以使一个非当前

的线程变成当前线程,同时,这个线程所属的进程也变成了当前进程。
模块窗口能够显示内存中的所有进程以及每个进程所调用的模块,如图3-22所示。
与线程状态窗口一样,模块窗口也用一个绿色的小箭头指示当前进程,非当前的进程没有任何指示。
3.9.4 分布式调试
Delphi支持在Windows 2000下进行分布式调试,包括COM跨进程调试和CORBA跨进程调试。Windows 95/98不支持分布式调试。
1.COM跨进程调试
如果在“Debugger Options”对话框的“Distributed Debugging”页选中“Enable COM cross-process  support”复选框,则下列3个功能将

被激活:
一是跨进程依附。当进行一个远程调用时,调试器将试图接管对RPC目标的控制。如果目标进程在另一台机器上,该机器上必须配置并运行了

BORDBG60.EXE服务。从“Event  Log”窗口和“Thread Status”窗口可以看出调试器是否成功地接管了对远程进程的控制。如果该进程原先是

作为客户被调试的,则跨进程RPC的目标就是服务器。反之,如果该进程原先是作为服务器被调试的,则跨进程RPC的目标就是客户。
二是调用跟踪。“Event Log”窗口将跟踪所有的跨进程RPC调用。对于每个调用,记载以下事件:客户开始调用服务器、服务器被调用、调用

结束。每个事件包含接口标识符(IID)和被调用的方法的零基准序号。
三是跨进程单步。单步操作的流程将沿着“分布式”线程而不是实际的线程。为了避免这种情况,你可以使用“Run”菜单上的“Trace to

Next Source Line”命令(Shift+F7)而不要使用一般的“Trace Into”命令(F7)。当然,进行跨进程跟踪的前提是调试器已经成功地依附了目

标进程,调用的方法必须含有调试信息。
从“Call Stack”窗口可以看出,源线程被阻塞,直到RPC方法在目标线程中执行完毕。
2.CORBA跨进程调试
进行CORBA跨进程调试必然涉及到VisiBroker ORB。如果在“Debugger Options”对话框的“Distributed Debugging”页选中“Enable CORBA

cross-process support”复选框,则除了上述三个功能被激活外,还有两个功能被支持。
一是ORB事件断点。“Event Log”窗口将记载更详细的ORB事件,包括相关的信息,诸如操作名、事务编号、接口和主机名等。这些ORB事件可

以作为断点,使程序暂停。当然,可以附加一些条件。例如,当操作名是“X”或者主机名是“Y”时程序才暂停。
二是只跟踪的进程。有时候,调试器无法接管对RPC目标的控制。例如,RPC目标所在的操作系统不是Windows,或者该主机不支持远程调试,或

者目标进程有访问限制。对于COM来说,将不会返回任何关于RPC目标的信息。对于CORBA来说,即使调试器无法接管控制,RPC目标仍然会生成

一个事件流。这种无法被调试但能够生成事件流的目标进程称为“只跟踪的进程”。对于只跟踪的进程来说,当遇到一个ORB事件断点导致该进

程停止时,它的当前线程可以被单步执行。不过,“Call Stack”窗口不跟踪它的栈。
3.9.5 其他调试手段
在程序开发过程中,随时可能想运行一下程序,看看代码修改后是否能达到原先设想的效果,但未必一定要用前面介绍的调试器来调试程序,

因为还有更简单的调试手段。
不妨回忆一下过去开发DOS程序时的做法。要知道程序是否能执行到某个地方,可以在这个地方插入发声语句,然后编译和运行程序,如果听到

PC机的扬声器“嘟”一声,表示程序能执行到这儿。要知道某个变量的值,可以插入Write语句直接在屏幕上输出。
在Windows环境下,上述调试手段仍然可以采用,只不过具体实施办法有所改变。
下面就介绍几种调试手段,要说明的是,用这些调试手段虽然简单方便,但也在程序中留下了“垃圾”,应当及时清除掉。
调用Windows的MessageBeep()是—种常用的调试手段,它的作用就是让PC机的扬声器“嘟”—声。
可以在事件句柄中调用MessageBeep(),从而可以判断是否触发了事件。
在同一个时刻,程序中最好只有一个地方调用了MessageBeep(),如果有多个地方调用了MessageBeep(),就很难区分到底是哪个地方使PC机的

扬声器“嘟”一声。
还有一种比较简单的调试手段就是直接在Form上输出。要在Form上输出文字,有3种方法:—是在Form的画布上输出,这就要用到TForm的

Canvas属性。程序示例如下:
    Form1.Canvas.Pen.Color:=clRed;
    Form1.Canvas.TextOut(100,100,’要输出的文字’);
这种方法的缺陷在于编程稍嫌复杂,可能有的读者对Canvas属性不熟悉。

原创粉丝点击