• ACCP软件工程师
  • BENET网络工程师
  • JAVA+大数据
  • Python工程师
  • 云计算工程师
  • Web前端工程师
  • 软件测试工程师

软件调试的一般思路

2013年02月20日 16:52供稿中心:兆隆教育

摘要: 1.plain new/delete.普通的new 定义如下: void *operator new(std::size_t) throw(std::bad_alloc); void operator delete(void*) throw(); 注:标准C++ plain new失败后抛出标准异常std::bad_alloc而非返回NULL,因此检查返回值是否为NULL判断分

1.plain new/delete.普通的new

  定义如下:

  void *operator new(std::size_t) throw(std::bad_alloc);

  void operator delete(void*) throw();

  注:标准C++ plain new失败后抛出标准异常std::bad_alloc而非返回NULL,因此检查返回值是否为NULL判断分配是否成功是徒劳的。

  测试程序:

  #include "stdafx.h"

  #include <iostream>

  using namespace std;

  char *GetMemory(unsigned long size)

  {

  char *p=new char[size];//分配失败,不是返回NULL

  return p;

  }

  int main()

  {

  try

  {

  char *p=GetMemory(10e11);// 分配失败抛出异常std::bad_alloc

  //……

  if(!p)//徒劳

  cout《"failure"《endl;

  delete [] p;

  }

  catch(const std::bad_alloc &ex)

  {

  cout《ex.what()《endl;

  }

  return 0;

  }

  2.nothrow new/delete不抛出异常的运算符new的形式,new失败时返回NULL.

  定义如下:

  void *operator new(std::size_t,const std::nothrow_t&) throw();

  void operator delete(void*) throw();

  struct nothrow_t{};  const nothrow_t nothrow;//nothrow作为new的标志性哑元

  测试程序:

  #include "stdafx.h"

  #include <iostream>

  #include <new>

  using namespace std;

  char *GetMemory(unsigned long size)

  {

  char *p=new(nothrow) char[size];//分配失败,是返回NULL

  if(NULL==p)

  cout《"alloc failure!"《endl;

  return p;

  }

  int main()

  {

  try

  {

  char *p=GetMemory(10e11);

  //……

  if(p==NULL)

  cout《"failure"《endl;

  delete [] p;

  }

  catch(const std::bad_alloc &ex)

  {

  cout《ex.what()《endl;

  }

  return 0;

  }

  3.placement new/delete 主要用途是:反复使用一块较大的动态分配成功的内存来构造不同类型的对象或者它们的数组。例如可以先申请一个足够大的字符数组,然后当需要时在它上面构造不同类型的对象或数组。placement new不用担心内存分配失败,因为它根本不分配内存,它只是调用对象的构造函数。

  测试程序:

  #include "stdafx.h"

  #include <iostream>

  #include <new>

  using namespace std;

  class ADT

  {

  int i;

  int j;

  public:

  ADT()

  {

  }

  ~ADT()

  {

  }

  };

  int main()

  {

  char *p=new(nothrow) char[sizeof(ADT)+2];

  if(p==NULL)

  cout《"failure"《endl;

  ADT *q=new(p) ADT;  //placement new:不必担心失败

  // delete q;//错误!不能在此处调用delete q;

  q->ADT::~ADT();//显示调用析构函数

  delete []p;

  return 0;

  }

  注:使用placement new构造起来的对象或数组,要显式调用它们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete.这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

  3、利用差异来缩小问题根源的范围

  假如有两个"同样"配置的机器,一台有问题,另外一台没有问题,那么要找出那台有问题机器出问题的原因,可以关注一下两台机器有什么差别。虽然配置看起来一样,但他们肯定有差别。找出这些可能和问题相关的差别,利用上一节介绍的实证性分析的方法来证明你的假设。

  另外一个例子,如果你开发中的软件,版本001没有问题,而版本002有问题,那么就看看这两个版本之间有什么差异,比如通过SCM来看看都改动了哪些文件,或者使用的第三方库是否发生了变化等。

  4、利用二分法

  我们都知道二分查找的效率很高。同样我们也可以利用二分法的一般原理来缩小问题的范围。还是以上面的那个例子,假如版本001没问题,而版本008有问题,那么怎么快速找到问题是在哪个版本里面引入的呢。最直接的办法,从版本002一直试到008,但是这样效率就太低了。我们假设问题被引入后就不会自动消失,这样就可以利用二分法来找到查找问题被引入的第一个版本。我们先看版本005有没有同样的问题,如果有,那么问题就在版本002-004之间引入的,然后在继续用同样的方法找下去,最多找3次就能找到首次引入这个问题的版本,然后就可以看看那个版本里面有什么改动,就可以把问题的根源缩小到这些改动上了。

  这也是持续集成的思想了,每次提交代码都会触发一次构建和单元测试。如果构建或者测试失败了,问题的的根源就是上一次代码提交造成的,在问题被引入的第一时间修复这个问题要比过了几个版本后再修复要容易一些。

  5、写出能重现问题的最短代码

  如果软件的问题是由于第三方的库导致的,在向第三方库报告问题的时候,最好是能写出一个简单的程序来向其说明问题的发生条件,而不是把直接把你的软件的问题扔给第三方。

  同样的,如果问题就是软件内部造成的,如果能写一个单元测试来重现问题,那么也能提高你测试和调试的速度,因为单元测试启动和运行的速度肯定比把整个系统启动起来的速度要快。任何时候,我们都要提高反馈的速度。

  6、小结

  在遇到问题的时候要先分析以缩小问题根源的范围,而不是直接就跳到代码里面去看哪里出错了(那些一眼就看出问题出错的地方除外)。如果问题在某个环境(这里的环境是各种可能因素的组合,比如OS,软件,第三方库或者系统)发生了,而在另外的环境没有发生,利用这两个环境的差异来找出可能导致问题的因素。利用实证的方法来逐渐缩小问题根源的范围,利用二分法或者其他策略来快速排除一些干扰因素。在找到问题的根源后,最好能写出一个单元测试来重现问题,它能提高测试和调试的反馈速度,也有助于向第三方描述问题。我相信本文介绍的方法大家都曾经使用过,我只是将大家都知道的内容简单总结了一下,希望能有些帮助。

 

©陕ICP备18020405号-2 Copyright  ©  2001-2018隶属于西安兆隆计算机培训中心版权所有