掌握Ｃ＋＋Ｂｕｉｌｄｅｒ的调试艺术 以代码为基础的调试方法 （作者：霍炬编译） 程序的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; 在这行代码上，不要吝啬，因为加上它，基本上就可以说所有的错误都不会漏掉了。无论在任何地方发生的错误都可以被捕捉到。 现在，所有以代码为基础的调试方法你都学会了，马上把他们加入到你的工程里去吧。最好能把它们变为你的习惯。这将对你的程序有很大帮助。