【原创】利用C++ RAII技术自动回收堆内存
2014-03-16 11:34
162 查看
【说明】这篇文章本来发布在我个人网站的博客上,但由于:1,打算以cnblogs为家了;2. 关于智能指针部分需要修订,所有将修订版发在这里,作为第一篇文章。
如果这个函数func()逻辑比较简单,问题不大,但是当中间的代码有可能抛出异常时,上面的代码就会产生内存泄露(memory leak),如下面代码中第11行和12行将不会被执行。当然有码友会说用try-catch包起来就可以了,对,没错,但是代码中到处的try-catch也挺被人诟病的:
而且当函数有多个返回路径时,需要在每个return前都要调用delete去释放资源,代码也会变的不优雅了。
鉴于此,我们就要想办法利用C++的一些语言特性,在函数退栈时能够将局部申请的动态内存自动释放掉。熟悉C++的码友们都知道,当一个对象退出其定义的作用域时,会自动调用它的析构函数。也就是说如果我们在函数内定义一个局部对象,在函数返回前,甚至有异常产生时,这个局部对象的析构函数都会自动调用。如果我们能够将释放资源的代码交付给这个对象的析构函数,我们就可以实现资源的自动回收。这类技术,通常被称为RAII (初始化中获取资源)。
创建一个特殊类,在其构造函数初申请资源;
封装目标对象,将申请资源的目标对象作为这个特殊类的成员变量;
在这个类的析构函数内,释放资源。
一个典型的例子就是标准库中提供的模板类std::auto_ptr。如在《C++程序设计语言》(《The C++ Programming Language, Special Edition》, Bjarne Stroustrup著,裘宗燕译)中第327页所描述的。
想要使用它,非常简单,例如
另一个例子,是利用GCC中的cleanup attribute。它可以指定一个函数,在该变量退出作用域时可以执行。例如Wikipedia上提到的宏
我们可以这样使用,例如
还有一个例子,是在刘未鹏的博客文章”C++11 (及现代C++风格)和快速迭代式开发“中的”资源管理“一节中看到的,他借助C++11的std::function实现了这一特性。感兴趣的码友可以到他博客内阅读。
代码如下,
为了使用方便,封装两个宏:
使用起来很简单,例如
同样的,有时我们需要申请一个动态二维数组,所以也实现一个对应的auto_free_2D_ptr
下面是个例子
到这里就结束了,有些码友可能会说,何必这么麻烦,boost内有很多智能指针供选择,用share_ptr, scoped_ptr, scoped_array,unique_ptr, auto_ptr 中的一个不就行了吗? 没错!如果你正在开发的代码中,允许用boost,并且在相关程序接口统一都用智能指针来管理、不会用到源对象指针的话,当然优先选boost,但是当你的代码中由于历史原因,有些接口不可变更,且new/delete, malloc/free都存在,而且依然需要使用源对象指针来完成大部分工作时,不妨试试我设计的这个阉割版的scoped_ptr/scoped_array。总之,根据自己的实际情况来选择合适的方案,如果标准方案不适用,就自己写一个。
如果码友有更好的实现方式或者发现什么问题,还请批评指正。
常遇到的动态内存回收问题
在C++的编程过程中,我们经常需要申请一块动态内存,然后当用完以后将其释放。通常而言,我们的代码是这样的:void func()
{
//allocate a dynamic memory
int *ptr = new int;
//use ptr
//release allocated memory
delete ptr;
ptr = NULL;
}
如果这个函数func()逻辑比较简单,问题不大,但是当中间的代码有可能抛出异常时,上面的代码就会产生内存泄露(memory leak),如下面代码中第11行和12行将不会被执行。当然有码友会说用try-catch包起来就可以了,对,没错,但是代码中到处的try-catch也挺被人诟病的:
void func()
{
//allocate a dynamic memory
int *ptr = new int;
throw “error”; //just an example
//use ptr
//release allocated memory
delete ptr;
ptr = NULL;
}
而且当函数有多个返回路径时,需要在每个return前都要调用delete去释放资源,代码也会变的不优雅了。
void func()
{
//allocate a dynamic memory
int *ptr = new int;
if (...)
{
//...a
//release allocated memory
delete ptr;
ptr = NULL;
return;
} else if (....)
{
//...b
//release allocated memory
delete ptr;
ptr = NULL;
return;
}
//use ptr
//release allocated memory
delete ptr;
ptr = NULL;
}
鉴于此,我们就要想办法利用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页所描述的。
template<class X>
class std::auto_ptr{
public:
//在构造函数中,获得目标指针的管理权
explicit auto_ptr(X *p = 0) throw(){ ptr = p;}
//在析构函数中,释放目标指针
~auto_ptr() throw(){ delete ptr;}
//...
//重装*和->运算符,使auto_ptr对象像目标指针ptr一样使用
X& operator*() const throw(){ return *ptr;}
X* operator->() const throw(){ return ptr;}
//放弃对目标指针的管理权
X* release() throw(){ X* t = ptr; ptr = 0; return t;}
private:
X *ptr;
};
想要使用它,非常简单,例如
#include <memory>
void func()
{
std::auto_ptr<int> p(new int);
//use p just like ptr
return;
}
另一个例子,是利用GCC中的cleanup attribute。它可以指定一个函数,在该变量退出作用域时可以执行。例如Wikipedia上提到的宏
#define RAII_VARIABLE(vartype,varname,initval,dtor) \
void _dtor_ ## varname (vartype * v){ dtor(*v);} \
vartype varname __attribute__((cleanup(_dtor_ ## varname))) = (initval)
我们可以这样使用,例如
void example_usage(){
RAII_VARIABLE(FILE*, logfile, fopen("logfile.txt", "w+"), fclose);
fputs("hello logfile!", logfile);
}
还有一个例子,是在刘未鹏的博客文章”C++11 (及现代C++风格)和快速迭代式开发“中的”资源管理“一节中看到的,他借助C++11的std::function实现了这一特性。感兴趣的码友可以到他博客内阅读。
笔者采用的方法
对于new/delete,使用上面提到的std::auto_ptr就可以了,但是对于new/delete[]一个动态的一维数组,甚至二维数组,auto_ptr就无能为力了。而且在一些项目中,特别是一些有着悠久历史的代码中,还存在着使用malloc, new混用的现象。所以笔者设计了一个auto_free_ptr类,实现目标资源的自动回收。它的实现比较简单,只利用了RAII的第三个特点——”在类的析构函数内释放资源”,但有一个优点是可以在申请堆内存代码前使用。代码如下,
//auto_free_ptr is only used for automation free memory
template<class T>
class auto_free_ptr
{
public:
typedef enum{invalid, new_one, new_array, alloc_mem} EFLAG;
auto_free_ptr(){ initialize();}
~auto_free_ptr(){ free_ptr();}
///set the pointer needed to automatically free
inline void set_ptr(T** new_ptr_address, EFLAG new_eflag)
{ free_ptr(); p_ptr = new_ptr_address; eflag = new_eflag;}
///give up auto free memory
inline void give_up(){ initialize();}
protected:
inline void initialize(){ p_ptr = NULL; eflag = invalid;}
inline void free_ptr() throw()
{
if(!p_ptr || !(*p_ptr)) return;
switch(eflag)
{
case alloc_mem:{ free(*p_ptr),(*p_ptr) = NULL, p_ptr = NULL; break;}
case new_one:{ delete (*p_ptr), (*p_ptr) = NULL, p_ptr = NULL; break;}
case new_array:{ delete[] (*p_ptr),(*p_ptr) = NULL, p_ptr = NULL; break;}
}
}
protected:
T** p_ptr; //!< pointer to the address of the set pointer needed to automatically free
EFLAG eflag; //!< the type of allocation
private:
DISABLE_COPY_AND_ASSIGN(auto_free_ptr);
};
为了使用方便,封装两个宏:
// auto-free macros are mainly used to free the allocated memory by some local variables in the internal of function-body
#define AUTO_FREE_ENABLE( class, ptrName, ptrType ) \
auto_free_ptr<class> auto_free_##ptrName; \
auto_free_##ptrName.set_ptr(&ptrName,auto_free_ptr<class>::ptrType)
#define AUTO_FREE_DISABLE( ptrName ) auto_free_##ptrName.give_up()
使用起来很简单,例如
void func(int nLftCnt, int nRhtCnt)
{
if (!nLftCnt && !nRhtCnt)
return;
unsigned *pLftHashs = NULL;
unsigned *pRhtHashs = NULL;
//在申请堆内存之前,使用auto_free_ptr
AUTO_FREE_ENABLE(unsigned, pLftHashs, new_array);
AUTO_FREE_ENABLE(unsigned, pRhtHashs, new_array);
//....
if (nLftCnt)
{
pLftHashs = new unsigned[nLftCnt];
//...a
}
if (nRhtCnt)
{
pRhtHashs = new unsigned[nRhtCnt];
//...b
}
//....
if (...)
{
//因为下面这个函数可以释放资源,所以在它前面放弃对目标指针的管理权
AUTO_FREE_DISABLE(pLftHashs);
AUTO_FREE_DISABLE(pRhtHashs);
//这个函数可以释放资源
free_hash_arrays(pLftHashs, pRhtHashs);
}
}
同样的,有时我们需要申请一个动态二维数组,所以也实现一个对应的auto_free_2D_ptr
//auto_free_2D_ptr is only used for automation free memory of 2D array
template<class T>
class auto_free_2D_ptr
{
public:
typedef enum{invalid, new_one, new_array, alloc_mem} EFLAG;
auto_free_2D_ptr(){ initialize();}
~auto_free_2D_ptr(){ free_ptr();}
///set the pointer needed to automatically free
inline void set_ptr( T** new_ptr_address,EFLAG new_eflag, int new_length_row )
{ free_ptr(); p_ptr = new_ptr_address; eflag = new_eflag; length_row = new_length_row;}
//give up auto free memory
inline void give_up(){ initialize();}
protected:
inline void initialize(){ p_ptr = NULL; eflag = invalid; length_row = 0;}
inline void free_ptr() throw()
{
if(!p_ptr || !(*p_ptr)) return;
for(int i = 0; i < length_row; i++)
{
if(!(*p_ptr)[i]) continue;
switch(eflag)
{
case alloc_mem:{ free((*p_ptr)[i]); break;}
case new_one:{ delete (*p_ptr)[i]; break;}
case new_array:{ delete[] (*p_ptr)[i]; break;}
}
(*p_ptr)[i] = NULL;
}
switch(eflag)
{
case alloc_mem:{ free((*p_ptr)); break;}
default:{ delete[] (*p_ptr); break;}
}
(*p_ptr) = NULL, p_ptr = NULL;
}
protected:
T** p_ptr; //!< pointer to the address of the set pointer needed to automatically free
EFLAG eflag; //!< the type of allocation
int length_row; //!< the row length such as ptr[length_row][length_col]
private:
DISABLE_COPY_AND_ASSIGN(auto_free_2D_ptr);
};
#define AUTO_FREE_2D_ENABLE( class, ptrName, ptrType, rowNum ) \
auto_free_2D_ptr<class> auto_free_##ptrName; \
auto_free_##ptrName.set_ptr(&ptrName,auto_free_2D_ptr<class>::ptrType, rowNum)
#define AUTO_FREE_2D_DISABLE( ptrName ) AUTO_FREE_DISABLE( ptrName )
下面是个例子
void func(int row, int col)
{
if (!row && !col)
return;
int **ptr = new int*[ row ];
for( int r = 0; r < row; ++r ){ ptr[r] = new int[ col ];}
AUTO_FREE_2D_ENABLE( int, ptr, new_array, row );
//....
}
到这里就结束了,有些码友可能会说,何必这么麻烦,boost内有很多智能指针供选择,用share_ptr, scoped_ptr, scoped_array,unique_ptr, auto_ptr 中的一个不就行了吗? 没错!如果你正在开发的代码中,允许用boost,并且在相关程序接口统一都用智能指针来管理、不会用到源对象指针的话,当然优先选boost,但是当你的代码中由于历史原因,有些接口不可变更,且new/delete, malloc/free都存在,而且依然需要使用源对象指针来完成大部分工作时,不妨试试我设计的这个阉割版的scoped_ptr/scoped_array。总之,根据自己的实际情况来选择合适的方案,如果标准方案不适用,就自己写一个。
如果码友有更好的实现方式或者发现什么问题,还请批评指正。
相关文章推荐
- 利用C++ RAII技术自动回收堆内存
- 利用动态创建层技术实现无冗余代码为表单自动添加错误提示
- (原创)利用扩展方法,给 IEnumerable<T> 增加一个生成 Html 的 select 标签的方法,不用 C# 中的反射技术
- 利用HTML5技术通过百度地图实现网页定位签到考勤(原创)
- [原创]利用橡皮条技术画图
- php技术创新:利用动态404页面实现全站自动静态化
- 【原创】利用PHP5的类定义自动载入代替繁琐低效的的外部文件包含方式
- Canvas与Image互相转换示例以及利用该技术实现微信长按自动识别二维码功能
- 浅谈如何利用PB实现仿QQ自动显示/隐藏窗口(原创)
- 利用HTML5与jQuery技术创建一个简单的自动表单完成
- [原创]C/S模式开发中利用WebClient自动升级
- 【算法】利用随机化算法对顺序表进行搜索【原创技术】
- AK-47 制造商 Kalashnikov 已成功研发 AI 武器 以 AK-47 闻名世界的俄罗斯军火商 Kalashnikov 近日宣布,其已成功研发全自动武器模块,能够利用人工智能技术识别目
- ASP.NET自动检测用户名是否注册(利用微软AJAX控件技术)
- 利用动态创建层技术实现无冗余代码为表单自动添加错误提示
- 利用HTML5与jQuery技术创建一个简单的自动表单完成
- 深度剖析C++对象池自动回收技术实现
- 利用 devcon.exe实现自动安装驱动的心得总结(原创)
- [原创]WCF技术剖析之五:利用ASP.NET兼容模式创建支持会话(Session)的WCF服务
- 【原创】自动更新程序1--网站的部署(技术:spring.net+三层架构+webservice+IrisSkin2换肤)