c++中的智能指針主要是基於RAII思想的。
不懂RAII思想的同學可以看看這篇博文-->RAII思想---利用對象生命週期來控制程序資源
智能指針的兩大要素
- RAII思想:把資源交給這個對象管理
- 像指針一樣的行爲(重載operator
*
和operator->
)
- template<class T>
- class SmartPtr
- {
- public://交給對象去管理
- SmartPtr(T* ptr=nullptr)
- :_ptr(ptr)
- {}
- ~SmartPtr()
- {
- if (_ptr)
- delete[] _ptr;
- }
- //像指針一樣的行爲
- T& operator*()//對象出了作用域還在,所以返回引用
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;//返回原生指針
- }
- private:
- T* _ptr;
- };
std::auto_ptr(C++98)
C++98版本的庫中提供了auto_ptr的智能指針。
想要實現一個智能指針就要實現這幾個功能:RAII思想,像指針一樣的行爲。但是對象會有拷貝構造和賦值,auto_ptr的原理是進行了管理權的轉移(管理權的轉移是當把a的值給b之後,就把a置成空),這是一種帶有缺陷的智能指針,會導致對象懸空。
實現的原理如下:
- template<class T>
- class auto_ptr
- {
- public:
- auto_ptr(T* ptr)
- :_ptr(ptr)
- {}
- ~auto_ptr()
- {
- if (_ptr != nullptr)
- {
- cout << "delete:" << _ptr << endl;
- delete _ptr;
- }
- }
- //拷貝構造ap1(ap2)---ap2拷貝構造ap1,此時把ap2置空,管理權交給ap1
- //this指針是ap1,ap是ap2
- auto_ptr(auto_ptr<T>& ap)
- :_ptr(ap._ptr)
- {
- ap._ptr = nullptr;//不執行此句會崩潰,同一資源釋放了兩次
- }
- //賦值 ap1=ap2
- auto_ptr<T>& operator=(auto_ptr<T>& ap)
- {
- if (this != &ap)//檢測是否給自己賦值
- {
- //釋放當前對象ap的資源
- if (_ptr)
- delete _ptr;
- //轉移ap中的資源到當前對象去
- _ptr = ap._ptr;
- ap._ptr = NULL;
- }
- return *this;//支持連續賦值
- }
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
auto_ptr是一種管理權轉移的思想,由於轉移之後原來的對象會被置空,會導致對象的懸空,如下:
- void test_auto_ptr()
- {
- cpp::auto_ptr<int> ap1(new int);
- cpp::auto_ptr<int> ap2(ap1);
- // *ap1 = 10;//拷貝構造後管理權轉移,ap1懸空
- *ap2 = 20;
-
- cpp::auto_ptr<int> ap3(new int);
- //ap3 = ap2;//賦值把p2置空了
- *ap3 = 40;
- }
這裏導致原來的對象懸空了,auto_ptr是帶有缺陷的早期設計,因此很多公司都會禁用這個auto_ptr。
std::unique_ptr(C++11)
C++11中開始提供更靠譜的unique_ptr。
unique_ptr是簡單粗暴的防止拷貝,這種比較簡單,效率高,但是功能不全面,不支持拷貝和賦值操作。具體實現思想如下:
- template<class T>
- class unique_ptr
- {
- public:
- unique_ptr(T* ptr)
- :_ptr(ptr)
- {}
- ~unique_ptr()
- {
- delete _ptr;
- }
- //拷貝
- unique_ptr(unique_ptr<T>& ap) = delete;
-
- //賦值
- unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
-
- T& operator*(){return *_ptr;}
-
- T* operator->(){return _ptr;}
- private:
- T* _ptr;
- };
std::shared_ptr(C++11)
C++11中開始提供更靠譜的並且支持拷貝的shared_ptr。
shared_ptr它的原理是引用計數,它的功能更加全面,支持拷貝構造。但是設計複雜,會有循環引用的問題。
這裏提到一個概念,引用計數,引用計數表示這塊空間被幾個對象引用,有一個對象析構了,引用計數就會減一,但是並不是每次析構都會釋放空間,而是最後一個引用它的對象纔去釋放它。
我們這裏的引用計數只需要一個就行了,所以很容易想到把它定義成靜態的,但是其實這也不可以,因爲一旦一個類中的對象管理不同的空間,就如下圖,Sp1和Sp2管理一塊空間,Sp3和Sp4和Sp5管理另一塊空間,用同一個引用計數的話,我們期望3,4,5析構後,這塊空間也被釋放掉,但是由於引用計數只減爲2,,沒有變爲1,他認爲你不是最後一個引用它的,所以不會釋放掉空間,就會有內存泄漏問題。
所以這裏的引用計數就不能跟着對象走,而是應該跟着空間走的,有一個空間就動態開闢一個引用計數跟着它,每個智能指針都能拿到該引用計數如下圖:
- template<class T>
- class shared_ptr
- {
- public:
- shared_ptr(T* ptr)
- :_ptr(ptr)
- ,_pcount(new int(1))//初始化,動態開闢
- {}
- ~shared_ptr()
- {
- if (--(*_pcount) == 0)//引用計數爲0時,最後一個指向它的釋放它
- {
- cout << "delete:" << _ptr << endl;
- delete _ptr;
- delete _pcount;
- }
- }
- //拷貝
- shared_ptr(const shared_ptr<T>& sp)
- :_ptr(sp._ptr)
- , _pcount(sp._pcount)
- {
- ++(*_pcount);//同一個引用計數
- }
-
- //賦值
- //sp1 = sp3;
- shared_ptr<T>& operator=(shared_ptr<T>& sp)
- {
- //if (this != &sp)//防止自己給自己賦值
- //兩個對象的ptr如果相同就不進入,否則--再++是無用功
- //要是相同的對象,ptr相同就不作操作
- //要是不同的對象,但是指向同一塊空間,ptr相同也不作操作
- if (_ptr != sp._ptr)
- {
- //判斷要賦值對象的引用計數是不是1
- //如果是1,釋放指針和引用計數再指向新的空間進行賦值
- if (--(*_pcount) == 0)
- {
- delete _ptr;
- delete _pcount;
- }
- //兩個指針指向同一塊空間,再++引用計數
- _ptr = sp._ptr;
- _pcount = sp._pcount;
- ++(*_pcount);
- }
- return *this;
- }
- T& operator*(){return *_ptr;}
- T* operator->(){return _ptr;}
- int use_count()
- {
- return *_pcount;
- }//查看引用計數是多少
- private:
- T* _ptr;
- int* _pcount;//引用計數
- };
看到這裏,很多童鞋肯定像我一樣以爲shared_ptr就到此結束了,其實並沒有,它在用起來沒有大問題,但是存在着一個問題,就是多線程的情況下的線程安全問題,來看下面這段代碼:
- void test_multi_thread_copy(cpp::shared_ptr<int>& sp,size_t n)
- {
- for (size_t i = 0; i < n; ++i)//拷貝n次ptr
- {
- cpp::shared_ptr<int> copy(sp);
- }
- }
- void test_shared_ptr_safe()
- {
- cpp::shared_ptr<int> sp(new int);
- std::thread t1(test_multi_thread_copy, sp, 1000);
- std::thread t2(test_multi_thread_copy, sp, 1000);
- cout << sp.use_count() << endl;
- t1.join();
- t2.join();
- }
t1和t2是我們創建的兩個線程,這兩個線程都去拷貝ptr1000次,此時多個線程同時對該空間進行++,--操作,那麼導致結果錯誤,並且打印出來的引用計數也是不確定的,說明此時的引用計數已經出現了問題,導致內存沒有釋放,會出現內存泄漏的問題。因爲這裏的 ++操作和 - - 操作不是原子的操作 ,存在線程安全問題。因此多線程編程時,要給這裏的引用計數進行加鎖操作,基於以上我們進一步完善shared_ptr。
- template<class T>
- class shared_ptr
- {
- public:
- shared_ptr(T* ptr)
- :_ptr(ptr)
- ,_pcount(new int(1))//初始化,動態開闢
- , _pmtx(new std::mutex)
- {}
- ~shared_ptr()
- {
- Release();
- //if (--(*_pcount) == 0)//引用計數爲0時,最後一個指向它的釋放它
- //{
- // cout << "delete:" << _ptr << endl;
- // delete _ptr;
- // delete _pcount;
- //}
- }
- void AddRef()//增加引用計數
- {
- _pmtx->lock();
- ++(*_pcount);//同一個引用計數
- _pmtx->unlock();
- }
- void Release()//釋放引用計數
- {
- _pmtx->lock();
- int flag = 0;
- if (--(*_pcount) == 0)
- {
- cout << "delete: " << _ptr << endl;
- delete _ptr;
- delete _pcount;
- flag = 1;
- }
- _pmtx->unlock();
- if (flag == 1){delete _pmtx;}
- }
- //拷貝
- shared_ptr(const shared_ptr<T>& sp)
- :_ptr(sp._ptr)
- , _pcount(sp._pcount)
- , _pmtx(sp._pmtx)
- {
- //++(*_pcount);//同一個引用計數
- AddRef();//加鎖,封裝成函數
- }
-
- //賦值
- //sp1 = sp3;
- shared_ptr<T>& operator=(shared_ptr<T>& sp)
- {
- //if (this != &sp)//防止自己給自己賦值
- if (_ptr != sp._ptr)
- {
- //if (--(*_pcount) == 0)
- //{
- // delete _ptr;
- // delete _pcount;
- //}
- Release();
- //兩個指針指向同一塊空間,再++引用計數
- _ptr = sp._ptr;
- _pcount = sp._pcount;
- _pmtx = sp._pmtx;
- AddRef();
- /*++(*_pcount);*/
- }
- return *this;
- }
-
- T& operator*() {return *_ptr;}
- T* operator->() {return _ptr;}
-
- int use_count() { return *_pcount;}//查看引用計數是多少
- private:
- T* _ptr;
- int* _pcount;//引用計數
- std::mutex* _pmtx;//定義一個鎖,大家都鎖在該鎖上,所以定義爲指針
- };
這樣加鎖之後,智能指針的引用計數的++和- - 是安全的,也支持拷貝構造,所以shared_ptr本身是安全的,智能指針的線程安全體現在引用計數的++和- -是線程安全的,但是它指向的對象不一定是線程安全的,因爲智能指針指向的對象本身並不受它的管控。
shared_ptr的循環引用以及weak_ptr()
- struct ListNode
- {
- std::shared_ptr<ListNode> _prev;
- std::shared_ptr<ListNode> _next;
- ~ListNode()
- {
- cout << "~ListNode()" << endl;
- }
- };
-
- void test_shared_ptr_cycle_ref()
- {
- std::shared_ptr<ListNode> cur(new ListNode);
- std::shared_ptr<ListNode> next(new ListNode);
-
- cur->_next = next;
- next->_prev = cur;
- }
這一段代碼中用shared_ptr管理了cur和next,同時也管理了cur和next裏面的_prev和_next,讓智能指針share_ptr指向cur和next時,兩個智能指針的引用計數都是1,再讓cur的_next指向了next,讓next的_prev指向了cur,此時兩個智能指針的引用計數都變成了2,出了test_shared_ptr_cycle_ref函數之後,cur和next應該去調析構函數,但是此時智能指針的引用計數不爲1,所以兩個智能指針的引用計數- -,那麼此時就沒有完成節點的釋放,因爲發生了循環引用。可以理解爲相互牽制,誰也無法進行釋放,畫個圖理解一下:
- cur和next兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete。
- cur的_next指向next,next的_prev指向cur,引用計數變成2。
- cur和next析構,引用計數減到1,但是_next還指向下一個節點。但是_prev還指向上一個節點。
- 也就是說_next析構了,next就釋放了。
- 也就是說_prev析構了,cur就釋放了。
- 但是_next屬於cur的成員,cur釋放了,_next纔會析構,而cur由_prev管理,_prev屬於next成員,所以這就叫循環引用,誰也不會釋放。
那麼這種問題要怎麼解決呢?解決方案就是使用weak_ptr:
- struct ListNode
- {
- std::weak_ptr<ListNode> _prev;
- std::weak_ptr<ListNode> _next;
- ~ListNode()
- {
- cout << "~ListNode()" << endl;
- }
- };
因爲在執行cur->_next = next;
和next->_prev = cur;
時,weak_ptr的_next和_prev不會增加cur和next的引用計數,這樣就保證了正常的釋放。
但是weak_ptr只能用於循環引用場景下,其他時候不能用。
shared_ptr定製刪除器
在上面寫的代碼中,都是一次只申請了一塊空間,然後讓智能指針管理,智能指針釋放的時候直接調用delete即可,但是不排除有這麼一種情況,那就是如果一次申請多塊空間呢?
- struct A
- {
- ~A()
- {
- cout << "~A()" << endl;
- }
- };
- //定製刪除器
- void test_shared_ptr_deletor()
- {
- std::shared_ptr<A> sp(new A[10]);
- }
- int main()
- {
- test_shared_ptr_deletor();
- system("pause");
- return 0;
- }
那此時就需要用delete[]來進行釋放,因此可以利用仿函數來定製刪除器。
- struct A
- {
- ~A()
- {
- cout << "~A()" << endl;
- }
- };
- template<class T>
- struct DeleteArray
- {
- void operator()(T* ptr)
- {
- delete[] ptr;
- }
- };
- //定製刪除器
- void test_shared_ptr_deletor()
- {
- DeleteArray<A> del;
- std::shared_ptr<A> sp(new A[10],del);
- }
- int main()
- {
- test_shared_ptr_deletor();
- system("pause");
- return 0;
- }