1. 程式人生 > >C++學習之“智慧指標”

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; }

問題分析:上面的問題分析出來我們發現有以下兩個問題?

  1. malloc出來的空間,沒有進行釋放,存在記憶體洩漏的問題。
  2. 異常安全問題。如果在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;
};

總結一下智慧指標的原理:

  1. RAII特性
  2. 過載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物件之間共享資源。例如:老師放學之前之前都會通知,讓最後走的學生記得把門鎖好。

  1. shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個物件共享。
  2. 在物件被銷燬時(也就是解構函式呼叫),就說明自己不使用該資源了,物件的引用計數減一。
  3. 如果引用計數是0,就說明自己是最後一個使用該資源的物件,必須釋放該資源;
  4. 如果不是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. 智慧指標物件中引用計數是多個智慧指標物件共享的,兩個執行緒中智慧指標的引用計數同時++或–,這個操作不是原子的,引用計數原來是1,++了兩次,可能還是2.這樣引用計數就錯亂了。會導致資源未釋放或者程式崩潰的問題。所以只能指標中引用計數++、–是需要加鎖的,也就是說引用計數的操作是執行緒安全的。
  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;
}

迴圈引用分析:

  1. node1和node2兩個智慧指標物件指向兩個節點,引用計數變成1,我們不需要手動delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用計數變成2。
  3. node1和node2析構,引用計數減到1,但是_next還指向下一個節點。但是_prev還指向上一個節點。
  4. 也就是說_next析構了,node2就釋放了。
  5. 也就是說_prev析構了,node1就釋放了。
  6. 但是_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中智慧指標的關係

  1. C++ 98 中產生了第一個智慧指標auto_ptr.
  2. C++ boost給出了更實用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不過注意的是TR1並不是標準版.
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr對應boost的scoped_ptr。並且這些智慧指標的實現原理是參考boost中的實現的.