shared_ptr概念
爲了確保用 new 動態分配的內存空間在程序的各條執行路徑都能被釋放是一件麻煩的事情。C++ 11 模板庫的 <memory>
頭文件中定義的智能指針,即 shared _ptr 模板,就是用來部分解決這個問題的。
只要將 new 運算符返回的指針 p 交給一個 shared_ptr 對象“託管”,就不必擔心在哪裏寫delete p語句——實際上根本不需要編寫這條語句,託管 p 的 shared_ptr 對象在消亡時會自動執行delete p。而且,該 shared_ptr 對象能像指針 p —樣使用,即假設託管 p 的 shared_ptr 對象叫作 ptr,那麼 *ptr 就是 p 指向的對象。
通過 shared_ptr 的構造函數,可以讓 shared_ptr 對象託管一個 new 運算符返回的指針,寫法如下:
shared_ptr<T> ptr(new T);
此後,ptr 就可以像 T* 類型的指針一樣使用,即 *ptr 就是用 new 動態分配的那個對象。
多個 shared_ptr 對象可以共同託管一個指針 p,當所有曾經託管 p 的 shared_ptr 對象都解除了對其的託管時,就會執行delete p
。
shared_ptr的原理
shared_ptr的原理:是通過引用計數的方式來實現多個shared_ptr對象之間共享資源。
- shared_ptr在其內部,給每個資源都維護了着一份計數,用來記錄該份資源被幾個對象共享。
- 在對象被銷燬時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一。
- 如果引用計數是0,就說明自己是最後一個使用該資源的對象,必須釋放該資源;
- 如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針了。
shared_ptr的簡單實現
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
template<class T>
class Shared_Ptr{
public:
Shared_Ptr(T* ptr = nullptr)
:_pPtr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
~Shared_Ptr()
{
Release();
}
Shared_Ptr(const Shared_Ptr<T>& sp)
:_pPtr(sp._pPtr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
//if (this != &sp)
if (_pPtr != sp._pPtr)
{
// 釋放管理的舊資源
Release();
// 共享管理新對象的資源,並增加引用計數
_pPtr = sp._pPtr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*(){
return *_pPtr;
}
T* operator->(){
return _pPtr;
}
int UseCount() { return *_pRefCount; }
T* Get() { return _pPtr; }
void AddRefCount()
{
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release()
{
bool deleteflag = false;
_pMutex->lock();
if (--(*_pRefCount) == 0)
{
delete _pRefCount;
delete _pPtr;
deleteflag = true;
}
_pMutex->unlock();
if (deleteflag == true)
delete _pMutex;
}
private:
int *_pRefCount;
T* _pPtr;
mutex* _pMutex;
};
std::shared_ptr的線程安全問題
- 智能指針對象中引用計數是多個智能指針對象共享的,兩個線程中智能指針的引用計數同時++或–,這個操作不是原子的,引用計數原來是1,++了兩次,可能還是2.這樣引用計數就錯亂了。會導致資源未釋放或者程序崩潰的問題。所以只能指針中引用計數++、–是需要加鎖的,也就是說引用計數的操作是線程安全的。
- 智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。
std::shared_ptr的循環引用
#include<memory>
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
system("pause");
return 0;
}
循環引用分析:
- node1和node2兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete。
- node1的_next指向node2,node2的_prev指向node1,引用計數變成2。
- node1和node2析構,引用計數減到1,但是_next還指向下一個節點。但是_prev還指向上一個節點。
- 也就是說_next析構了,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的引用計數。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
如果不是new出來的對象如何通過智能指針管理呢?其實shared_ptr設計了一個刪除器來解決這個問題
// 仿函數的刪除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
return 0;
}