C++學習之“智慧指標”
寫在前面
1.為什麼需要智慧指標
2.智慧指標的使用及原理
3.各種版本的智慧指標瞭解及實現
auto_ptr: c++ 98 (管理權轉移)
unique_ptr: c++11 (防拷貝)
scoped_ptr: boost庫 (防拷貝)
shared_ptr: c++11 (共享、引用計數) (重點)
weak_ptr: c++11 (解決迴圈引用的問題、輔助shared_ptr)
4.C++11和boost中智慧指標的關係
1.為什麼需要智慧指標?
分析下面一段程式:
#include <vector>
//歸併排序
void _MergeSort (int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = left + ((right - left) >> 1);
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + left, tmp + left, sizeof(int)*(right - left + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
...
vector<int> v(1000000000, 10);
...
}
int main()
{
int a[5] = { 4, 5, 2, 3, 1 };
MergeSort(a, 5);
return 0;
}
問題分析:上面的問題分析出來我們發現有以下兩個問題?
- malloc出來的空間,沒有進行釋放,存在記憶體洩漏的問題。
- 異常安全問題。如果在malloc和free之間如果存在拋異常,那麼還是有記憶體洩漏。這種問題就叫異常安全。
2.智慧指標的使用及原理
2.1 RAII
RAII(Resource Acquisition Is Initialization資源獲取即初始化)是一種利用物件生命週期來控制程式資源(如記憶體、檔案控制代碼、網路連線、互斥量等等)的簡單技術。
在物件構造時獲取資源,接著控制對資源的訪問使之在物件的生命週期內始終保持有效,最後在物件析構的時候釋放資源。藉此,我們實際上把管理一份資源的責任託管給了一個物件。這種做法有兩大好處:
·不需要顯式地釋放資源;
·採用這種方式,物件所需的資源在其生命期內始終保持有效。
/使用RAII思想設計的SmartPtr類
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
2.2 智慧指標的原理
上述的SmartPtr還不能將其稱為智慧指標,因為它還不具有指標的行為。指標可以解引用,也可以通過->去訪問所指空間中的內容,因此:AutoPtr模板類中還得需要將* 、->過載下,才可讓其像指標一樣去使用。
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;
};
總結一下智慧指標的原理:
- RAII特性
- 過載operator*和opertaor->,具有像指標一樣的行為.
3.各種版本的智慧指標瞭解及實現
1.auto_ptr
auto_ptr的實現原理:管理權轉移的思想。
下面簡化模擬實現了一份AutoPtr來了解它的原理:
模擬實現一份簡答的AutoPtr,瞭解原理
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr)
{}
~AutoPtr()
{
if(_ptr)
delete _ptr;
}
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
// 檢測是否為自己給自己賦值
if(this != &ap)
{
// 釋放當前物件中資源
if(_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() { return _ptr;}
private:
T* _ptr;
};
int main()
{
AutoPtr<Date> ap(new Date);
AutoPtr<Date> copy(ap);
ap->_year = 2018;
return 0;
}
一旦發生拷貝,就將ap中資源轉移到當前物件中,然後另ap與其所管理資源斷開聯絡,這樣就解決了一塊空間被多個物件使用而造成程式奔潰問題
現在再從實現原理層來分析會發現,這裡拷貝後把ap物件的指標賦空了,導致ap物件懸空,通過ap物件訪問資源時就會出現問題。
2.unique_ptr
unique_ptr的實現原理:簡單粗暴的防拷貝。
下面簡化模擬實現了一份UniquePtr來了解它的原理。
template<class T>
class UniquePtr
{
public:
UniquePtr(T * ptr = nullptr)
: _ptr(ptr)
{}
~UniquePtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
C++98防拷貝的方式:只宣告不實現+宣告成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
C++11防拷貝的方式:delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
T * _ptr;
};
3.scoped ptr
特點:不能共享控制權。scoped_ptr不能通過其他scoped_ptr共享控制權,因為在scoped_ptr類的內部將拷貝建構函式和=運算子過載定義為私有的。我們看下scoped_ptr類的定義就清楚了
1 namespace boost
2 {
3 template<typename T> class scoped_ptr : noncopyable
4 {
5 private:
6
7 T *px;
8
9 scoped_ptr(scoped_ptr const &);
10 scoped_ptr &operator=(scoped_ptr const &);
11
12 typedef scoped_ptr<T> this_type;
13
14 void operator==( scoped_ptr const & ) const;
15 void operator!=( scoped_ptr const & ) const;
16 public:
17 explicit scoped_ptr(T *p = 0);
18 ~scoped_ptr();
19
20 explicit scoped_ptr( std::auto_ptr<T> p ): px( p.release() );
21 void reset(T *p = 0);
22
23 T &operator*() const;
24 T *operator->() const;
25 T *get() const;
26
27 void swap(scoped_ptr &b);
28 };
29
30 template<typename T>
31 void swap(scoped_ptr<T> &a, scoped_ptr<T> &b);
32 }
4.shared_ptr
shared_ptr的原理:是通過引用計數的方式來實現多個shared_ptr物件之間共享資源。例如:老師放學之前之前都會通知,讓最後走的學生記得把門鎖好。
- shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個物件共享。
- 在物件被銷燬時(也就是解構函式呼叫),就說明自己不使用該資源了,物件的引用計數減一。
- 如果引用計數是0,就說明自己是最後一個使用該資源的物件,必須釋放該資源;
- 如果不是0,就說明除了自己還有其他物件在使用該份資源,不能釋放該資源,否則其他物件就成野指
針了。
#include <thread>
#include <mutex>
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{
// 如果是一個空指標物件,則引用計數給0
if (_ptr == nullptr)
*_pRefCount = 0;
}
~SharedPtr()
{
Release();
}
//拷貝建構函式
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
// 如果是一個空指標物件,則不加引用計數,否則才加引用計數
if (_ptr)
AddRefCount();
}
// sp1 = sp2
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
++(*sp._pRefCount);
// 釋放管理的舊資源
Release();
// 共享管理新物件的資源,並增加引用計數
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
if (_ptr)
AddRefCount();
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
int UseCount() {return *_pRefCount;}
T* Get() { return _ptr; }
int AddRefCount()
{
// 加鎖或者使用加1的原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
return *_pRefCount;
}
int SubRefCount()
{
// 加鎖或者使用減1的原子操作
_pMutex->lock();
--(*_pRefCount);
_pMutex->unlock();
return *_pRefCount;
}
private:
void Release()
{
// 引用計數減一,如果減到零,則釋放資源
if (_ptr && SubRefCount() == 0)
{
delete _ptr;
delete _pRefCount;
}
}
private:
int* _pRefCount; // 引用計數
T* _ptr; // 指向管理資源的指標
mutex* _pMutex; // 互斥鎖
};
std::shared_ptr的執行緒安全問題
需要注意的是shared_ptr的執行緒安全分為兩方面:
- 智慧指標物件中引用計數是多個智慧指標物件共享的,兩個執行緒中智慧指標的引用計數同時++或–,這個操作不是原子的,引用計數原來是1,++了兩次,可能還是2.這樣引用計數就錯亂了。會導致資源未釋放或者程式崩潰的問題。所以只能指標中引用計數++、–是需要加鎖的,也就是說引用計數的操作是執行緒安全的。
- 智慧指標管理的物件存放在堆上,兩個執行緒中同時去訪問,會導致執行緒安全問題。
std::shared_ptr的迴圈引用問題
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;
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;
}
ps:如果不是new出來的物件如何通過智慧指標管理呢?其實shared_ptr設計了一個刪除器來解決這個問題.具體參見文章“仿函式與定置刪除器”。
5.weak_ptr
weak_ptr是為了配合shared_ptr而引入的一種智慧指標,因為它不具有普通指標的行為,沒有過載operator*和->,它的最大作用在於協助shared_ptr工作,像旁觀者那樣觀測資源的使用情況。weak_ptr可以從一個shared_ptr或者另一個weak_ptr物件構造,獲得資源的觀測權。但weak_ptr沒有共享資源,它的構造不會引起指標引用計數的增加。使用weak_ptr的成員函式use_count()可以觀測資源的引用計數,另一個成員函式expired()的功能等價於use_count()==0,但更快,表示被觀測的資源(也就是shared_ptr的管理的資源)已經不復存在。weak_ptr可以使用一個非常重要的成員函式lock()從被觀測的shared_ptr獲得一個可用的shared_ptr物件, 從而操作資源。但當expired()==true的時候,lock()函式將返回一個儲存空指標的shared_ptr。
簡單實現一下吧:
template<class T>
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{}
WeakPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
{}
WeakPtr(WeakPtr<T>& wp)
:_ptr(wp._ptr)
{}
WeakPtr<T>& operator=(SharedPtr<T>& sp)
{
_ptr = sp._ptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
4.C++11和boost中智慧指標的關係
- C++ 98 中產生了第一個智慧指標auto_ptr.
- C++ boost給出了更實用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不過注意的是TR1並不是標準版.
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr對應boost的scoped_ptr。並且這些智慧指標的實現原理是參考boost中的實現的.