引言:
由於 C++ 語言沒有自動內存回收機制,程序員每次 new 出來的內存都要手動 delete。程序員忘記 delete,流程太複雜,最終導致沒有 delete,異常導致程序過早退出,沒有執行 delete 的情況並不罕見。
RAII(Resource Acquisition Is Initialization)
資源分配即初始化,定義一個類來封裝資源的分配和釋放,在構造函數完成資源的分配和初始化,在析構函數完成資源的清理,可以保證資源的正確初始化和釋放。
所謂智能指針就是智能/自動化的管理指針所指向的動態資源的釋放。
STL--auto_ptr
Boost庫的智能指針(ps:新的C++11標準中已經引入了unique_ptr/shared_ptr/weak_ptr)
在這裏,對於aut_optr,scoped_ptr , shared_ptr 進行剖析
一. aut_optr
std::auto_ptr 屬於 STL,當然在 namespace std 中,包含頭文件 #include<memory> 便可以使用。std::auto_ptr 能夠方便的管理單個堆內存對象。爲了解決單個對象被重複釋放多次的情況,我們使用的aut_optr的目的就是轉移權限,顧名思義就是說再拷貝構造新的對象時,由於此時有兩個對象指向同一塊空間,所以將原來的指針賦NULL,然後將這塊空間的所有權交給新的對象指針。
對應代碼:<AutoPtr>
#include<iostream> using namespace std; template<class T> class AutoPtr { public: AutoPtr(T* ptr) :_ptr(ptr) {} AutoPtr() :_ptr(NULL) {} AutoPtr<T>(AutoPtr<T>& ap) //權限轉移 : _ptr(ap._ptr) { ap._ptr = NULL; } AutoPtr<T>& operator=(AutoPtr<T>& ap) { if (&ap != this) { delete _ptr; _ptr = ap._ptr; ap._ptr = NULL; //權限轉移 } return *this; } ~AutoPtr() { if (_ptr) { delete _ptr; _ptr = NULL; } } T& operator*() { return *_ptr; } private: T* _ptr; }; void Test() { AutoPtr<int> ap1(new int(2)); AutoPtr<int> ap2 = ap1; AutoPtr<int> ap3(new int(3)); ap3 = ap1; } int main() { Test(); return 0; }
雖然表面上解決了重複釋放的問題,但是卻存在一個很大問題,請看右圖:
(先忽略 s 的存在)
若有s2對象,指向自己的一塊空間,現在要拷貝構造一個s3對象,那麼順應上面的代碼,s2會被置NULL,而這塊空間重歸s3所管理。
如果考慮到這塊空間已經有多個指針指向,即s和s2,那麼再拷貝構造s3時,s2被置空,此時,s變成垂懸指針。這就是一個打的問題所在!
二. scoped_ptr
實用的智能指針,思想就是防拷貝,在大多時候用不到拷貝構造和賦值運算符重載,那麼我們做的就是寫出構造函數和析構函數,拷貝構造和賦值運算符重載只聲明不定義。這裏有幾點要說明:
<1>在代碼中,將拷貝構造和賦值運算符重載設置成保護或者私有的,爲什麼?世界之大,總有些人想害朕。。話歸主題,原因是防止有壞人在外面修改,倘若被修改也不會調用修改的,因爲無法訪問得到。
<2>爲什麼要聲明拷貝構造和賦值函數,原因是如果不聲明,編譯器會調用自己默認的構造和賦值函數,那麼防拷貝就無從談起。
代碼如下:<ScopedPtr>
#include<iostream> using namespace std; template<class T> class ScopedPtr { public: ScopedPtr(T* ptr) :_ptr(ptr) {} Scoped() :_ptr(NULL) {} ~ScopedPtr() { if (_ptr) { delete _ptr; _ptr = NULL; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T* GetPtr() { return _ptr; } protected: ScopedPtr<T>(const ScopedPtr<T>& sp);// 只聲明不定義,防拷貝 ScopedPtr<T>& operator = (const ScopedPtr<T>& sp); private: T* _ptr; }; void Test() { ScopedPtr<int> sp1(new int(2)); ScopedPtr<int> sp2 = sp1; ScopedPtr<int> sp3(new int(3)); sp3 = sp1; } int main() { Test(); return 0; }
auto_ptr和scopedptr的取捨:
boost::scoped_ptr 用於確保動態分配的對象能夠被正確地刪除。scoped_ptr 有着與std::auto_ptr類似的特性,而最大的區別在於它不能轉讓所有權而auto_ptr可以。事實上,scoped_ptr永遠不能被複制或被賦值!scoped_ptr 擁有它所指向的資源的所有權,並永遠不會放棄這個所有權。scoped_ptr的這種特性提升了我們的代碼的表現,我們可以根據需要選擇最合適的智能指針(scoped_ptr 或 auto_ptr)。要決定使用std::auto_ptr還是boost::scoped_ptr, 就要考慮轉移所有權是不是你想要的智能指針的一個特性。如果不是,就用scoped_ptr. 它是一種輕量級的智能指針;使用它不會使你的程序變大或變慢。它只會讓你的代碼更安全,更好維護。
三. sharede_ptr
在上面我們看到 boost::scoped_ptr 獨享所有權,不允許賦值、拷貝,boost::shared_ptr 是專門用於共享所有權的,由於要共享所有權,其在內部使用了引用計數。boost::shared_ptr 也是用於管理單個堆內存對象的。
這個智能指針解決了auto_ptr獨佔的問題,採用引用計數的方法,一旦最後一個這樣的指針被銷燬,也就是一旦某個對象的引用計數變爲0,這個對象會被自動刪除。這在非環形數據結構中防止資源泄露很有幫助。
用法:刪除共用對象
對應代碼:<SharedPtr>
#include<iostream> using namespace std; template<class T> class SharedPtr { public: SharedPtr(T* ptr) :_ptr(ptr) , _pCount(new long(1)) {} SharedPtr() :_ptr(NULL) , _pCount(new long(1)) {} SharedPtr<T>(const SharedPtr<T>& sp) : _ptr(sp._ptr) , _pCount(sp._pCount) { ++(*_pCount); } SharedPtr<T>& operator=(const SharedPtr<T>& sp) { if (&sp != this) { if (--(*_pCount) == 0) // 減到0釋放對象一次 { delete _ptr; delete _pCount; } _ptr = sp._ptr; _pCount = sp._pCount; ++(*_pCount); } return *this; } ~SharedPtr() { if (_ptr) { if (--(*_pCount) == 0) { delete _ptr; delete _pCount; } } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } long GetCount() { return *(_pCount); } T* GetPtr() { return _ptr; } private: T* _ptr; long* _pCount; }; void Test() { SharedPtr<int> sp1 = new int(1); SharedPtr<int> sp2 = sp1; SharedPtr<int> sp3 = new int(2); sp3 = sp1; } int main() { Test(); return 0; }
但是shared_ptr看起來完美,但是也存在一下問題:
1. 引用計數更新存在着線程安全
2.循環引用
3.定置刪除器
我們先在這裏討論第二種情況,即循環引用問題
我們引出循環引用的場景圖:
此時,兩個節點的引用計數都爲2,因爲有兩個指針分別指向a和b,此時就有了一個問題,_next析構時等着_prev析構,_prev要析構,只能b析構;而_prev也在等_next析構,_next要析構,只能a析構。這樣就有個問題類似”你等我,我等你,無休止,永遠不會析構,一直循環“。
要解決這個問題,就又要引出弱指針:weak_ptr
weak_ptr唯一的功能就是解決shared_ptr的循環引用問題,那是怎麼解決的呢?
我們看代碼:
#include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> using namespace boost; struct ListNode { shared_ptr<ListNode > _prev; shared_ptr<ListNode > _next; //weak_ptr<ListNode > _prev; // 在此,weak_ptr可以解決循環引用的問題 //weak_ptr<ListNode > _next; ~ ListNode() { cout<<"~ListNode()" <<endl; } }; void Test () { // 循環引用問題 shared_ptr <ListNode > p1( new ListNode ()); shared_ptr <ListNode > p2( new ListNode ()); cout <<"p1->Count:" << p1. use_count()<<endl ;// use_count是庫裏的引用計數 cout <<"p2->Count:" << p2. use_count()<<endl ; // p1節點的_next指向 p2節點 p1->_next = p2; // p2節點的_prev指向 p1節點 p2->_prev = p1; cout <<"p1->Count:" << p1. use_count ()<<endl ; cout <<"p2->Count:" << p2. use_count ()<<endl ; }
這樣問題就得到了解決。
至此,我們也知道了,當需要寫一個智能指針時,我們儘可能的去寫scoped_ptr或者shared_ptr,而千萬不要寫auto_ptr,問題上面已經詳細分解。
偏文不到之處,還請評正。