代码为基础的调试方法
程序的bugs越少,最终用户对这个程序的评价越高。而开发人员事先对bugs的处理越多,最终用户能提供的关于bugs的信息就越多,也越准确,这样,开发人员在接到最终用户反映之后,就能够快速找到出现bugs的那部分代码,并以最快速度发布程序的升级包。
在这份教程中,我们从最基本的部分开始,逐步介绍许多在调试程序时“应该做”或“不应该做”的原则。正如你将看到的,这份教程中所指的“调试”这个词所包含的意思很多,而不只是如大部分人所想到的--利用IDE集成的调试器的“调试”。我希望读过这份教程之后,读者可以在思路上有所收获。
写易读的代码
第一点,大概也是最重要的一点,就是写干净易读的代码。易读的代码是很有价值的。请想象一下,如果随便扫视一眼代码或注释,就能立刻知道这段代码的的作用,以及在写代码的时候为什么要这样写,当时的思路是什么,那么就可以节约大量时间。这样的代码,在写的时候可能会稍稍慢一些,不过,当你调试程序时,就不会花上几个小时来寻找bugs,相反,你可以快速,简单的完成除错工作。这时,你就会觉得多花一些时间使程序易读是很值得的。
所以,我推荐你在写程序的时候,应该养成自己的风格,或是读一读Scott的关于代码风格的文章。
使用Exceptions和Exception的处理方法
我们教程的下一步,仍然是以代码为基础的。因为除去一些少数的情况,开发人员不可能总是依靠于集成的调试工具。所以,学会用其它的方法来找到烦人的bugs是很重要的。一些重要的、处理的错误可能会在窗体之外发生。在C++标准制定出来之前的黑暗日子里,在程序里面发出发生错误的信号,通常是通过返回错误代码完成的(现在这种方法仍然应用于OLE技术和一些Winapi函数),这样的处理方法很容易就会被忽略。(比如说,你经常检查winapi函数的返回值吗?)所以,出现问题的可能性并不小。由于以上的原因,我们需要一个这样的机制,它能让我们不能忽略这些错误,而且,这个机制应该能被我们控制和自定义的。在这样的需求下,异常处理机制出现了。需要一个特殊的错误类型吗?简单,定义一个新的异常类型就行了(和定义一个类的方法差不多),然后抛出(throw)它。下面这个例子说明了这一过程。
例1:
//----------------------------------------------------------------
class MyException
{
public:
AnsiString iMessage;
MyException(AnsiString Message) { iMessage=Message;}
};
throw new MyException(“Test Exception Message”);
//---------------------------------------------------------------
就是它!(不是十分好,下面我们会继续完善它)。简单高效,而且便于自定义。也许你现在会问:“我可以使抛出异常了,但是,怎么控制它们呢?我的意思是,我想在代码的最前面排除异常。”C++Builder为我们中定义了try {} catch (...) {}机制。这和我们刚刚定义的异常机制的结构很相似。这个机制完全可以按照需要自定义。要使用异常处理了,只要把要执行的代码放到try块里面,为了让程序知道出现异常后应该做什么,还需要定义一个catch()或是__finally块。catch()语句里面可以指定一个要捕捉的类型或是变量(比如例1,就是catch(MyException &E){ /* 异常处理代码/}这个机制很强大,甚至可以用它来捕捉树结构或是继承类的异常,如果捕捉了基类的异常,它就能捕捉到继承这个基类的所有的类的异常。比如,在VCL中,所有的异常都是继承于Exception类。所以,catch(Exception& E)可以捕捉到除了EsocketError的所有VCL异常。(这点请特别注意,以后还将继续讨论。)为了让这个机制更强大,C++Builder中还定义了catch(…)语句。(没错,就是三个点)使用这条语句可以捕捉到所有的异常。还有更多的功能吗?当然,你可以添加更多的catch()语句,可以向使用if...else if...语句那样使用它。注意,在一系列的catch()语句中,错误不会被重复的捕捉,也就是说,如果前面的catch()语句捕捉到了错误,后面的catch()语句将不会捕捉这条错误。
例2:
//----------------------
try
{
// 正常代码
}
catch(EDBEngineError &E)
{
// 处理数据库引擎错误
}
catch(EExternalError &E)
{
// 处理窗口类的错误
}
catch(Exception &E)
{
// 处理所有的VCL错误
}
//----------------------
请看例2,它的代码运行流程是这样的:“错误是EDBEngineError吗?是->处理它。不是->运行下一个catch语句”“错误是EExternalError吗?是-〉处理它。不是-〉运行下一个catch语句”等等。
这个机制还有更多的功能。如果你想处理异常,但是不想在处理的位置停止,那么可以重新抛出异常。这时,程序将继续寻找下一个catch()语句来处理这个异常。这个方法和“throw”差不多。这样,你处理过的异常会再次被抛出,继续寻找下一个catch语句来处理它。
最后一个要说的是__finally(这不是标准的用法,是Borland添加的一个好方法),在__finally{}程序块中代码,无论是否发生异常都会被执行。这是一个清理程序中使用new分配的本地变量,设置用作旗标的变量值为正常的好位置。(比如,把一个等待状态的光标图标设置为正常光标。)
就是这些了。有时间的话,请看看C++Builder帮助文件中的Exception类以及继承Exception的类。这些将对于理解本节所说的内容有很大帮助。
使用记录机制
你不可能总是用调试器来调试代码,在某些情况下,可能无法使用内部集成的调试器,这时候,你就不得不依靠其他手段调试程序了。(比如:Windows NT服务程序,ISAPI/CGI程序,实时应用程序等等)。这时候,有经验的程序员可能会借助古老的调试方法,例如,使用一些分类的记录机制来确定程序实际运行的过程。我们很幸运,现在有一系列的方法可以简单的完成这样的工作。下面将介绍3种我最喜欢的方法。
第一个:OutputDebugString。(WinAPI: VOID OutputDebugString(LPCTSTR lpOutputString);)很幸运,微软彻底的实现了调试子系统。它包括的一些特点可能让你想把自己的记录系统扔掉。应用程序在调试器进程中运行时OutputDebugString将用C字符串把调试器输出的信息打印出来。如果程序没有在调试器进程中运行,它将忽略这些调用。它会很好的在客户的机器上运行,不会弹出信息窗口。如果在发布给客户的时候,忘记去掉这些代码程序仅仅会变慢一点,不会有别的不良后果。
第二个方法:使用了GExperts,通过 dbugint.pas接口进行调试。它是个可以称之为伟大的程序,你可以把它分发给客户。和OutputDebugString一样,如果客户没有这个程序,它就根本什么也不作。(它会自动检测机器上是否安装了客户端)。要使用dbugintf,它很容易被加入到你的工程中,加入#include "dbugintf.HPp"(要把它加入工程,然后会编译它的pascal文件)。然后,你就可以直接使用SendDebug(要送到记录文件的字符串); 或者,你需要它更机警一些,可以使用SendDebugEx(它给TMsgDlgType增加了一个新的消息类型)SendMethodEnter, SendMethodExit, SendSeparator等等(用法都差不多)。如果你打算给最终用户分发客户端 (Gdebug.exe),不要忘记include所需要的程序包。GExperts可以在http://www.gexperts.org 得到,它是免费的。
第三个,大概是最艰苦的方法,就是使用你自己的记录控制。这个方法可能不是你想象的这么简单。你可能首先会想到“在窗体上扔一个RichEdit,把它设置为只读的,然后往里面写记录”是这样吧?理论上不错,但是,实施起来...首先,使用RichEdit控件来做记录,会大大降低应用程序的速度,还会在内存中造成碎片,甚至丢失内存。通常,在运行10分钟左右之后,会使整个计算机的速度变慢!(这样做简直是在犯罪!)所以,如果你希望在自己的记录中能够使用彩色和图标,那么最好自己创建一个组件。如果没有这么高的要求,那么有一个简单有效的方法,就是使用ListBox控件作记录,把ListBox的Style属性设置为lbOwnerDrawFixed,这样句柄将会自绘。(GExperts的控制台就是用这样的方法制作的)。
将记录和异常处理结合使用
你不用总是担心可能会发生什么偶然的异常。一般来说,通过很多的bugs测试后(尽量折磨程序,看看它会不会崩溃),应用程序在运行是应该不会出现什么错误。下面的这个技术,建议组件开发者,在第一次把组件放在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的了。
现在,让我们看看前面的内容, ClassName()给了我们什么样的帮助呢?它只是用于返回字符串“Exception”吗?每一次,E都被声明为异常类型?这是VCL另一个优秀的地方,所有的类都从TObject继承,所以,这些类都能自动获得正确的类型和基类的类型,所有的更多的信息都可以在这里找到。(请参见TObject的帮助)所以,尽管我们使用了Exception &E,其中的E.ClassName()将返回捕获到的产生异常的实际类名。得到这些好处需要付出的代价是编译出来的可执行文件变大了一些。所有的Delphi/CBuilder用户都注意到了这一点,但是他们说,没有付出就没有收获。在http://www.bytamin-c.com/的howto栏目可以看到Xiphias的一系列文章。其中,他提到了使用TStringList作记录的方法:先将错误通过TStringList的Add方法加入到StringList里面,然后使用SaveToFile保存到硬盘上。不过要注意在程序结束的时候不要忘记使用SaveToFile()方法把TStringList保存起来。或者,也可以每次捕捉到异常之后都保存一次。
在代码外部进行异常处理
这是使用代码处理异常的最后一节,以后,我们将使用IDE的调试工具。但是,这节中讲到的方法在处理致命错误时是很重要的。比如说:可以显示一个包含了错误信息的对话框,这样,用户报告bugs的时候会清除的多。你肯定不想听到用户报告说:“啊,这有个对话框,上面写着什么地址发生了错误”。有个好办法能改善这种状况,请往下面看。
第一步:在主窗体(工程设置里面,自动创建的第一个窗体)中创建这样一个函数:
void __fastcall AppLevelExceptionHandler(TObject *Sender, Exception *E)
{
}
然后,在里面加上显是错误(E->Message)的代码,错误类型(记住前面提到过的E.ClassName())以及其他需要的细节信息。
第二步:把它与系统挂钩。很简单,只需要在窗体的OnCreate事件中加入这行:
Application->OnException=AppLevelExceptionHandler;
在这行代码上,不要吝啬,因为加上它,基本上就可以说所有的错误都不会漏掉了。无论在任何地方发生的错误都可以被捕捉到。
现在,所有以代码为基础的调试方法你都学会了,马上把他们加入到你的工程里去吧。最好能把它们变为你的习惯。这将对你的程序有很大帮助。