BUG,规范,断言和调试(2)

发表于:2011-11-04来源:未知作者:领测软件测试网采编点击数: 标签:Bug管理
编译器不能捕获这种运行时才会出现的错误(顺便说一下,我在CSDN上居然还看到有人抱怨编译器不会报告除0错误);也不能捕获算法中的BUG和检验参数中的数

  编译器不能捕获这种运行时才会出现的错误(顺便说一下,我在CSDN上居然还看到有人抱怨编译器不会报告除0错误);也不能捕获算法中的BUG和检验参数中的数据。但要是你知道怎么做的话,这类问题很容易被发现。你可以用SetProcessWorkingSetSize函数或者msconfig工具减少虚拟内存大小,或者使用Virtual PC之类的虚拟机或者磁盘配额策略来模拟内存和磁盘空间不足的情况。

  你有可能想在这种极限情况下调试你的代码,但是大多数时间内,内存分配不会失败,而设置条件断点又太麻烦了。这时候可以在代码里面加上一段用来在内存分配失败时触发调试器的断言代码

  void MyZeroMemory(char* strBuffer, int length)...{ assert(strBuffer!=NULL);}

  如果使用的是MFC或者ATL,建议使用对应的宏ASSERT和ATLASSERT。现在你可以编写健壮的代码使得程序在strBuffer这块内存分配失败时也能够正常运行。

  现在的问题是,加入的这些代码增加了应用程序的大小,减慢了运行速度。在解决了内存分配失败造成的程序崩溃的问题之后,有必要在发布的版本中去掉这些断言代码。一个简单的办法是使用预处理标识符:

  void MyZeroMemory(char* strBuffer, int length)...{#ifdef DEBUG assert(strBuffer!=NULL);#endif}

  这样你可以只维护同一份代码。当然,这也意味着调试的代码在发行版中会被去除,所以为了避免不可预料的行为,为了调试而加入的代码应该尽可能少地影响应用程序的行为。

  你有可能需要重新定义assert来实现扩展的行为——例如在assert断言失败中断程序时打开源文件并且跳到assert那一行——这时候你可以编写自己的断言函数,然后重定义assert为这个断言函数。

  #ifdef DEBUG

  /**//*display a dialog and if the user selected break

  , jump to the assert line*/

  void _assert(char*,unsigned int);

  #define assert(f)\

  if(f)\

  ...{}\

  else

  _assert(__FILE__,__LINE__)

  #else

  #define assert(f)

  #endif

  空的if语句块可能看起来有点奇怪,但是这可以避免和宏外的if-else产生冲突。同样,最后一行语句没有结束的分号,因为在使用的时候再加上会更加自然。

  assert最有用的地方就是用来检验函数的参数——但是也可以在其他地方起作用。在程序中的断言语句越多,异常的情况就越容易被侦测到。

  既然assert是代码,它不可避免的需要注释。即使是自己写的代码,过了六个月之后再来审视也可能需要一点时间来重新理解这部分代码。一个简单的注释可以把这部分时间减少:

  void MyZeroMemory(char* strBuffer, int length)

  ...{

  /**//*should not be called when buffer allocation failed*/

  #ifdef DEBUG

  assert(strBuffer!=NULL);

  #endif

  }

  在编写完函数之后,应该审视函数中的代码,之后在函数的开头验证函数正常运行所需的条件。如果你在写一个库函数,那么应该在函数的文档中加入函数正常运行所需的条件——否则就会增加使用者发现BUG的难度。举例来说,Windows API的文档不可谓不详尽,但是我在用汇编调用Windows API的时候,也花了很长时间才发现调用Windows API之前栈顶要设置成4的倍数。

  注意不要把一些条件当成默认成立的了。assert(sizeof(int)==4);这样的语句在一些人看来很荒谬,但是在Windows开发中通常是32位的long在一些64位平台上已经是64位的了,而在目前还不知道sizeof(int)在什么时候会升位。如果你的代码依赖于int的大小,那么写上这行可以在未来升位之后更快发现问题。

  一些保守的程序员在参数错误时会让函数继续运行——返回一个错误码——但是不报告错误。在编写核心模块时这可能很有必要,但是这也经常会把BUG藏起来——在多层函数返回之后时候,错误码经常会丢失或者被替代。尽量不要使用保守编程来替代断言,如果你认为保守编程会造成定位问题的困难,那么就加上断言代码。

  在一些时候,校验参数数据似乎是不可能的事情——想象一下那些被设计来搞糊涂解密者的加密算法的中间数据——但是校验这种复杂算法的方法也不是没有。为了确认手算和心算的正确性,我们会使用电子计算器的结果来进行比较,反过来,我们也可以编写另外一个的算法来断言计算结果的正确性。这种方法也可以被用来断言一个函数的汇编版本和C版本的一致性——为了获取最大性能,函数的汇编版本的算法可能和C版本的有很大差异。当然,不是每个函数都有必要用这种方式来验证,实际上,只有极其重要的算法和对性能极其敏感的代码才会需要这种双保险来验证。同样,为了调试而加入的算法也应该尽可能少地影响应用程序的行为。

  最后,你不应该在发布程序时从代码中去掉断言语句,而是把它们留在那里以供你升级或者查找BUG时使用。帖来自于网易社区:http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_10938d0575c4afe

  消灭不可预料的行为

  断言并不能抓住所有BUG——它们都是人写出来的,而是人就会犯错误。一些常见的错误包括:

  使用状态不确定的资源

  在释放资源之后继续访问资源

  在资源重新定位之后继续引用旧的资源

  申请资源之后丢失对资源的引用

  访问时未注意是否越界

  忽略错误信息

  这些并不是杞人忧天的问题——实际上,这些问题属于日常开发中最常见的问题。这些问题的特点是,它们并不是时常造成程序行为的异常,并且症状不可重复。以内存为例,释放内存之后编译器和操作系统通常不会自动去擦除内存中的内容,所以继续访问内存不太可能造成程序行为的异常——直到内存被重新分配出去,而另一块代码开始重写这块数据。申请资源之后丢失对资源的引用可能只是造成长时间运行之后系统资源不足而已。另外,这些问题都是算法的问题,而编译器并不会替你校验你的算法,你自己也不太可能会怀疑你自己的算法。

原文转自:http://www.ltesting.net