C++ builder的除错艺术,让你更好了解C++

来源:网络时间:2013-01-06

  简介

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

  书写干净的代码

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

  使用异常及异常处理能力

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

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

  例子:

  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…的样子差不多。这里要牢牢记住!如果你捕捉到了一个异常类型,那么以后就它不会被再次捕捉到了!所以先看下面的代码…

  你可以看到,这里按照 "是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)其给你的一些最终用户的话。

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

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

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

  (并用函数的类名及函数名代替字串中的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)中创建一个象如下这样的函数:

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

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

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

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

发表评论

最新评论(共2条)

2345市网友

写得好,有水平

2011-09-25 0

回复@2345网友:

  • 取消

2345市网友

高手写的太好了

2011-07-28 0

回复@2345网友:

  • 取消