【原創】利用C++ RAII技術自動回收堆內存


【說明】這篇文章本來發布在我個人網站的博客上,但由於: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。總之,根據自己的實際情況來選擇合適的方案,如果標准方案不適用,就自己寫一個。

如果碼友有更好的實現方式或者發現什么問題,還請批評指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM