Windows程序调试----第三部分 调试技术----第12章 非常规策略

来源:互联网 发布:淘宝商品过期说明什么 编辑:程序博客网 时间:2024/06/09 13:45

12章非常规策略

    当你在调试中陷入困境时,有时候必须采取一些非常规的手段。本章将要介绍一些调试技术,当你正常调试过程不能成功解决问题时可以尝试使用这些技术。

    首先,我想给我所谓的“非常规策略”(desperate measures)个准确的定义是很重要的事。我们先来说说非常规策略不是什么:非常规策略不等于最后的方法。这些技术不是当你放弃了所有的希望、不能想出其他任何办法的时候才能使用的。相反地,当你觉得需要从常规的调试过程中抽身出来,你可以试试这些方法。例如,最先给出的两个建议是重启Windows和重新编连程序。这两个技术不是常规的,因为你不会每次发现一个错误就去重启Windows和重新编连程序。在调试的时候,如果Windows或者其他程序开始表现出异常的或出人意料的行为。我会立刻停止手上的工作,重新启动Windows:这样经常就能解决问题。所以,重启Windows显然不是最后的方法;实际上,这可能是我最先做的事情之一。

    当你陷入困境的时候、可以考虑这些非常规策略,但记住这不是最后的方法。

    Desperate在字典中的一个定义是采用极端的手段试图逃避失败和挫折。如果你把“极端”理解为不寻常的朱西,这个定义就适用于于这里了。当错误或其环境中有什么东西使得正常的调试方法不能奏效的时候,你必须使用非常规的手段。这些情况下,错误的原因可能根本不在你的代码内部。这个原因可能在代码的外部,例如系统配置或者硬件的问题。有时候错误原因是Windows自身的一个错误;有时候问题是某种系统崩溃或系统资源耗尽。少数情况下,所谓的错误甚至可能不是错误,程序运行正确,但并不像你期望的那样,而你的期望是错误的,在这样的情况下,你可能浪费了大量的时间在你的代码中跟踪这个错误,而最后发现错误并不在代码中。

    提醒大家注意:在把错误归咎于你的代码之外的原因之前你需要有一个很好的理由,正如第1章“调试的过程”中所说,有的程序员遇到程序错误时过分急于抱怨他们的同事、编译器甚至Windows自身。先不要抱怨或者否认。虽然错误的原因有可能是外部因素,但是先假设问题出在你自己的代码中总是比较谦逊并且有作用,在找到足够的可以说明问题不在代码中的证据之后再怀疑其他因素也不迟。如果你只是快速地检査了一遍代码没有发现错误,这样的证据不够有说服力,不过,一旦你开始搜集那些可以证明错误不是由代码引起的证据,使用本章介绍的方法会很有用,可以节省你大量的时间,并少走很多弯路。

12.1 检查简单的东西

    下面这些事很容易做,而且不需要什么理由。当你对自己说“等一等,这样做没有意义”的时候,就可以考虑使用这些方法了。

重启Windows

    当你发现Windows或者其他程序表现出异常的或出人意料的行为时,就应该重新启动WindowsWindows系统可能崩溃了,或者资源耗尽了。这个的原因可能是你的程序中的一个错误,或者其他程序中的错误,甚至Windows自身的错误;不管问题或其原因是什么,都应该重启Windows以消除操作系统给调试带来的干扰。

从头重新编连你的程序

    虽然Visual C++在编译需要编译的模块方面干得很漂亮,但有时候事情会乱套,这时候你的程序应该从头重新编连,例如,Visual C++的编辑并继续(Edit and Continue)特性有的时候可能导致问题,因为它不执行任何pre_Linkpost_build的步骤。如果你的程序要求这些步骤,这样做就会导致错误。当你的程序表现出异常的或者意外的行为,或者Visual C++编译器因为一个内部编译器错误而失败的时候,最好从头开始重新进行编连。例如,程序可能在执行一个以前可以工作而且源代码没有改动的任务时崩溃。不能在合法的源代码位置上设罝断点是同步出了问题的一个很好的标志。首先,你应该删除工程中的DebugRelease文件夹,以保证重新编连是完全的。最好在每次升级你的计算机之后都删除DebugRelease文件夹,并重新编连。如果你觉得程序或调试器的行为不可理喻,就进行一次完整的编连,以消除编译链接出错的可能。

重新启动Visual C++环境

    有一次调试时,当我跟踪进入WinMain的时候程序崩溃了。于是我在WinMain的开始处设置了一个断点,使程序运行到这个断点,然后往下走了一步,程序就崩溃了。因为没有渉及全局变量的初始化,不会是我的代码的问题。问题出在什么地方呢?是我在断点对话框中设置的一个异常的断点表达式。当我清除所有的断点之后,问题也就没有了。

    Visual C++有一些超常的能力,但这些特性也会引起奇怪的问题。如果你的程序表现得很奇怪,你可以试着清除所有的断点,关闭或隐藏观察窗口,检查工程设置对话框看最近做了些什么修改。这些步骤可以去掉Visual C++环境引起的异常行为。

12.2 开动你的脑筋

    当然,你应该一直开动你的脑筋:在调试过程中梦游是不太可能产生什么有用的结果的。当你陷入困境的时候,开动脑筋总是比一味蛮干要有用得多。这里是一些保证你的头脑高效运转的技术。

不要依赖调试器

    程序员容易陷入困境的一个地方是试图在调试器中重现错误。虽然一般来说调试器总是你的最强大、最有用的调试工具,但这句话只有当你能在可以承受的时间内重现这个错误的时候才是正确的。如果你不能重现这个错误,就该考虑使用其他技术解决问题了。不要过于依赖调试器。

    不能在调试器中重现问题的原因可能有很多。其中,一些与海森堡不确定原理有关(见第1),即调试器自身的存在改变了程序的行为,从而使得一些类型的错误不可能重现。

    你应该记住一点,不一定要在调试器中看到错误才能指导发生了些什么,根据周边证据重新构造犯罪现场可能会更有用。现在你有源代码,知道它应该怎么做,所以你可以在你的脑子里执行有关的部分,确定代码真正的执行情况。当然,你可以在调试的辅助下配置你的程序,例如使用断言、跟踪语句和日志文件等,补充你的周边证据。一旦你收集到了足够的信息,你可以用演绎或归纳的逻辑或者通过创造性的思考发现这个错误(见第1章“调试的过程”中的讨论)

理解程序

    不管你是否正在使用调试器。你都必须理解程序是如何工作的。这包括知道它的目的、它的设计、组件及组件间的关系,还有它的核心技术。试图调试一个你并没有真正理解的程序很可能一无所获,甚至是有害的。

    理解一个程序的最好的办法是浏览它的源代码和文档(应该有一个文档),看懂它的工作原理,然后再浏览一次代码,这次应该根据错误对它进行分析。确定错误可能是怎样发生的,然后确定可能是哪段代码引起的。对于每个可能的原因,比较你期望的行为和实际行为,消除一些可能性。最后,仔细地看剩下的代码,找出问题所在。

    我发现,如果我足够努力,有时可能再进行一些测试,我可以不借助调试器,只依靠浏览代码就解决很多错误。这个工作难度很大,但是如果陷入了困境,一般也不怎么浪费时间。即使没有找出问题,至少对程序和问题有了一个更好的了解,你可能会找到一个发现问题的更好方法,另外,理解程序可以帮助你确定问题不在什么地方,这也可以帮助你集中精力对付出问题的地方。

运行一个测试用例

    有时候代码太复杂了,我们不能全部理解。例如,我曾经做过一个程序,实现复杂的图形变换。为了显示数据,我不得不在文档中缩放数据。然后旋转和拖动数据,然后在视图中改变它的原点(这都是真的)。虽然理解其中的一个或两个变换不是那么困难,我发现同时考虑这三个变换几乎是不可能做到的。那么,你怎么才能理解这样一个程序以寻找错误呢?是的,你不会有什么好方法;但是,你可以运行一个单独的测试用例。例如,你可以拿一个单独的点来进行调试,例如左上角,然后运行一次代码,看这个点是怎么做变换的。如果代码过于复杂,不能凭空想象出来,那么可以通过一些策略性的测试用例来调试问题。

    如果代码过于复杂,不能凭空想象出来,可以通过一些策略性的测试用例来调试问题。

尝试一种新的方法

    如果你陷入困境,停下来想一想。问问自己为什么会陷入困境。是不是忽略了什么东西?是不是做了不正确的假设?是不是在重复相同的、没有作用的过程?是不是注意力集中在一些不重要的任务上面没有建立起整体概念?如果是这样,停止你正在做的事情,考虑尝试一种新的方法,即使不能马上看到这个尝试是正确的。虽然你的尝试看起来是浪费时间,但是失败也可能会提供有价值的信息,甚至揭示出正确的方向(爱迪生因为发明了白炽灯泡的技术而闻名于世;不过,他的另一个出名的原因是因为他睡得很少)

12.3 重新检查你的假设

    在调试的过程中做了很多假设,尤其是如果你是通过一个错误报告间接地知道错误信息的。当你陷入困境的时候,最好回过头来看看这些假设,保证它们是正确的。如果假设有误,很可能你寻找错误的努力都是建立在错误的方向上。

关于错误的假设

    下面是程序员关于错误的典型的假设:

    •这确实是一个错误。少数情况下,“错误”可能是一种特性。例如,假设曾经改正过一个错误,表现为输入正确的数据而得到一个错误消息,现在,又得到了错误消息。根据以前的经历,很容易假设这个错误又出现了,这个错误消息是不正确的。但是,有可能这个错误消息是正确的。数据确实是不好的。这个假设会使得你浪费时间试图改正一个事实上不存在的错误。我曾经不止一次地犯过这种错误。程序员有时会按他们认为应该的方式工作,即使以前的经验不是这样。

    •错误没有被改正。错误经常会不止一次被提出。有些时候改正一个错误同时也改正了另一个,另外一些时候其他程序员改正了错误,其结果是,有时候会调试一个实际上已经被改正的错误;所以,当不能重现错误时要注意这个可能性,但是,错误不会自己跑掉,所以不能假设错误己经被修正了。应该检查错误数据库和源代码控制系统中的相关文件,验证错误是否己经被改正。

    •错误报告是正确的。错误报告有时候是不正确的、不完整的,或者误导性的。当你怀疑一个错误报告有问题时,不要害怕要求一份更清楚、更完整的信息。

    •测试程序或脚本是正确的。如果错误是由一个测试程序或测试脚本检测出来的,那么存在测试本身就有错误的可能性。如果不能重现一个测试程序成脚本报告的错误,请注意这一点。

    •错误可以被重现。有的时候,错误实际上不是一个错误,而是系统崩溃的症状。还有的时候错误只在特定版本的Windows上可以重现,很可能还需要一个特定的服务包。因此,测试人员应该尽量重现他们找到的错误,并报告使用的Windows版本。

关于程序的假设

    下面是程序员关于Windows程序的典型的假设:

    •源代码是正确的。确认你使用的是正确版本的源代码。如果程序有多个版本,很容易犯这个错误。

    •编译器是正确的。确认使用的是正确版本的编译器,而且编译和链接选项是正确的。编译器选项,例如/Zp(对齐结构的成员),特别容易引起错误。可以使用文本编辑器打开工程的DSP文件(Visual C++中打开时,一定要以文本文件形式打开),简单地检查一下编译和链接选项。在编辑器中,你可以比较当前的编译链接选项设置和基本的设置,也可以比较调试版本和发行版本的设置。

    •编连过程是正确的。确认编连过程使用的是正确的文件,并且成功地执行了所有必需的步骤,确认使用的库的版本正确,而且调试版本使用的是调试库,发行版本使用的是发布库,还有MFCC运行时刻函数库都是动态的或者都是静态的。

    •Windows系统组件是正确的。确认调试的时候使用的是正确的Windows版本。不幸的是,检查这个假设并不像看起来那么简单,因为界。。(5有很多版本,而且一些\105 API是和正一起发布的。例如,你可以看看MSDN文档中任何一个shell函数(例如ShGetSpecialFolderPath)Requirements部分。真是令人恐慌。

    •第三方组件是正确的。确认程序需要的所有第三方组件都已经安装并且注册了。

    Windows注册表是正确的。程序需要的所有注册表设置是正确的。如果程序需要环境变量(希望它不需要),也要进行检査。

    •程序选项是正确的。典型的Windows程序都会有很多选项。有些程序有多种模式。用户界面和内部都是如此。有的错误只有在某种选项和模式的组合下才会发生,这一点也不奇怪,但是很容易被很多程序员和测试人员忽视。

    •数据是正确的。程序要求的数据必须可用,而且格式正确。确认程序使用的数据是希望它使用的数据,而不是某个旧版本或者测试版本的数据。

    •安装程序是正确的。确认安装程序正确地安装了程序,将正确的文件拷贝到了正确的位置,并进行了正确的注册表设置。如果不是,所有东西都谈不上。

    •外围设备是正确的。如果程序使用外围设备时有问题,确认这个外围设备已经正确安装、连接、打开、激活,而且工作正常。例如,如果在某台打印机上打印时出了问题,应该先确认打印机已经接上线、打开,并且正确地连接、安装了。然后检查打印机的自测试,确保打印机工作正常,也不缺纸或缺墨。

    很多时候,你可以不作任何检查地接受这些假设,尤其是当你已经在这个程序工作了一段时间时。但是,如果你使用一台不是平时使用的计算机或其他硬件进行调试,或者你调试的程序有一段时间没有碰过时,如果当调试陷入因境,检查这些假设是个很好的主意。注意任何建立在错误的假设之上的结论都很可能是错误的。记住:有很多原因可能使一个程序不正常工作,不一定是代码中的错误

12.4 检査明显的事物

    有的时候,你陷入困境,只是因为忽视了一个回想起来很明显的细节。所谓的明显都是回想时的感觉;当你忽视它的时候,你并不觉得它很明显。

    下面是一些应该检查的明显的细节:

    •查看编译器的警告。在/W4警告级别上进行编译,査看所有的警告信息。偶尔也要用发行版本进行编连:在检査未初始化局部变量方面,Visual C++编译器对发行版本做的比调试版本要好得多,因为这是在优化时做的检查

    •激活MFC跟踪语句。运行Visual C++Tracer工具可以取消MFC中的跟踪语句。运行Tracer,保证它们是打开的。

    •查看跟踪语句。査看跟踪语句输出寻找线索。不幸的是,跟踪语句输出总是很容易被忽略。

    •检査新的代码。运行Visual C++WinDiff工具或类似工具,查看最近修改过的代码,然后手工或使用调试器仔细检査这些改动。新的问题很可能是由新的代码引起的。WinDiff工具可以比较整个目录,这样可以在几秒种时间里找出所有的修改代码。

    •检查编连。在一次编连中使用的特定的编译和链接选项可能是错误的因素,打开工程设置对话框,仔细检査调试和发行版本中的所有设置。你可以创建调试和发布两个版本,比较它们的行为,检查编连的问题。虽然不同的行为可能说明编连的问题,但是它也可能意味着代码中的错误。调试和发行版本的差别以及它们如何影响错误在第7章“使用Visual C++调试器调试”中有所讨论。

    •检查组件。确认你在调试你想调试的组件。如果你在Visual C++中打开一个DLL工作空间,然后在调试会话中指定了一个可执行文件,这个可执行文件将会被使用,而不是装入工作空间的DLL。可执行文件会使用它在Windows文件搜索序列中找到的第一个匹配的DLL(关于Windows文件搜索序列的文档见APl函数LoadLibrary)。类似地,COM函数CoCreateInstance会使用它在注册表中找到的服务器程序,不管你在工作空间中装入了什么。在Visual C++中,你可以使用Debug菜单下的Modules命令确定程序使用的真正的组件

    •检査系统。检查系统,确保资源都是可用的,包括GDI资源、内存和硬盘空间。可用的硬盘空间会影响可用的内存,因为硬盘用做虚拟内存交换文件。你可以运行微软系统信息工具快速地确定系统状态。如果有的系统资源很低,那么采取必要的措施提高可以使用的资源数量。当然,还要检查所有的外围设备,确保它们都是打开的,而且都连接正常。

12.5 检查代码

    很可能问题只是代码中的一个愚蠢的错误(这种情况不是第一次发生了)。检查有关的代码,确认它们;

    •调用了正确的的函数。

    •使用正确的参数而且顺序正确。

    •使用正确的函数调用习惯(例如,对窗口和对话框过程,总是使用_stdcall,或者CALLBACK)

    •使用正确的变量。

    •使用正确的变量类型。

    •使用正确的变量限制符,例如constvolatile

    •初始化所有的变量和数据成员。

    •将释放了的指针和句柄赋0

    •使用正确的括号。

    •使用正确的逻辑,例如没有虚悬的else子句。

    •使用头文件声明跨文件共享元素,不要声明extern变量。

    •使用正确的强制转换类型。

    •没有副作用。

    •异常安全。

    此外,确认你已经查看了有关代码中的注释,有时候代码是很好的文挡。

12.6 检查系统

    错误是由系统引起的或者与系统有关的可能性总是存在的。如果Windows程序是像MS-DOS程序那样自包含的,与系统有关的错误就会少得多。一旦你组合了不同版本的Windows和不同版本的各种系统DLL,就很有可能因系统配置不当而导致错误。

    判断一个问题是否与系统有关的第一个标志是这个程序在不同的计算机上表现出不同的行为。例如,你的程序在某一台机器上会崩溃,但是在其他机器上工作良好。如果发生了这种情况,寻找一下规律。在一些不同的机器上试着运行这个程序。是否程序在Windows 2000上和在Windows 98上表现不一样,是否程序在两个版本中都兼容,服务包是否是一个因素,IE(它经常会装一些系统DLL)的版本是否是一个因素,问题是否只限于某一台机器,驱动器是否是一个因素,当在Windows安全模式下运行程序行为是否有变化?你做的每一次测试都应能提供更多的信息,帮助你限制问题的范围。

    如果问题只发生在那些没有安装最新服务包的计算机上,就在其中一台计算机上安装最新的服务包,看错误是否就没有了。如果成功了,你很幸运:除非你发现了别的问题,你可以认为问题出Windows自身,让你的软件要求这个服务包。不过,这其实是一个假设:有时候系统的变化其实没有增加或减少系统的错误,而是揭露了程序中隐藏的错误。最好保证你用来开发的机器上的WindowsIEVisual C++都是最新版本的,而且带有最新版本的服务包。这个措施能减少你的程序受到其他人的错误的牵连的可能性。另一方面,保证一些测试机器没有最新的软件,这样就能跟踪与系统有关的问题。

    在跟踪另一些类型的与系统有关的错误方面,微软的系统信息工具很有用。假设问题只在一台特定的计算机上发生——这说明是一个系统问题。要检查这个错误,可以试着运行系统信息的Tools菜单中的有关工具。下面这些诊断工具非常有用:

    Windows Registry Checker检査注册表的破坏。

    Version Conflict Manager找到并重新安装过时的系统文件。它很可能是被安装程序替换掉的。

    System File Checker验证系统文件的完整性。

    ScanDisk检查硬盘,看是否有错误。

    DirectX Diagnostic Tool检查各种DirectX文件和驱动器。

    如果问题看起来和服务包或系统崩溃没关系,很可能问题是一个DLL冲突。例如,我曾经做过一个程序,在办公室里的所有机器上都工作正常,除了两台。在检查并否认了很多假设之后,我决定査看是否是DLL冲突。我比较工作正常的计算机上该程序使用的DLL和工作不正常的情况,发现那两台不能工作的计算机有相同版本的Advapi32.dllOleaut32.dll。将这两个文件替换掉,问题就没有了。这两个不好的系统文件是Microsoft SDK的一个beta版本安装的。这一点都不好玩。显然,微软的某些人没有尝够他们自己的狗食(“尝自己的狗食”(eat you own dogfood)是一个开发原则,要求软件公司在开发时使用自己的软件,这样,如果产品中有什么问题,公司就可以在产品被发放到市场之前先发现并解决问题)

    可以使用如下方法比较两个计算机上的DLL。这个技术看起来可能有点烦琐:

    1.运行Microsoft的系统信息工具。

    2.使用File菜单中的Export命令把结果输出到一个文件。

    3.Visual C++调试器中运行你的程序。

    4.使用Debug菜单中的Modules命令确定程序依赖的DLL。或者,你可以使用Visual C++DependencyWalker工具确定相关DLL(这个工具的可执行文件是Common\Tools\Depends.exe)

    5.在一个文本编辑器中打开输出的系统信息,找出和程序模块有关的行。

    6.将模块列表装入到一个电子表格中并打印(这些信息用制表符分隔)

    7.将程序模块和其他机器上的模块相比较,检查版本号、文件日期、文件大小和路径。

    如果错误的存在与一组特定的DLL的使用关联,你很可能碰到了DLL冲突。如果文件的路径不一致,可能机器上安装有多个版本的文件,而程序没有使用你希望的那个。这种情况称为“DLL恶梦”(DLL hell)

    使用Microsoft的系统信息工具帮助诊断与系统有关的问题,

    还有一种方法也可以得到类似的信息,而且不那么麻烦,就是使用Windows 98版的Dr. Watson(在第6章“在Windows中调试”中已作了介绍)。不幸的是,一般不能从Dr. Watson中获得程序的模块信息,除非程序崩溃了,所以如果程序正常,这个方法不能奏效(但是,可以修改代码有意识地使程序崩溃:打开Dr. Watson,使程序崩溃,然后运行Dr. Watson查看系统的快照信息。在View菜单中选择Advanced View命令,然后选择Modules标签,这里可以看到所有程序模块的版本、路径、文件日期和地址信息)

    Windows 98版的Dr. Watson可以帮助你诊断与系统有关的问题。

    最后,我还是要重申我经常强调的。程序在不同版本上的Windows上表现出不同的行为并不一定说明是系统的错误,仍有可能是代码中存在错误Win2000Win98使用本质上不同的两套代码,所以一个有问题的程序可能表现出不同的行为。实际上,除非你可以用一个服务补丁或者通过修改一个系统错误或者消除DLL冲突的办法解决问题,你应该假设问题在代码中。至少,检查系统而没有发现系统问题能够给你信心,说明你的努力方向没有错误。这个过程也能给你一点信息,帮助你找到问题。

12.7 再次检查文档

    虽然我发现MSDN文档非常的好,但是其中有两个陷阱,很容易误导人。第一个陷阱是其中的一些文档非常老了。很多文档开始的地方有一节写着“本文中的信息适用于:......”,一定要读这一节!确保文档和你正在做的事情是有关的。有些文档讨论的问题只与Windows 3.1有关,但这个通常不明显,除非你去精读这一节。

    在阅读MSDN文档的时候要小心。其中有一些很旧了。

    第二个陷讲涉及当使用新的或不熟悉的API函数时对Windows API文档的Requirements部分的检査。新的shell和普通控件 API(Comctl32.dllShell32.dllShlwapi.dll)尤其危险,因为它们要求特定版本的WindowsIE。为了避免麻烦,选择Windows的版本并定义_WIN32_IE符号,强制与目标版本兼容。例如,为了与Windows 95Windows NT4.0的所有版本兼容,应该在include所有Shell和普通控件头文件之前这样定义_WIN32_IE宏:

    #define _WIN32_IE  0x0400

    ...

    #include <shlobj.h>

    #include <shlwapi.h>

    #include <commctrl.h>

    注意,_WIN32_IE的默认值为0x0500,要求Windows 2000IE5

    定义_WIN32_IE宏保证和Windows的目标版本兼容。

12.8 依靠其他人

    如果能自给自足,自己把自己从问题中解救出来,当然是很值得崇敬的,不过,在有的时候,应该考虑向其他人寻求帮助。毕竞,在一个你没有能力解决的问题上浪费不必要的时间并不那么令人崇敬。虽然不希望过于依赖他人,但是当你真的陷入困境时,应该向有经验的同事请求帮助。

为什么这样有用

    不同的人有不同的专长和经验,所以询问其他的人能帮助你解决问题并不是什么令人惊奇的事。令人惊奇的是有时候别人并没有说什么就解决了问题。有些时候,其他人可能只是咕哝了两句就能帮助你解决问题。可能这是某种形式的非语言的交流,不过经常发生的情形是理清你对错误所做的工作和推理之间的关系就足以揭示问题

    这种情况发生有很多原因。当对其他人解释什么东西的时候,一般来说你比平时更自觉。把一个问题用语言表达出来迫使你明确假设并说明理由——你自己并不总做这个工作。还要证明所采取的步骤是正当的,同时证明没有采取别的步骤也是正确的;你无法对你的假设含糊其词,或者忽视明显的细节。因此,任何虚假的逻辑都变得很显眼。和一个同事交谈可以使你从另一个视角看问题,用一种从听众的角度可以理解的方式解释事情——这也是你平时自己不做的事情

    在《The Practice of Programming(Addison-Welsley, 1999)—书中,Brian KemighanRob Pike说了一个故事:一个大学的计算中心在咨询台边上放了一个小熊,任何需要帮助的人在从咨询人员那里寻求帮助之前必须首先向这只熊解释自己遇到的问题。只要这些学生努力解释了自己的问题,我相信这只熊是有帮助的。

什么时候提问

    请求帮助的最大问题是确定提问的最佳时机。如果问得太晚了,你已经浪费了自己的很多时间。问得太早了,你是在浪费别人的时间。当请求帮助的时候,应该体谅别人。这也是尊重他人时间的最好表现。

12.9 使用新闻组

    除了直接询问他人,也可以通过Internet在开发者的新闻组中发布问题。Deja.com有几个和调试有关的论坛。在新闻组中问问题应该和面对面地问别人问题同样有礼貌。不要浪费别人的时间,即使你不认识对方;一定要自己先做你的家庭作业。努力地解决自已的问题,然后看看是否有类似的问题曾经提出过。看看相同的问题如何一遍一遍地被提出是值得注意的。这些新闻组有很好的搜索功能,使用这个功能。

    以下发布一个调试问题的一些注意事项:

    •对程序给出明确的信息。提供足够的信息,使读者能够理解你所做的事情。描述程序是做什么的,怎么编连。使用了哪些组件。说明编译器(包括服务包)版本,以及任何有关的编译选项;说明运行的Windows(包括服务包)的版本;如果使用MFC,说明程序使用动态链接库还是静态链接库;还要说明程序是单线程的还是多线程的。给出所有有关的细节。

    •如果程序是多线程的,说明线程的类型(工作线程、UI线程)以及使用哪个函数创建线程(例如CreateThread_beginthreadex或者AfxBeginThread)。这个线程是在DIlMain函数中创建的吗?描述关闭线程的过程。还要描述线程执行的代码:它是否调用C运行时刻函数库函数、MFC函数或者COM接口?如果调用了COM接口,如何获得接口指针?它在一个单线程套间里还是多线程套间?如果在一个单线程套间里,它是否一直正确地处理消息?时序如何影响问题?程序的行为在调试版本和发行版本中有何差异?在调试器中运行和单独运行时有没有不同,在单处理器和多处理器机器上运行呢?给出所有有关的信息。

    •对问题给出详细的描述。提供足够的信息,使读者能理解问题是什么和导致问题的特定行为。程序崩溃了吗?如果是,不要只是说它崩溃了。描述程序是如何崩溃的,你做了些什么使得程序崩溃。是否因为一个未捕捉的异常而崩溃?如果是,是什么样的异常?有没有断言失败?如果有,这个断言说了些什么?是否有Windows APl失败?如果是,GetLastError返回的错误代码是什么?调用堆栈里有些什么?

    •尽量简洁。将问题简化到最简单的形式。如果能用一个简单的代码片段(最好少于20)演示这个问题,一定要一起贴上去。不要拷贝整页整页的代码。

    •尽量清楚。在发布之前读一读你的问题,确保它说了你想说的,而且有意义。

    •写好主题行,使它有很强的描述能力而且简要,一个好的主题行有较大的可能产生良好反应,而且对其他人更有用。类似“这里有一个奇怿的问题”或者“求助!!”之类的主题行不能引起读者的任何兴趣。

    •说明你己经做了你的作业。描述你已经做了的试图解决问题的工作,以及这些工作的结果。

    •确认问题发布在一个适当的新闻组论坛中。

    要成为一个好的新闻组参与者,还应该对收到的任何好意作出反应。下面是一些不错的主意:

    •发布最后的结果。例如,如果有人建议你尝试三种不同的方法,你发现其中一种方法找到了问题,而另外两种没有效果。现在你的工作已经做完了,但是新闻组里的问题还没有答案。不要使这个问题没有结果地挂着,将你调试的最后结果发布出来,这样别人也能受益。

    •通过回答别人的问题帮助其他人。你应该努力回答至少和提出的问题一样多的问题。

    以下是一些你在新闻组中应该避免的事:

    •除非在一个新手的论坛中发帖子,不要发布那些只需快速地搜索MSDN或者读几页入门性文章就能解决的问题。不要只是因为你没钱不想买参考书就问问题。

    •不要不尊敬他人。

    •不要针对个人。

    •不要装腔作势。

    •不要作假的挑战,例如,“我打赌你们这些天才没有一个能解决这个问题...”。虽然你希望以一种鼓励别人回答的方式发布问题,但是这个计谋不会有用。

    •不论你的问题多么紧急,也不要说诸如此类的话:“求助!!!如果我不能在明天之前解决这个问题,我会丢了饭碗!”这样的话看起来很不专业而不是引人同情,所以你不能得到很多的帮助。如果你获得了什么反应,也很可能是建议你考虑换一个工作。

    •不要在多个新闻组论坛中同时发布同一个问题,这是非常考虑不周的。想一想:如果你在五个新闻组发布了同一个问题,其中四个组的努力都将会被浪费。很多人掌管多个论坛,看到多个重复的帖子会使他们失望;而这些人一般都是最权威的——那些你希望会回答问题的人。

    可以公平地说,这些参与新闻组的人都愿意帮助你,但他们不愿意替你完成你的工作。要体谅别人,要有礼貌,要耐心,就会有人回答你的问题。

    这里有一个不好的新闻组问题的例子(以及典型的反应),和一个改进后的版本。

    不好的问题:“我创建了一个程序。但是当我运行它的时候,收到了一个错误:‘创建空文档失败’。我应该怎么做?”

    典型的反应(如果这个问题还能得到反应):“创建一个调试版本,设置断点,跟踪代码,找到问题。”

    下面这个改进的问题更加清楚,而且说明你已经做了自已的工作:

    “我创建了一个MFC SDI程序,而且以前可以正常工怍,可是现在我在Windows 95(任何服务包)下运行它时,在程序初始化的时候收到一个对话框,显示错误:‘创建空文档失败’。我用的是Visual C++6.0SP3。我跟踪了代码,但是这个问题看起来好象在MFC内部,在CSingleDocTemplate::OpenDocumentFile中。

    有趣的是,我在Windows 98Windows 2000下测试了这个程序,运行是正常的。另外,在Windows 95下,如果安装了lE 4.0,也能够工作。我用Visual C++5.0重新编连了这个程序,发现它能在任何平台下工作,即使没有IE。于是我检查了MSDN,没有发现什么线索。看起来Visual C++6.0是主要问题。

    其他人有没有见过这个问题?有什么建议?”

12.10 结束危险的生活

    现在是总结的时候了。好的,先承认这一点——为了使工程快一点完成,你忽略了一些小地方。你试图逃脱惩罚,不过这一次不行了,现在你必须付出代价。为什么不花几天的时间检查代码,自己先发现错误,而不是花两周的时间来找出一个错误?

    下面总结了一些使代码没有错误的最有效的技术:

    •开发并采用断言的策略,

    •开发并采用跟踪语句的策略。

    •开发并采用异常的策略。

    •用/W4警告级别进行编译,修改所有的编译警告。

    •用/GZ编译器选项进行编译,检査所有未初始化的变量和堆栈问题。

    •指令调试栈显示有用的源代码信息,将数据以一种可读的格式堆放。

    •如果使用MFC,在派生自CObject的类中确实实现AssertValidDump

    •使用C++auto_ptr智能指针类或类似的类防止内存泄露。

    •在编程时釆取防护措施。

    •重写那些书写草率、你自己也知道有很多错误的代码。

    如果你忽略了一些小地方,结果现在遇到了不能解决的错误,最好结束危险的生活,花一点时间保证事情的正确性。如果你已经这样做了,一定会有好处。