C++11 智能指針的具體使用
智能指針的原理
RAII
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最後在對象析構的時候釋放資源。 借此,我們實際上把管理一份資源的責任托管給瞭一個對象。這種做法有兩大好處:
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內始終保持有效。
我們使用RAII的思想設計SmartPtr類:
template <class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } private: T* _ptr; };
智能指針的原理
上述的SmartPtr還不能將其稱為智能指針,因為它還不具有指針的行為。指針可以解引用,也可以通過->去訪問所指空間中的內容 ,因此:SmartPtr模板類中還得需要將* 、->重載下,才可讓其像指針一樣去使用。
template <class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} T& operator*() { return *_ptr; } T* operator->() { return _ptr; } ~SmartPtr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } private: T* _ptr; };
智能指針使用:
總結智能指針的原理:
- RAII特性
- 重載operator*和opertaor->,具有像指針一樣的行為。
auto_ptr
1.auto_ptr的使用及問題
auto_ptr的頭文件#include<memory>
auto_ptr的使用:
為什麼此時訪問sp的成員時會報錯呢?我們來看看它們的地址。
我們發現在拷貝構造之後,sp管理的地址為空,而sp1管理的地址是之前sp所管理的地址,管理權發生瞭轉移。那麼上面所說的報錯也很容易想通,因為sp管理的地址為空,不能進行訪問。
auto_ptr的問題:當對象拷貝或者賦值後,管理權進行轉移,造成前面的對象懸空。auto_ptr問題是非常明顯的,所以實際中很多公司明確規定瞭不能使用auto_ptr。
auto_ptr的實現原理:管理權轉移的思想,下面簡化模擬實現瞭一份AutoPtr來瞭解它的原理:
template<class T> class AutoPtr { public: AutoPtr(T* ptr) :_ptr(ptr) {} //拷貝:管理權轉移 AutoPtr(AutoPtr<T> &sp) :_ptr(sp._ptr) { sp._ptr = nullptr; } //賦值:管理權轉移 AutoPtr& operator=(AutoPtr<T> &sp) { if (this != &sp) { if (_ptr) delete _ptr; _ptr = sp._ptr; sp._ptr = nullptr; } return *this; } ~AutoPtr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr; };
unique_ptr
為瞭解決拷貝或者賦值時管理權轉移的問題,出現瞭unique_ptr。
unique_ptr解決問題的方式非常粗暴:防拷貝,也就是不讓賦值和拷貝
unique_ptr的使用:
unique_ptr的實現原理:簡單粗暴的防拷貝,下面簡化模擬實現瞭一份UniquePtr來瞭解它的原理:
template<class T> class UniquePtr { public: UniquePtr(T* ptr) :_ptr(ptr) {} // C++11防拷貝的方式:delete UniquePtr(const UniquePtr<T> &) = delete; UniquePtr& operator=(const UniquePtr<T>&) = delete; T* operator->() { return _ptr; } T& operator*() { return *_ptr; } ~UniquePtr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } private: //C++98防拷貝的方式:隻聲明不實現+聲明成私有 //UniquePtr(UniquePtr<T> const &); //UniquePtr& operator=(UniquePtr<T> const &); T* _ptr; };
shared_ptr
c++11中提供更靠譜的並且支持拷貝的shared_ptr
shared_ptr的使用
shared_ptr中拷貝與賦值都是沒有問題的。
shared_ptr的原理
- shared_ptr的原理:是通過引用計數的方式來實現多個shared_ptr對象之間共享資源。shared_ptr在其內部,給每個資源都維護瞭著一份計數,用來記錄該份資源被幾個對象共享。
- 在對象被銷毀時(也就是析構函數調用),就說明自己不使用該資源瞭,對象的引用計數減一。
- 如果引用計數是0,就說明自己是最後一個使用該資源的對象,必須釋放該資源;
- 如果不是0,就說明除瞭自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針瞭。
shared_ptr中成員函數:use_count(對象數據的引用計數)
示例:
示例詳解:
利用引用計數簡單的實現SharedPtr,瞭解原理:
template<class T> class SharedPtr { public: SharedPtr(T* ptr) :_ptr(ptr) ,_count(new int(1)) {} SharedPtr(const SharedPtr<T> &sp) :_ptr(sp._ptr) ,_count(sp._count) { //計數器累加 ++(*_count); } SharedPtr& operator=(const SharedPtr<T> &sp) { //判斷管理的是否是同一份資源 if (_ptr != sp._ptr) { //計數-1,判斷之前管理的資源是否需要釋放 if ((--(*_count)) == 0) { delete _ptr; delete _count; } _ptr = sp._ptr; _count = sp._count; //計數器累加 ++(*_count); } return *this; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } ~SharedPtr() { if (--(*_count) == 0) { delete _ptr; delete _count; _ptr = nullptr; _count = nullptr; } } private: T* _ptr; int* _count;//給每份資源開辟一個計數器 };
但是還存在一個線程安全的問題:
- 智能指針對象中引用計數是多個智能指針對象共享的,兩個線程中智能指針的引用計數同時++或–,這個操作不是原子的,引用計數原來是1,++瞭兩次,可能還是2。這樣引用計數就錯亂瞭。會導致資源未釋放或者程序崩潰的問題。所以隻能指針中引用計數++、- -是需要加鎖的,也就是說引用計數的操作是線程安全的。
- 智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。
這裡我們通過加鎖來解決線程安全問題:
template<class T> class SharedPtr { public: SharedPtr(T* ptr) :_ptr(ptr) ,_count(new int(1)) ,_mutex(new mutex) {} SharedPtr(const SharedPtr<T> &sp) :_ptr(sp._ptr) ,_count(sp._count) ,_mutex(sp._mutex) { //計數器累加 AddCount(); } SharedPtr& operator=(const SharedPtr<T> &sp) { //判斷管理的是否是同一份資源 if (_ptr != sp._ptr) { //計數-1,判斷之前管理的資源是否需要釋放 if (SubCount() == 0) { delete _ptr; delete _count; delete _mutex; } _ptr = sp._ptr; _count = sp._count; _mutex = sp._mutex; //計數器累加 AddCount(); } return *this; } //線程安全的累加器 int AddCount() { //加鎖 _mutex->lock(); ++(*_count); _mutex->unlock(); return *_count; } int SubCount() { _mutex->lock(); --(*_count); _mutex->unlock(); return *_count; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } ~SharedPtr() { if (SubCount() == 0) { delete _ptr; delete _count; delete _mutex; _ptr = nullptr; _count = nullptr; _mutex = nullptr; } } private: T* _ptr; int* _count;//給每份資源開辟一個計數器 mutex* _mutex; //每一份資源有一個獨立的鎖 };
shared_ptr的循環引用
循環引用的場景:
struct ListNode { int _data; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> node2(new ListNode); node1->_next = node2; node2->_prev = node1; }
node1和node2兩個智能指針對象指向兩個節點,兩個節點的引用計數都是1。node1->next指向node2,node2->prev指向node1,兩個節點的引用計數都變成2。程序運行完之後,析構node1和node2,node1和node2所指向的節點引用計數分別減1,但是node1->next指向下面節點,node2->prev指向上面節點,此時,兩個節點的引用計數都為1,所以兩個節點不能析構。
引用計數為0時,如果要析構node1節點,就先要去析構node1中的自定義結構,然後再析構node1。也就是說node1->next析構瞭,node2就釋放瞭;node2->prev析構瞭,node1就釋放瞭。但是_next屬於node的成員,node1釋放瞭,_next才會析構,而node1由_prev管理,_prev屬於node2成員,所以這就叫循環引用,誰也不會釋放。
解決方案:
在引用計數的場景下,把節點中的_prev和_next改成weak_ptr就可以瞭
原理就是,node1->_next = node2和node2->_prev = node1時weak_ptr的_next和_prev不會增加node1和node2的引用計數。
weak_ptr最大作用就是解決shared_ptr的循環引用
struct ListNode { int _data; weak_ptr<ListNode> _prev; weak_ptr<ListNode> _next; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> node2(new ListNode); node1->_next = node2; node2->_prev = node1; }
註意:
weak_ptr不能單獨使用,可以用shared_ptr創建
//weak_ptr錯誤使用 weak_ptr<ListNode> node1(new ListNode); //weak_ptr正確使用 shared_ptr<ListNode> node2(new ListNode); weak_ptr<ListNode> node3(node2);
到此這篇關於C++11 智能指針的具體使用的文章就介紹到這瞭,更多相關C++11 智能指針內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!