编写高质量代码:改善C++程序的150个建议(十六)

建议31:了解new_handler的所作所为

  在使用operator new申请内存失败后,编译器并不是不做任何的努力直接抛出std::alloc异常,在这之前,它会调用一个错误处理函数(这个函数被称为new-handler),进行相应的处理。通常,一个好的new-handler函数的处理方式必须遵循以下策略之一:

  Make more memory available(使更大块内存有效)

  operator new会进行多次的内存分配尝试,这可能会使其下一次的内存分配尝试成功。其中的一个实现方法是在程序启动时分配一大块内存,然后在new-handler第一次被调用时释放它供程序使用。

  Install a different new-handler(装载另外的new-handler)

  程序中可以同时存在多个new-handler,假如当前的new-handler不能获得更多的内存供operator new分配使用,但另一个new-handler却可以做到。在这种情形下,当前的 new-handler则会通过调用set_new_handler在它自己的位置上安装另一个new-handler。当operator new 下一次调用 new-handler时,它会调用最新安装的那一个。

  Deinstall the new-handler(卸载new-handler)

  换句话说,就是将空指针传给set_new_handler,此时就没有了相应的new-handler。当内存分配失败时,operator new则会抛出一个异常。

  Throw an exception(抛出异常)

  抛出一个类型为bad_alloc或继承自bad_alloc的其他类型的异常。

  Not return(无返回)

  直接调用abort或exit结束应用程序。

  以上的这些处理方式让我们在实现new-handler functions时拥有了更多的选择与自由。这些各式各样的new-handler函数是可以通过调用标准库函数set_new_handler进行特殊定制的,你可以按照自己的方式来对编译器的这一行为进行设定。这个函数同样也声明在 <new> 中:

  1. namespace std  
  2. {  
  3.  typedef void (*new_handler)();  
  4.  new_handler set_new_handler(new_handler p) throw();  
  5. }

  通过函数声明可以看到set_new_handler的形参是一个指向函数的指针,这个函数在 operator new无法分配被请求的内存时调用。set_new_handler的返回值是一个指向函数的指针,指向的是set_new_handler调用之前的异常处理函数。所以,可以按照以下方式使用set_new_handler函数:

  1. //error-handling function  
  2. void MemErrorHandling()  
  3. {  
  4.  std::cerr << "Failed to allocate memory\n";  
  5.  std::abort();  
  6. }  
  7. //Application  
  8. const long long DATA_SIZE = 1024*1024*1024;  
  9. int main()  
  10. {  
  11.      std::set_new_handler(MemErrorHandling);  
  12.      std::cout << "Attempting to allocate 1 GB...";  
  13.      char *pDataBlock = NULL;  
  14.      try  
  15.      {  
  16.         pDataBlock = new char[DATA_SIZE];  
  17.      }  
  18.      catch(std::alloc& e)  
  19.      {  
  20.         ... //some processing codes  
  21.      }  
  22.      ... // other processing code  
  23. }

  假如operator new分配空间的请求得不到满足,MemErrorHandling函数将被调用,程序将按照函数中的设定处理方式运行。在标准C++中,标准set_new_handler为用户类统一指定了错误处理函数global new-handler。上述代码采用的就是global new-handler形式。

通过上述示例代码可以看出,new_handler必须有主动退出的功能,否则就会导致operator new内部死循环。因此new_handler一般会采用如下形式,伪代码表示如下:

  1. void MemErrorHandling()  
  2. {  
  3.   if( 有可能使得operator new成功)  
  4.   {  
  5.       做有可能使得operator new成功的事  
  6.       return;  
  7.   }  
  8.   // 主动退出  
  9.    abort/exit 直接退出程序  
  10.    或 set_new_handler(其他newhandler);  
  11.    或 set_new_handler(0)  
  12.    或 throw bad_alloc()或派生类  
  13. }

  当然,我们可以根据被分配对象的不同,采用不同的方法对内存分配失败进行处理,实现对class-specific new-handlers 的支持。为了实现这一行为,需要为每一个class 提供专属的set_new_handler和operator new版本。假设要为A class设定特殊的内存分配失败处理方式,则需要在类A中声明一个new_handler类型的静态成员(static member),并将其设置为A class的new-handler处理函数。所以就得到了下面的代码:

  1. class A  
  2. {  
  3. public:  
  4.     static std::new_handler set_new_handler(std::new_handler p) throw();  
  5.     static void * operator new(std::size_t size) throw(std::bad_alloc);  
  6.     static void MemoryErrorHandling();  
  7. private:  
  8.     static std::new_handler m_curHandler;  
  9. };  
  10. // 静态类成员定义  
  11. std::new_handler A::m_curHandler = NULL;

  C++标准中规定set_new_handler函数应该保存传递给它的函数指针,并返回前次调用时被保存的函数指针。上面A类中的set_new_handler也应该这么做:

  1. std::new_handler A::set_new_handler(std::new_handler p) throw()  
  2. {  
  3.     std::new_handler oldHandler = m_curHandler;  
  4.     m_curHandler = p;  
  5.     return oldHandler;  
  6. }

  接下来,我们自己定义该类的处理函数:

  1. void MemoryErrorHandling()  
  2. {  
  3.     ... // processing code  
  4. }  
  5. void * operator new(std::size_t size) throw(std::bad_alloc)  
  6. {  
  7.      set_new_handler(MemoryErrorHandling);  
  8.      return ::operator new(size);  
  9. }

  当然我们可以采用更好的设计方式(如类继承)来实现,这里就不赘述。如果读者感兴趣可以自己思考一下,或者求助于资源丰富的Internet,它会提供详尽的参考资料。

  请记住:

  了解new_handler的所作所为,并通过标准库函数set_new_handler对内存分配请求不能被满足的处理函数进行特殊定制。

建议32:借助工具监测内存泄漏问题

  内存管理确实是一个令众多C/C++程序员感到费神又费力的问题,内存错误通常都具有隐蔽性,难以再现,而且其症状一般不能在相应的源代码中找到。C/C++应用程序的大部分缺陷和错误都和内存相关,预防、发现、消除代码中和内存相关的缺陷,成为C/C++程序员编写、调试、维护代码时的重要任务。然而任何人都无法时刻高度谨慎,百密中难免会有一疏,一不小心就会发生内存问题。如果泄漏内存,则运行速度会逐渐变慢,并最终会停止运行;如果覆盖内存,则程序会变得非常脆弱,很容易受到恶意用户的攻击。因此,需要特别关注C/C++编程的内存问题,特别是内存泄漏。幸运的是,现在有许多的技术和工具能够帮助我们验证内存泄漏是否存在,寻找到发生问题的位置。

  内存泄漏一般指的是堆内存的泄漏。如果我们使用malloc函数或new操作符从堆中分配到一块内存,在使用完后,程序员必须负责调用相应的free或delete显式地释放该内存块,否则,这块内存就不能被再次使用,此时就出现了传说中的“内存泄漏”问题。如下面的代码片段所示:

  1. void Function(size_t nSize)  
  2. {  
  3.      char* pCharnew char[nSize];  
  4.      if( !SetContent(pChar, nSize ) )  
  5.      {  
  6.           cout<<"Error: Fail To Set Content"<<endl;  
  7.           return;  
  8.      }  
  9.      ...//using pChar  
  10.      delete pChar;  
  11. }

  程序在入口处分配内存,在出口处释放内存,但是这里忽视了代码片段中的return;,如果函数SetContent()失败,指针pChar指向的内存就不会被释放,会发生内存泄漏。这是一种常见的内存泄漏情形。

  检测内存泄漏的关键是要能截获对分配内存和释放内存的函数的调用。通过截获的这两个函数,我们就能跟踪每一块内存的生命周期。每当成功分配一块内存时,就把它的指针加入一个全局的内存链中;每当释放一块内存时,再把它的指针从内存链中删除。这样,当程序运行结束的时候,内存链中剩余的指针就会指向那些没有被释放的内存。这就是检测内存泄漏的基本原理。

  检测内存泄漏的常用方法有如下几种:

  MS C-Runtime Library内建的检测功能

  使用MFC开发的应用程序时,会在Debug模式下编译执行,程序运行结束后,Visual C++会输出内存的使用情况,如果发生了内存泄漏,在Debug窗口中会输出所有发生泄漏的内存块的信息,如下所示:

  1. Detected memory leaks!  
  2. Dumping objects -> 
  3. mainFrm.cpp(45) : {352} normal block at 0x0058A4B8, 40 bytes long.  
  4. Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  
  5. Object dump complete.

  这是因为在编译过程中,IDE自动加入了内存泄漏的检测代码。MFC在程序执行过程中维护了一个内存链,以便跟踪每一块内存的生命周期。在程序退出的时候,dbgheap.c文件中的extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(void)函数被调用,遍历当前的内存链,如果发现存在没有被释放的内存,则打印出内存泄露的信息。

  一般,大家都误以为这些内存泄漏的检测功能是由MFC提供的,其实不然。这是VC++的C运行库(CRT)提供的功能,MFC只是封装和利用了MS C-Runtime Library的Debug Function而已。所以,在编写非MFC程序时我们也可以利用MS C-Runtime Library的Debug Function加入内存泄漏的检测功能。

  要在非MFC程序中打开内存泄漏的检测功能非常容易,只须在程序的入口处添加以下代码:

  1. _CrtSetDbgFlag(  _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)  
  2.              |  _CRTDBG_LEAK_CHECK_DF               );

这样,在程序运行结束时,如果还有内存块没有释放,它们的信息就会被打印到Debug窗口里,如下面的代码片段所示:

  1. #include <crtdbg.h> 
  2.  
  3. #ifdef _DEBUG  
  4. #define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)  
  5. #endif  
  6. void EnableMemLeakCheck()  
  7. {  
  8.      _CrtSetDbgFlag( _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)  
  9.                  | _CRTDBG_LEAK_CHECK_DF              );  
  10. }  
  11.  
  12. int main()  
  13. {  
  14.      EnableMemLeakCheck();  
  15.      _CrtSetBreakAlloc(53);  
  16.      int* pLeak = new int[10];  
  17.  
  18.      return 0;  
  19. }

  在Debug模式下,程序退出时,内存块pLeak因为没有显式地释放,发生了内存泄漏,泄漏信息被打印出来:

  1. Detected memory leaks!  
  2. Dumping objects -> 
  3. main.cpp(26) : {53} normal block at 0x002E1508, 40 bytes long.  
  4. Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  
  5. Object dump complete.

  请读者思考一下,_CrtSetBreakAlloc(53)起到的是什么作用?

  目前这种方式只支持MS系统开发环境。当然,如果开发系统环境是Linux,也可以根据MS C-Runtime Library内建检测功能的实现方式开发出自己的Linux C-Runtime Library内建检测版本。

  外挂式的检测工具

  如果开发的是一个大型程序,MS C-Runtime Library提供的检测功能便显得有点笨拙了。此时,我们可以采用外挂式的检测工具BoundsChecker或Insure++。

  BoundsChecker采用的是一种被称为Code Injection的技术,来截获对分配内存和释放内存的函数的调用的。简单地说,当程序开始运行时,BoundsChecker的DLL被自动载入进程的地址空间中,然后它会修改进程中对内存分配和释放的函数调用,让这些调用首先转入它的代码,然后再执行原来的代码。BoundsChecker在做这些动作时,无须修改被调试程序的源代码或工程配置文件,这使得使用它非常简便、直接。而Insure++则是利用其专利技术(源码插装和运行时指针跟踪)来发现大量的内存操作错误,准确报告错误的源代码行和执行轨迹。

  如果开发环境是Linux,MS C-Runtime Library内建检测功能就会彻底失效,BoundsChecker或Insure++也无能为力。这时,外挂式的检测工具Rational Purify或Valgrind便派上了用场。

  Rational Purify主要是针对软件开发过程中难以发现的内存错误、运行时错误。它可以在软件开发过程中自动地发现错误,准确地定位错误,并提供完备的错误信息,从而减少调试时间。同时它也是市场上唯一支持多种平台的相关工具,并且可以和很多主流开发工具集成。Purify 可以检查应用的每一个模块,甚至可以查出复杂的多线程或进程应用中的错误。另外,它不仅可以检查 C/C++,还可以对 Java 或 .NET 中的内存泄漏问题给出报告。

 在 Linux 系统中,使用 Purify非常简单,只须重新编译程序:

purify g++ -g main.cpp -o LeakDetector

  运行编译生成的可执行文件LeakDetector,就可以定位出内存泄漏的具体位置。

  除了Rational Purify,Valgrind 也是Linux系统下开发应用程序时用于调试内存问题的有效工具。它尤其擅长发现内存管理的问题,检查发现程序运行时的内存泄漏。

  至于上述这些外挂式检测工具的具体使用方法就不赘述了。

  根据应用程序的具体情况,合理采用上述方法和工具,可以有效防止和查找代码中的内存泄漏问题,并且能和开发人员日常编码无缝结合,有效提高开发效率,增强应用程序鲁棒性。

  请记住:

  内存泄露是一个大问题,但是可以通过一定的方法或借助于专业的检测工具,来查找并发现这些问题,有效地提升程序员的开发效率。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值