C++Builder调试

掌握C++Builder的除错艺术:第一篇-正确书写代码

1.        简介

2.        书写干净的代码

3.        使用异常及异常处理能力

4.        使用记录(logging)机制

5.        结合使用记录机制与类的异常处理机制

6.        处理您代码外产生的异常

7.        你的回合

8.        版权说明

简介

这篇文章,我将从最基本的开始谈起。但希望可以涉及更广的层面,而不仅仅是为你的程序除错(debug)。你将会看到,我认为除错(debugging)这个字的全部意义,并不只是通过IDE的内建机制来运行的。我希望在这篇小小的文章结束时,几乎每个读者都可以学到至少是一件新东西,并把它藏到你的兵器库中。记住,你程序中的错误(BUG)越少,你的最终用户对你的程序的感觉就越好;你对错误(BUG)的处理越好,用户们发现错误(BUG)时就越乐于告诉你以便你改正错误。好了,现在系好安全带,戴上护目镜,让我们开始一段疯狂的路程!

书写干净的代码

首先而且也许是最重要的一点是书写干净、可读的代码是极其重要的。能够在写完一段代码后回顾一下并给它加上注释来说明这段代码用来做什么和为什么这么做,将会省去你以后跟踪代码的无数个痛苦的小时。也许你会多花一点时间来书写,但当你花过N小时来跟踪那些难以捉摸的BUG时,你就会同意多花点时间来让程序代码可读是多么值得了。(你本可以很容易完成除错的)。如果没有这么做过,我建议你停下来,读读另一篇Scott的精彩文章-代码的风格(大家需要的话,将会尽快翻译)。

使用异常及异常处理能力
现在进入下一步,这仍然是基于代码的步骤。(除了在极少数的情况下,你不能老是使用系统内建的除错器,所以知道其他可以找出这些麻烦的虫子的办法总是个好主意)。本步骤完全是关于如何做到,更重要的是处理好在你的窗体出现异常时系统扔给你的(产生的)错误。在C++标准得到认可前黑暗的旧日子里,应用程序通常会通过返回值来发出错误信号(这种方法在OLE和一些WINAPI函数中仍在使用)。很显然,你可以很轻易的忽略这些(事实上也是经常的,我的意思是你经常检查一个WINAPI函数的返回值吗?)。

所以他们决定….,okay,我们需要一个新的机制,一个你不能忽略的。但你可以处理,定制(自定义 customize)。异常就此出现了。想要一个特殊的错误类型标志?容易的很,定义一个新的异常类型(不过是一个类,没别的),抛出来(产生这个异常)。完了。

例子:

class MyException
{
public:
               AnsiString iMessage;
               MyException(AnsiString Message) { iMessage=Message;}
};
throw new MyException(“Test Exception Message”);

就这么简单!(当然不是很完全,我会很快加上的)。漂亮而又简单,并且非常容易定制来满足您的需要。Okey,你会问到:“我能产生异常了,但如何处理它们?我的意思是,我想在第一时间(位置)从我的代码中排除异常!”这当然很容易做到,实际上还很容易定制呢!标准委员会为我们定义了try {/* code */} catch (...) {/* code */ }机制,跟异常机制一样,它完全可以定制来满足您的需要!只需把您的执行代码段放在try模块中就行了,您还需要一个catch( ) 或  __finally 模块来告诉程序(如果)得到一个异常的时候作什么。现在就是你这么做的好处,你定义了一个类class类型并且输入变量来捕捉异常-通过声明catch( )。(在前面的例子中,应该是这样-catch(MyException &E) { /*在这里书写捕捉到异常后的处理代码*/})为了让这个系统更有力,你可以建立完整的子类继承树。这样当你捕捉基类时你可以捕捉所有从这个基类继承的异常类型(VCL中一个很好的例子就是所有的异常都是从Exception类继承而来的,所以catch(Exception& E) 将捕捉所有的VCL异常,当然包也括您所产生的。但EsocketError除外,见xiphias在http://www.bytamin-c.com/ 的howto (若你不喜欢E文的话,我会尽快翻译)。记住这个想法,我会在以后另一个步骤详细说明)。要让它再有力一些的话,标准委员会决定包括如下的声明catch(…) ,没错括号中就是三个点。此声明允许我们捕捉任何异常,我的意思是所有的异常。还想再有力一些?当然可以,你可以用附加的catch( )声明,跟if..else if…的样子差不多。这里要牢牢记住!如果你捕捉到了一个异常类型,那么以后就它不会被再次捕捉到了!所以先看下面的代码…

 
Try
{
               // 程序的正常运行代码
}
catch(EDBEngineError &E)
{
               // 处理基于数据库引擎的错误
}
catch(EExternalError &E)
{
               //通常处理基于windows的错误
}
catch(Exception &E)
{
               // 处理所有其他的VCL错误
}

你可以看到,这里按照 "是EDBEngineError吗? 是->处理,不是?->继续捕捉" "是EexternalError吗? 是-> 处理, 不是?-> 继续下一次捕捉" 等等… 这样的顺序排列。

接着还有更多的内容。如果你希望对某个异常做些什么,又不希望异常就此消失,你可以重新抛出(产生)这个异常。它将继续向后寻找新的catch()过程来处理它。我不能说我经常这么做。但最好应该知道,就象“抛出”一样简单。就是这样,throw将带着已经被你处理过的异常继向后寻找另一个catch来处理它。

最后而不是最不重要的 (这部分不包括在标准规范中,倒更象是Borland专有的增加版)就是 __finally 声明,使用一个 __finally{ } 模块,你可以指定不管有否异常产生都将运行的代码。这里是清除你通过new方法分配的局部变量及将所有应该设定回正常状态的标志复位(例如将一个等待状态的鼠标指针复位成正常状态)的最方便的地方。

呸,太多了!休息一下吧,有空可以看一看C++Builder帮助中的Exception类, (所有E开头的,你会注意到它们都是从Exception类继承来的。这也是定制你自己的异常类的好练习!) 当你回来时,我们将进入下一步旅程。

使用记录(logging)机制

您不可能总是使用除错器来除虫,有时你没法依靠内建除错器的力量,所以有时你将不得不求助于其他的除错手段来调试程序。(典型例子如:NT服务、ISAPI/CGI程序、实时应用程序…等等)此时您将不得不求助于我们这样经验丰富的程序员才会谈到的老式的除错/调试技术。例如产生使用某种记录(logging)机制来看看程序的头巾下面到底发生了什么的念头。幸运的是,有许许多多的现成的机制可以让我们的这项工作变得容易些。这里我将谈到我所偏爱的三种方法,你也可以将您自己的方法email给我,我会考虑加入这一部分。

Okay 先说第一种,(调试/除错输出字串)OutputDebugString。幸运的是Microsoft已经为我们实现了一个非常广泛的调试/除错子系统。包括实现您自己的调试/除错记录系统的机制。程序在一个调试/除错进程内运行的时候,OutputDebugString将它的参数(一个c string)输出到调试/除错器的输出上下文,若调试/除错器没有运行,OutputDebugString就被忽略。如果没有弹出消息的时候,OutputDebugString在终端上也可以很好的运行,当你分发给客户前别忘了移去它(通过 #ifdef DEBUG…#endif’),程序可以运行的更快一点。“Wow,又好又容易!”你也许会说“但当程序不能在调试/除错器内运行时,该怎么办?”

请牢记,这只是我的观点,基于一种观念的评价,我个人使用Gexperts的dbugint.pas界面来调试/除错。这是个非常优秀的独立的小程序。如果愿意,您可以将它分发给你的客户们。如果没有这么做,象OutputDebugString一样,如果没有安装,它实际上就什么也不做:)(它将注意终端是否已经安装在机器上)。要使用dbugint.pas的话很容易,将它加入你的工程并加上 #include "dbugintf.hpp"(因为是pascal文件,你必须将它加入你的工程以便C++Builder编译器生成hpp头文件。)然后你只需使用SendDebug(“要送到记录中的字串”);或者你也许想更灵活些,还有SendDebugEx-增加一个消息类型参数来调用TmsgDlgType(详细说明参考VCL在线帮助),SendMethodEnter, SendMethodExit, and SendSeparator 等等(十分自解释的名字)。只是别忘记加入必须的package包,若你打算将此终端(Gdebug.exe)其给你的一些最终用户的话。Gexpert可以从http://www.gexperts.org/ 获得并且是免费的。

第三种我要指出的是,这也许是最难的选择-实现你自己的记录控制台。可没有你想的那么简单!你也许首先会想到“扔个Richedit控件在form上,将它设为只读的,然后开始记录,对吗?” 错!理论上挺好,但实践呢,使用RichEdit控件来记录将降低程序的运行速度、使内存破碎,丢失、通常会在10分钟内使整个机器慢下来!!(要说明白为什么得花上点时间才行,但我向你可以保证)。所以你所需要的是计划好你的记录机制的需要,并开始计划一个定制控件若你想要个彩色的图标的话。还有一个选择,需要做点工作,但可是非常有效。就是使用一个ListBox控制来记录,并将Style属性设为lbOwnerDrawFixed,这样句柄将会自绘。(这也是Gexperts和它的Gdebug console所做的)。要做许多工作,但哈哈,如果你想做…

结合使用记录机制与类的异常处理机制
现在进入下一步:)(跟你打赌你从未意识到设置一个优秀的调试/除错系统需要做如此多的工作!)你不用总是预料各种偶然的异常会发生什么,而且绝大多数时候当程序经过大量的除虫测试(尽量攻击程序,试图让它崩溃)后,你根本不用担心这些。下面这个技术,我建议任何组件开发者第一次在IDE中测试一个新组件/新代码时应该完全遵照。因为在IDE中一个异常会带来很多问题,有时甚至重启IDE也无济于事(我自己已经这么做了)。其实也很简单。在您代码的每个函数前,或者至少在所有主要的函数前后加上:

try 
{ 
前端代码
}
catch(Exception &E) 
{ 
SendDebugMessage(“Exception caught in classname::functionname of type:” +E.ClassName()
+” with the message:”+E.Message);
};

(并用函数的类名及函数名代替字串中的classname和functionname)。这样你很快很快就知道异常发生在何处,也不用你强行关闭IDE啦。

Okay,是时候回顾一下了。ClassName()方法是如何帮助我们的?不想每次都只得到一个“Exception“串就完了吧?难道是因为将E声明为一个异常?不对。这是VCL比较酷的部分,任何从Tobject继承来的类能够自动知道其自身的类型、其基类的类型、等等许多有趣的信息,你可以察看Tobject的帮助。所以尽管我们使用的是Exception &E,E.ClassName()将会找出我们得到的异常的实际的类名(译者注:C++的多态性)。这些好处的代价就是可执行文件的体积更大了,几乎所有的C++Builder/Delphi程序员都会发现这一点。(No Pain, No Gain)没有痛苦,就没有收获.他们说….

Xiphias增加了TstringList的AddingLine方法,SavetoFile方法是另一种记录(logging)的有效形式。最后应该保证你的应用程序总是写记录文件(logfile),或这每次捕捉到异常时重写记录文件。

处理您代码外产生的异常
现在的步骤是我们开始学习基于IDE的除错器之前的最后一个基于代码的步骤。但也许在有严重错误发生时,对装饰应用程序来说这是最重要的步骤。举例来说,这是显示一个包含错误详细内容的对话框理想的时机。这时弹出在屏幕上的对话框可以方便最终用户能够向您报告错误。我敢保证您痛恨“oh,有个什么框子上说在什么地址发生了个什么异常错误”这样的报告。其实完全可以很容易的实现更好的情况,也不会限制你打算如何处理它。第一步是在你的主窗体(例如:工程的自动创建窗体列表中的第一个form)中创建一个象如下这样的函数:

void __fastcall AppLevelExceptionHandler(TObject *Sender, Exception *E)
{
}

然后加入合适的代码来显示错误(E->Message),错误类型(记住E.ClassName(),只有此时才是它的E.ClassName()),和联系您的详细方法及其他你想加上的任何东西。第二步当然是将它与系统挂钩,这在C++Builder里很容易实现:

Application->OnException=AppLevelExceptionHandler;

将上一行代码加到form的 OnCreate 事件中。不要吝啬!你加了这一行后几乎可以保证不会错过任何异常,而且无论哪里异常处理失败时它都会出现在你的眼前!

你的回合
现在你已经得到所有你刚才学习的有用的信息了。是时候开始把它们加到你现在的工程里去了,否则就忘掉吧,要不然,就把它变成编程习惯的一部分。这是你的自由!

在这个系列的下个部分,我将讨论内建除错器的使用,来看看你的程序运行时都干些什么,如何单步跟踪代码、设置断点、察看变量、和会把新手们吓的人事不醒的所有其他有趣的工具。直到这里,您的bugs也许只是小虫子了吧。

 

原著:Bill King

翻译:史平洋  CKER

 

掌握C++Builder的除错艺术:第二篇-近距离观察

1.        调试可执行程序前的准备

2.        工程选项

3.        设置断点并闯入可执行程序

4.        察看储存在变量中的值

5.        使用Watches(观察)

6.        使用Inspectors(巡视器)

7.        使用Evaluate/Modify(求值/修改)

8.        Stepping Through, Over and Around Blocks of Code

9.        Stepping的类型

10.   Stepping的注解

11.   其他提示

 

Okay,(再小小准备一下)现在开始追踪、搜索经过前次的努力后仍然躲在代码中的bug的时候了,也就是开始跟踪前一篇文章代码里标记过的bug/异常。首先是准备阶段。

调试可执行程序前的准备

在我们开始调试可执行程序前,我们需要确保一些设置在大多数情况下的正确性。我将会一条接一条的过一遍,并简单解释一下为什么必须那样做。(如果您对有些东西感兴趣的话,按下帮助按钮,会有许多更详尽的内容)。现在就开始吧,先打开Project|Options选项。

工程选项

首先我们在"Compiler"(编译)标签处停下。您只需简单的单击"Full debug"(完全调试模式)按钮,我们所需的绝大多数的其余设置就已经搞定了。将"Code optimization"(代码优化)设为"None"(无)总是件好事,这样做实际上告诉编译器:所有的事情都已做好,只需产生机器码就行了。而不要为了提高一点点运行速度尝试进行其他的智能优化。(当然,一切都完成之后,您可以打开此项。)这样做的好处是大大降低了我们调试的难度。因为程序中的代码与我们书写的一样,没有被编译器优化过。在"debugging"(调试)面板中,将"Debug information"(调试信息)选上(点一下),并且必须设置为"Line number information"(行数信息)。我还建议将"Disable inline expansions"(禁用内联扩展)一项选上。内联扩展对发布的代码来说很好,但调试时最好还是关掉此项,他只会让您更头痛。

然后是"Pascal"标签,尤其在您的工程里连接了Pascal单元或使用了基于Pascal的VCL控件时(若您拥有其Pascal源码时,编译器会自动使用此节中的设置重新编译)。这里您必须将"Optimization"优化选项禁用,然后通常我会将"debugging"(调试)部分的所有选项选上(打钩)。

接下来是"Linker"(链接)标签,我们需要选上"Create debug information"(生成调试信息)。"Use dynamic RTL"(使用动态RTL)以及"Don’t generate state files"(不要生成状态文件)是造成麻烦的选项。我通常都会使用状态文件(这样允许增量链接,但会在编译目录下产生一个4倍于可执行程序或更大的文件),换个角度来说,这样会增加链接大工程时的速度。而使用dynamic RTL本身就是个争论,尚有很多赞同和反对的讨论。

下一个是"Directories/Conditionals"(路径/条件)标签。在这里我们想要设定"Directories/Conditionals"(调试源路径)的值。我们永远都应将此处设定为$(BCB)/source/vcl,但是如果您有任何其他的组件附加的话,通常将它们的路径也加上是个好主意(路径与路径之间用”;”分隔或者您可以用按下…按钮弹出的对话框来设定它们)。

最后也是最重要的设置是在"Packages"(程序包)标签上。根据所有恰如其分的调试经验您必须禁用"Build with runtime packages"(带运行时程序包编译)。这么做的原因是程序包本身不包含而且不能包含调试信息。这样做,也许不利于您跟踪标准的VCL代码,例如想看清楚VCL函数y中参数x是如何起作用的时候。但是大多数时候,您这么做将会发现调试器将您的绝大多数“症状”归结给VCL,尽管“病因”就在您的源代码中(或在其他的组件中(这已经在我们所有人身上发生了))。一旦您发布您的正式版本时,您可以决定是否使用程序包。(译者注:程序包的本质是一个特殊的DLL,不带运行程序包(静态)编译可以让您的程序脱离Cbuilder独立运行。),但在调试时,请禁用掉。按下OK按钮,我们已经准备好啦。下一个对话框只需打开一次,但最好还是来检查以下我们在这里的设定是否正确。好了,打开”Tools|Debugger Options…”吧。

对话框最下方的"Integrated debugging"(集成调试器)选项是关键所在。确信已经打上钩。按下OK按钮准备编译可执行程序吧。我建议重新来一次彻底的编译(选择Project|Build All),如果您修改过您的设置的话(尤其是改变”building with packages”方式后)。这将保证我们的所有程序单元按照我们所希望的那样被编译。

设置断点并闯入可执行程序

象您所见过的其他任何一款调试器一样,C++Builder提供强大的断点设置功能。基本上,断点是指代码中的一个点,程序执行至此停下(与退出不同,这只是执行中的暂停)并将控制权交还给调试器。设置一个断点相当容易。只需在您想要设置的程序代码行左侧的灰色槽形区域点击,您会看到一个红点出现,这一行也会变红。程序运行到这一点就会暂停,将控制权交还给调试器。

您也许会问如果我不想每次都停下来呢?当然可以,而且还很容易做到,这取决于您暂停程序的标准是什么?(译者注:条件断点)。在刚才那个断点(红点)上右击鼠标并从弹出菜单上选择” Breakpoint Properties”(断点属性)。此处可以设定两种属性"Condition"(条件)和"Pass Count"(通过次数)。Condition(条件)属性太方便了。您可以利用if()语句输入几乎是任意的条件。但请牢记条件中的所有变量,对此断点都应是可见的。条件属性并未被编译器编译到执行程序中,而是在运行时,当程序运行至断点暂停后,检查断点的条件是否满足。条件为真,停下,否则让程序继续运行。另一个属性"Pass Count"(通过次数)也很容易理解。断点将被通过Pass Count次后停下。结合使用这两个属性,在调试您的代码时,您可以设定非常严格的断点。

还有一件要牢记的是,当您在调试器中发生异常时,会以产生异常处的那一行代码上的断点的形式出现。这种情况很容易制造。一旦您得到一个异常后应做的步骤我会在以后展示如何在堆栈中回溯并跟踪找出异常发生的真正原因(如引起异常产生的那一小片代码)。

另一个要牢记的提示是当您运行您的程序时,代码窗口左侧有蓝点的任意一行都可以设成断点。所有非法的断点将会变为红点中带一个黄色的小叉,这一行代码也会变成黄褐色。合法的断点则变为红点中带一个绿色的小钩。运行时,您可以设置/修改任意一点,断点立即生效而无须重新编译。

察看储存在变量中的值

一旦您的程序在您的断点处停下后,该做什么?有一件事您想做而且必须做的,那就是察看储存在您程序中的各种变量真实的值。这部分内容涉及的方面很多,您一定要坚持,忍受这些枯燥的东西。幸运的是当您看完这些,您一定会对调试器这部分最强大的功能有些新的理解。有许多种方法可以察看变量的值,主要要根据您的目的来决定。我会从察看当前函数的Local Variables(局部变量)开始把他们都讲完。

察看局部变量没有太多可以讲的。只需点击”View|Debug Windows|Local Variables”,或按下ctrl-alt-L将会弹出一个窗口,显示了当前函数的局部变量。窗口中的变量将会随您单步向下执行或回溯的函数体的更新而更新。

使用Watches(观察)

下一步您可以通过设定一个variable watch(变量观察)来察看程序中的变量。就象它的名称所表达的,观察一个变量并将其值显示在变量观察窗口中(点击"View|Debug Windows|Watches"或按下 ctrl-alt-W)。您可以通过两个途径来添加一个观察,第一种是在代码窗口中高亮选择您要观察的变量或表达式(是的!它可以理解并对绝大多数简单表达式求值,比如(i*j)+05 或者 SomeVector[i].Name)并右击鼠标,选择"Debug|Add Watch at Cursor"或按下ctrl-f5,就会加入观察窗口。如果必要,同时会打开观察窗口

您还可以通过在观察窗口的空白处双击来添加。这时会弹出添加watch对话框,"Expression"(表达式)域的意思无须多说,但另几个域我想解释一下,它们也同样方便。

"Repeat count"(重复值)用于您观察一个已知长度的数组变量(比如一个blah[50]数组)。您要将Expression(表达式)设为数组的名字(本例中是blah)。"Repeat count"设为数组的元素数量(本例中是50)。然后就会显示数组的每个元素(如:blah[0], blah[1], blah[2]…)。

"Digits"(小数位数)用来设定显示十进制浮点数的小数位数的。下面的点选集合是用来强制设定变量的显示类型的(将无符号长整数显示为十六进制格式)。还有一点要特别说明的是,如果您在watch窗口中用鼠标右击一个watch后的弹出菜单上会出现"Break When Changed"的选项,这将在变量上设定一个断点,在此变量发生变化时会暂停程序。

使用Inspectors(巡视器)

巡视变量是察看变量中的数据的第三种办法。也几乎是观察完整的类的数据的最佳方法。可以有两种方法来巡视一个变量。第一种是在local variable window(局部变量窗口)中,双击一个变量,将会弹出"Debug Inspector"(调试巡视器)窗口,里面显示了这个变量所有的"Data" (variables) (数据(变量))、"Methods" (functions)(方法(函数))和"Properties"(属性)。如果这是个简单数据,将会显示此变量的名称及其中的值。(译者注:如果是数组呢?真不错!)

您会注意到,Debug Inspector(调试巡视器)很象property editor(属性编辑器)。当然如此,更加重要的是,事实上您可以在运行时实时改变这些值!!!小心使用啦!改入坏值的结果会让您有说不出来的悲痛。巡视器的这个能力可用来快速测试(假设的)游戏关卡(译者注:好像FPE,GM),而不用有编译-运行-修改-编译-运行的循环。

(举例巡视Form1)在properties(属性)页上,您将会看到某些属性实际上并没有显示其的值,而是显示了{read=,write=}。如果这些值可以被赋值的话,当您在此区域单击后,您会注意到一个"?"按钮出现在属性的右侧。单击这个按钮将会系统执行适当的函数来尝试取回属性的值。我们可以在这儿举个例子-就举Form1的MDIChildCount的属性吧。在MDIChildCoun的属性值区域上单击,在按下"?"按钮,哇,0(正是非-MDI的程序的指定值)。调试巡视器强大的能力并未到此为止。在巡视器的成员变量的适当区域双击可以打开成员变量的巡视窗口,提供与您开始打开窗口一样的能力。

巡视器窗口的另一个有用的功能是从对象继承的能力。这可以在通过在适当区域上右击选择"Descend"(继承)来做到。继承的结果是产生了一个新的变量。您会注意到顶部的下拉List box中的变量名称已经换成新的变量名了。您可以直接在ListBox中切换巡视的变量。这使得在对象的不同部分快速切换变得非常简单,而不会让大大小小的巡视器窗口扰乱您的工作空间。

有一点要牢记的是,如果您离开函数,或者离开变量的作用范围,调试巡视器会失去对变量的跟踪。若您需要再次察看的话,请重新设置巡视器。但是您在当前函数的代码中单步运行的话,巡视器会自动刷新。

 使用Evaluate/Modify(求值/修改)

最后一种显示变量或代码块的值的途径是Evaluate/Modify(求值/修改)窗口。这个窗口可以象打开巡视器一样打开,在您要Evaluate/Modify(求值/修改)的代码行上右击选择"Debug|Evaluate/Modify"就会弹出Evaluate/Modify(求值/修改)窗口。本窗口用来对表达式/变量求值并/或修改。Watches和/或inspectors也可以实现同样的功能。但如果您想要修改的话,这里恐怕是最好的地方。

“察看变量已经听够了,现在我想去看看我的代码到底怎么了,而不是干坐在这里。”我听见您如是说。下一节我们将在函数体内外单步跟踪直至断点(还记得前面的东西么?)。

在代码块内外进行Stepping(单步执行/跟踪)

在此有一件事要牢记,前面讲的绝大多数察看变量的办法是动态更新的。所以当您跟踪至新的一行的时候,变量的值会被自动重新求值,并且显示在窗口中的值被更新成新的当前值。

Okay,当您点击您所设置的断点时,您会看到您所需检查的变量。下一步就是在代码中单步执行并且近距离观察实际发生的与您所猜测的是否一致(通过使用watches/inspectors并跟随代码的执行路径前进)。

Stepping的类型

这部分解释起来似乎很简单。但下面的大部分都是来讨论您用这些能做什么(好像在别处没有听到过?)。您可以告诉调试器五种"stepping types"(单步执行类型)。我们会按Run菜单上的顺序来解释。我同时会给出它们的快捷键。在调试时使用快捷键的次数会多过使用菜单选项的(比较按10次键盘与选10次菜单,我想您知道什么更好)。

第一种是"Step Over"(在函数体外单步执行)或者f8。这会让调试器执行代码至当前函数的下一行可见的代码停下,或者当运行至当前函数的最后一行时,调试器返回至调用函数停下。在您知道所调用的函数没有问题时,stepping over功能很方便。

下一个是"Trace Into"(跟踪至函数体内)或f7。如果运行的当前行是调用一个函数,调试器将把我们带到调用的函数的第一行(即使是隐含调用函数,例如一个write属性)。否则执行至当前行的下一行。当您想看看这个函数到底做些什么时,这个功能非常方便。

接下来是"Trace to Next Source Line"(跟踪至下一个源代码行)或shift-f7。调试器会运行至下一个具有调试信息的代码行。这与"Trace Into"的区别,让我们举例来说明吧。当我们调用一个没有源代码的windows API函数并且这个API函数调用了我们代码中的一个回调函数。"Trace to Next Source Line"将在回调函数的第一行停下来,而"Trace Into"会忽略这个回调函数并在当前程序的下一行停下来。

然后是"Run to Cursor"(运行至光标处)或f4。调试器将运行代码直至光标所在的行。这使得不需设置断点又可以跳过大片代码变得很方便(考虑只需停顿一次的情况)。

还有"Run until Return"(运行至返回)或shift-f8。调试器将运行代码直至当前函数返回调用他的函数。当您不想手工单步执行至函数结束(假如您陷入一个循环中的话,这个过程会变的十分冗长)时,这么做就会很方便的跳到函数的结尾处。

最后一个是"Program Reset"(程序重置)或ctrl-f2。调试器会中断已运行的程序并返回至调试器。除非迫不得已不要这么做,因为对象所使用的资源没有被释放!!!(在一个数据库应用程序中,这样做2-3次后,您将不得不关闭并重启IDE,因为BDE内部资源将耗尽。您已经被警告啦)。当您不得不中止可执行程序时,这非常有用。

最后(却在菜单很上面的位置)是"Run"(运行)或f9。调试器将运行至程序结束,除非碰到您点击新的断点或发生异常。

Stepping的注解

要牢记的是所有这些单步执行方式在调试器通过断点时,调试器会对断点求值,若断点需要就停下。如果出现异常,调试器也会停下。

正如您所见,在正确的地方设置断点,察看变量并在代码中单步跟踪,我们能够以非常近的距离观察程序在做什么就好象它真的运行一样。这些功能在追查最困难的bugs---逻辑bug时,相当方便。

其他提示

如果您真的很勇猛并有汇编语言的经验,您可以点击"View|Debug Windows|Cpu"打开一个"cpu view"CPU窗口。这里不仅显示了当前可执行程序的指令的汇编指令,还有象CPU标志、寄存器的内容和不断更新的内存印象

使用Call Stack ("View|Debug Windows|Call Stack")可以很方便的找出发生异常之前或遇到断点之前的函数调用历史。记住,这里只是显示什么函数被什么函数调用过,并非真正的调用过程的历史记录。因此,这个窗口可能先会让您感到困惑。但是只要开着它来stepping through单步跟踪代码至函数体内,好好观察一下您就会完全明白了。

C++BUILDER4、5中,watch, local variable, 和 call stack窗口可以驻留在code主窗口中,使用起来更加方便。C++BUILDER5中还可以设定调试布局,以便在调试时使用(针对您在设计时使用的设计布局来讲的)。

使用所有我所公布的所有技术再加上一点点耐心和一双火眼金睛,您应该可以解决99%的bug。若您想找到bug,耐心是必要的。花点时间,深呼吸一下,不要让挫折吓倒。如果有必要,走出去5分钟干点别的,您会惊奇的发现自己有了新的视野或者发现开始时漏掉了什么步骤。

如果您有其他的技巧和提示觉得应该加入此文,请尽管来信告诉我们,我们会在以后的版本中加入您的意见。

好了,我希望这篇文章在某种程度上有助于耐心的读者们。如果正是这样,我会十分高兴自己干了件不错的工作。也许您所有的bugs都已微不足道了。

原著:Bill King

翻译:史平洋 - CKER

转自:

http://blog.csdn.net/cker/article/details/4192

http://blog.csdn.net/cker/article/details/4196

参考:

BCB程序调试技术:http://wenku.baidu.com/view/a2dc51543c1ec5da50e270dd.html

 

posted @ 2013-05-14 10:58  金石开  阅读(3267)  评论(0编辑  收藏  举报