【说明】这篇文章本来发布在我个人网站的博客上,但由于:1,打算以cnblogs为家了;2. 关于智能指针部分需要修订,所有将修订版发在这里,作为第一篇文章。

常遇到的动态内存回收问题

在C++的编程过程中,我们经常需要申请一块动态内存,然后当用完以后将其释放。通常而言,我们的代码是这样的:

   1: void func()

   2: {

   3:     //allocate a dynamic memory

   4:     int *ptr = new int;

   5:  

   6:     //use ptr

   7:  

   8:     //release allocated memory

   9:     delete ptr;

  10:     ptr = NULL;

  11: }

如果这个函数func()逻辑比较简单,问题不大,但是当中间的代码有可能抛出异常时,上面的代码就会产生内存泄露(memory leak),如下面代码中第11行和12行将不会被执行。当然有码友会说用try-catch包起来就可以了,对,没错,但是代码中到处的try-catch也挺被人诟病的:

   1: void func()

   2: {

   3:     //allocate a dynamic memory

   4:     int *ptr = new int;

   5:  

   6:     throw “error”; //just an example

   7:  

   8:     //use ptr

   9:  

  10:     //release allocated memory

  11:     delete ptr;

  12:     ptr = NULL;

  13: }

而且当函数有多个返回路径时,需要在每个return前都要调用delete去释放资源,代码也会变的不优雅了。

   1: void func()

   2: {

   3:     //allocate a dynamic memory

   4:     int *ptr = new int;

   5:  

   6:     if (...)

   7:     {

   8:         //...a

   9:  

  10:         //release allocated memory

  11:         delete ptr;

  12:         ptr = NULL;

  13:         return;

  14:     } else if (....)

  15:     {

  16:         //...b

  17:  

  18:         //release allocated memory

  19:         delete ptr;

  20:         ptr = NULL;

  21:         return;

  22:     }

  23:  

  24:     //use ptr

  25:  

  26:     //release allocated memory

  27:     delete ptr;

  28:     ptr = NULL;

  29: }

鉴于此,我们就要想办法利用C++的一些语言特性,在函数退栈时能够将局部申请的动态内存自动释放掉。熟悉C++的码友们都知道,当一个对象退出其定义的作用域时,会自动调用它的析构函数。也就是说如果我们在函数内定义一个局部对象,在函数返回前,甚至有异常产生时,这个局部对象的析构函数都会自动调用。如果我们能够将释放资源的代码交付给这个对象的析构函数,我们就可以实现资源的自动回收。这类技术,通常被称为RAII (初始化中获取资源)。

什么是RAII以及几个例子

在C++等面向对象语言中,为了管理局部资源的分配以及释放(resource allocation and deallocation),实现异常安全(exception-safe)、避免内存泄露等问题,C++之父Bjarne Stroustrup发明了一种叫做”初始化中获取资源“ (RAII, Resource Acquisition Is Initialization,也可以叫做Scope-Bound Resource Management)的技术。简单来说,它的目的就是利用一个局部对象,在这个对象的构造函数内分配资源,然后在其析构函数内释放资源。这样,当这个局部对象退出作用域时,它所对应的的资源即可自动释放。在实现上,它通常有三个特点:

  • 创建一个特殊类,在其构造函数初申请资源;
  • 封装目标对象,将申请资源的目标对象作为这个特殊类的成员变量;
  • 在这个类的析构函数内,释放资源。

一个典型的例子就是标准库中提供的模板类std::auto_ptr。如在《C++程序设计语言》(《The C++ Programming Language, Special Edition》, Bjarne Stroustrup著,裘宗燕译)中第327页所描述的。

   1: template<class X>

   2: class std::auto_ptr {

   3:  

   4: public:

   5:     //在构造函数中,获得目标指针的管理权

   6:     explicit auto_ptr(X *p = 0) throw() { ptr = p; }

   7:     //在析构函数中,释放目标指针

   8:     ~auto_ptr() throw() { delete ptr; }

   9:  

  10:     //...

  11:  

  12:     //重装*和->运算符,使auto_ptr对象像目标指针ptr一样使用

  13:     X& operator*() const throw() { return *ptr; }

  14:     X* operator->() const throw() { return ptr; }

  15:  

  16:     //放弃对目标指针的管理权

  17:     X* release() throw() { X* t = ptr; ptr = 0; return t; }

  18:  

  19: private:

  20:     X *ptr;

  21: };

想要使用它,非常简单,例如

   1: #include <memory>

   2:  

   3: void func()

   4: {

   5:     std::auto_ptr<int> p(new int);

   6:  

   7:     //use p just like ptr

   8:  

   9:     return;

  10: }

另一个例子,是利用GCC中的cleanup attribute。它可以指定一个函数,在该变量退出作用域时可以执行。例如Wikipedia上提到的宏

   1: #define RAII_VARIABLE(vartype,varname,initval,dtor) \

   2:     void _dtor_ ## varname (vartype * v) { dtor(*v); } \

   3:     vartype varname __attribute__((cleanup(_dtor_ ## varname))) = (initval)

我们可以这样使用,例如

   1: void example_usage() {

   2:   RAII_VARIABLE(FILE*, logfile, fopen("logfile.txt", "w+"), fclose);

   3:   fputs("hello logfile!", logfile);

   4: }

还有一个例子,是在刘未鹏的博客文章”C++11 (及现代C++风格)和快速迭代式开发“中的”资源管理“一节中看到的,他借助C++11的std::function实现了这一特性。感兴趣的码友可以到他博客内阅读。

笔者采用的方法

对于new/delete,使用上面提到的std::auto_ptr就可以了,但是对于new/delete[]一个动态的一维数组,甚至二维数组,auto_ptr就无能为力了。而且在一些项目中,特别是一些有着悠久历史的代码中,还存在着使用malloc, new混用的现象。所以笔者设计了一个auto_free_ptr类,实现目标资源的自动回收。它的实现比较简单,只利用了RAII的第三个特点——”在类的析构函数内释放资源”,但有一个优点是可以在申请堆内存代码前使用。

代码如下,

   1: //auto_free_ptr is only used for automation free memory

   2: template<class T>

   3: class auto_free_ptr

   4: {

   5: public:

   6:     typedef enum {invalid, new_one, new_array, alloc_mem} EFLAG;

   7:     auto_free_ptr() { initialize();  }

   8:     ~auto_free_ptr(){ free_ptr();    }

   9:  

  10:     ///set the pointer needed to automatically free

  11:     inline void set_ptr(T** new_ptr_address, EFLAG new_eflag)

  12:     { free_ptr(); p_ptr = new_ptr_address; eflag = new_eflag; }

  13:  

  14:     ///give up auto free memory

  15:     inline void give_up() { initialize(); }

  16:  

  17: protected:

  18:     inline void initialize() { p_ptr = NULL; eflag = invalid; }

  19:     inline void free_ptr() throw()

  20:     {

  21:         if(!p_ptr || !(*p_ptr)) return;

  22:  

  23:         switch(eflag)

  24:         {

  25:         case alloc_mem:  { free(*p_ptr),     (*p_ptr) = NULL, p_ptr = NULL; break; }

  26:         case new_one:    { delete (*p_ptr),  (*p_ptr) = NULL, p_ptr = NULL; break; }

  27:         case new_array:  { delete[] (*p_ptr),(*p_ptr) = NULL, p_ptr = NULL; break; }

  28:         }

  29:     }

  30:  

  31: protected:

  32:     T** p_ptr;   //!< pointer to the address of the set pointer needed to automatically free

  33:     EFLAG eflag; //!< the type of allocation

  34:  

  35: private:

  36:     DISABLE_COPY_AND_ASSIGN(auto_free_ptr);

  37: };

为了使用方便,封装两个宏:

   1: // auto-free macros are mainly used to free the allocated memory by some local variables in the internal of function-body

   2: #define AUTO_FREE_ENABLE( class, ptrName, ptrType ) \

   3:     auto_free_ptr<class> auto_free_##ptrName; \

   4:     auto_free_##ptrName.set_ptr(&ptrName,auto_free_ptr<class>::ptrType)

   5:  

   6: #define AUTO_FREE_DISABLE( ptrName ) auto_free_##ptrName.give_up()

使用起来很简单,例如

   1: void func(int nLftCnt, int nRhtCnt)

   2: {

   3:     if (!nLftCnt && !nRhtCnt)

   4:         return;

   5:  

   6:     unsigned *pLftHashs = NULL;

   7:     unsigned *pRhtHashs = NULL;

   8:  

   9:     //在申请堆内存之前,使用auto_free_ptr

  10:     AUTO_FREE_ENABLE(unsigned, pLftHashs, new_array);

  11:     AUTO_FREE_ENABLE(unsigned, pRhtHashs, new_array);

  12:  

  13:     //....

  14:  

  15:     if (nLftCnt)

  16:     {

  17:         pLftHashs = new unsigned[nLftCnt];

  18:         //...a

  19:     }

  20:  

  21:     if (nRhtCnt)

  22:     {

  23:         pRhtHashs = new unsigned[nRhtCnt];

  24:         //...b

  25:     }

  26:  

  27:     //....

  28:  

  29:     if (...)

  30:     {

  31:         //因为下面这个函数可以释放资源,所以在它前面放弃对目标指针的管理权

  32:         AUTO_FREE_DISABLE(pLftHashs);

  33:         AUTO_FREE_DISABLE(pRhtHashs);

  34:  

  35:         //这个函数可以释放资源

  36:         free_hash_arrays(pLftHashs, pRhtHashs);

  37:     }

  38: }

同样的,有时我们需要申请一个动态二维数组,所以也实现一个对应的auto_free_2D_ptr

   1: //auto_free_2D_ptr is only used for automation free memory of 2D array

   2: template<class T>

   3: class auto_free_2D_ptr

   4: {

   5: public:

   6:     typedef enum {invalid, new_one, new_array, alloc_mem} EFLAG;

   7:     auto_free_2D_ptr()  { initialize(); }

   8:     ~auto_free_2D_ptr() { free_ptr();   }

   9:     

  10:     ///set the pointer needed to automatically free

  11:     inline void set_ptr( T** new_ptr_address,EFLAG new_eflag, int new_length_row )

  12:     { free_ptr(); p_ptr = new_ptr_address; eflag = new_eflag; length_row = new_length_row; }

  13:  

  14:     //give up auto free memory

  15:     inline void give_up() { initialize(); }

  16:  

  17: protected:

  18:     inline void initialize() { p_ptr = NULL; eflag = invalid; length_row = 0;}

  19:     inline void free_ptr() throw()

  20:     {

  21:         if(!p_ptr || !(*p_ptr)) return;

  22:  

  23:         for(int i = 0; i < length_row; i++)

  24:         {    

  25:             if(!(*p_ptr)[i]) continue;

  26:             switch(eflag)

  27:             {

  28:             case alloc_mem:  { free((*p_ptr)[i]);    break; }

  29:             case new_one:    { delete (*p_ptr)[i];   break; }

  30:             case new_array:  { delete[] (*p_ptr)[i]; break; }

  31:             }

  32:             (*p_ptr)[i] = NULL;

  33:         }

  34:         switch(eflag)

  35:         {

  36:         case alloc_mem: { free((*p_ptr));    break; }

  37:         default:        { delete[] (*p_ptr); break; }

  38:         }

  39:         (*p_ptr) = NULL, p_ptr = NULL; 

  40:     }

  41:  

  42: protected:

  43:     T** p_ptr;      //!< pointer to the address of the set pointer needed to automatically free

  44:     EFLAG eflag;    //!< the type of allocation

  45:     int length_row; //!< the row length such as ptr[length_row][length_col]

  46:  

  47: private:

  48:     DISABLE_COPY_AND_ASSIGN(auto_free_2D_ptr);

  49: };

  50:  

  51: #define AUTO_FREE_2D_ENABLE( class, ptrName, ptrType, rowNum ) \

  52:     auto_free_2D_ptr<class> auto_free_##ptrName; \

  53:     auto_free_##ptrName.set_ptr(&ptrName,auto_free_2D_ptr<class>::ptrType, rowNum)

  54:  

  55: #define AUTO_FREE_2D_DISABLE( ptrName )  AUTO_FREE_DISABLE( ptrName )

下面是个例子

   1: void func(int row, int col)

   2: {

   3:     if (!row && !col)

   4:         return;

   5:  

   6:     int **ptr = new int*[ row ];

   7:     for( int r = 0; r < row; ++r ) { ptr[r] = new int[ col ];}

   8:  

   9:     AUTO_FREE_2D_ENABLE( int, ptr, new_array, row );

  10:  

  11:     //....

  12: }

到这里就结束了,有些码友可能会说,何必这么麻烦,boost内有很多智能指针供选择,用share_ptr, scoped_ptr, scoped_array,unique_ptr, auto_ptr 中的一个不就行了吗? 没错!如果你正在开发的代码中,允许用boost,并且在相关程序接口统一都用智能指针来管理、不会用到源对象指针的话,当然优先选boost,但是当你的代码中由于历史原因,有些接口不可变更,且new/delete, malloc/free都存在,而且依然需要使用源对象指针来完成大部分工作时,不妨试试我设计的这个阉割版的scoped_ptr/scoped_array。总之,根据自己的实际情况来选择合适的方案,如果标准方案不适用,就自己写一个。

如果码友有更好的实现方式或者发现什么问题,还请批评指正。

【原创】利用C++ RAII技术自动回收堆内存的更多相关文章

  1. 利用C++ RAII技术自动回收堆内存

    在C++的编程过程中,我们经常需要申请一块动态内存,然后当用完以后将其释放.通常而言,我们的代码是这样的: 1: void func() 2: { 3: //allocate a dynamic me ...

  2. Java堆内存是线程共享的!面试官:你确定吗?

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ...

  3. 微信团队原创分享:iOS版微信的内存监控系统技术实践

    本文来自微信开发团队yangyang的技术分享. 一.前言 FOOM(Foreground Out Of Memory),是指App在前台因消耗内存过多引起系统强杀.对用户而言,表现跟crash一样. ...

  4. 利用jmap和MAT等工具查看JVM运行时堆内存

    jmap JDK自带了一些工具可以帮助我们查看JVM运行的堆内存情况,常用的是jmap命令 jmap -heap <pid> 打印堆的使用情况 那么,从这个输出中我们也可以大致看出堆的结构 ...

  5. 栈 堆 stack heap 堆内存 栈内存 内存分配中的堆和栈 掌握堆内存的权柄就是返回的指针 栈是面向线程的而堆是面向进程的。 new/delete and malloc/ free 指针与内存模型

    小结: 1.栈内存 为什么快? Due to this nature, the process of storing and retrieving data from the stack is ver ...

  6. java中栈内存与堆内存(JVM内存模型)

    java中栈内存与堆内存(JVM内存模型) Java中堆内存和栈内存详解1 和 Java中堆内存和栈内存详解2 都粗略讲解了栈内存和堆内存的区别,以及代码中哪些变量存储在堆中.哪些存储在栈中.内存中的 ...

  7. Linux堆内存管理深入分析(下)

     Linux堆内存管理深入分析 (下半部) 作者@走位,阿里聚安全 0 前言回顾 在上一篇文章中(链接见文章底部),详细介绍了堆内存管理中涉及到的基本概念以及相互关系,同时也着重介绍了堆中chunk分 ...

  8. Linux堆内存管理深入分析(上)

    Linux堆内存管理深入分析(上半部) 作者:走位@阿里聚安全   0 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞 ...

  9. Linux堆内存管理深入分析

    (上半部) 作者:走位@阿里聚安全 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞利用两种.国内关于栈溢出的资料相对较 ...

随机推荐

  1. 如何检查oracle的归档空间是否满了?

    如何检查oracle的归档空间是否满了? 关于如何检查归档空间是否慢了,大多数人会去先检查放归档的目录的磁盘空间是否满了,通过该归档目录空余情况来判断归档空间是否满了,但我觉得这个方法不一定代表实际情 ...

  2. HDU1963Investment(DP)

    简单DP,题解见代码

  3. .java 文件中只能定义一个public class 且与文件名相同

  4. sql表设计器的几个默认值

    sql表设计器的几个默认值: 空字符串‘’(注意是单引号) 当前时间getdate() 逻辑值0或1 汉字或英文字符串需在前面加大写N,并用单引号引起如: N'已发货'

  5. 16.ARC

    Swift 使用自动引用计数(ARC)机制来跟踪和管理应用程序的内存.通常情况下,Swift 内存管理机制会一直起作用,我们无须自己来考虑内存的管理.ARC 会在类的实例不再被使用时,自动释放其占用的 ...

  6. Ehcache(05)——缓存的查询

    http://haohaoxuexi.iteye.com/blog/2117505 缓存的查询 目录 1.    使Cache可查询 1.1     基于Xml配置 1.2     基于代码的配置 2 ...

  7. AVR JTAG MKii 引脚布局 ( JTAG 和 ISP )

    1,JTAG和ISP引脚复用,JTAG的TCK,TDO,TDI分别与ISP的SCK,MISO,MOSI复用:2,VT引脚必须接目标板电源,不然仿真器接口电路不工作: JTAG接口引脚定义如下 标配的J ...

  8. 不要滥用div,保持代码的整洁

    这篇文章算是很基础的了.旨在介绍如何保证页面代码的整洁.以维护性.使用有语义的页面标签,减少标签的滥用. 1. 移除不必要的<div>标签 嵌套在<form><ul> ...

  9. asp.net解决:当前上下文中不存在名称“Session”

    第一种方法:将使用session的类页面继承 System.Web.UI.Page类,方法:public class AddUser : System.Web.UI.Page 第二种方法:在page里 ...

  10. dwr2反推

    package services; import org.directwebremoting.Browser; import org.directwebremoting.ScriptSessions; ...