智能指針-使用、避坑和實現


本文原文:智能指針-使用、避坑和實現

在上篇文章(內存泄漏-原因、避免以及定位)中,我們提到了用智能指針來避免內存泄漏,今天借助本文,從實踐避坑實現原理三個角度分析下C++中的智能指針。

本文主要內容如下圖所示:

  1. 智能指針的由來
  2. auto_ptr為什么被廢棄
  3. unique_ptr的使用、特點以及實現
  4. shared_ptr的使用、特點以及實現
  5. weak_ptr的使用、特點以及實現
  6. 介紹筆者在工作中遇到的一些職能指針相關的坑,並給出一些建議

背景

內存的分配與回收都是由開發人員在編寫代碼時主動完成的,好處是內存管理的開銷較小,程序擁有更高的執行效率;弊端是依賴於開發者的水平,隨着代碼規模的擴大,極容易遺漏釋放內存的步驟,或者一些不規范的編程可能會使程序具有安全隱患。如果對內存管理不當,可能導致程序中存在內存缺陷,甚至會在運行時產生內存故障錯誤。換句話說,開發者自己管理內存,最容易發生下面兩種情況:

  • 申請了內存卻沒有釋放,造成內存泄漏
  • 使用已經釋放的內存,造成segment fault

所以,為了在保證性能的前提下,又能使得開發者不需要關心內存的釋放,進而使得開發者能夠將更多的精力投入到業務上,自C++11開始,STL正式引入了智能指針。

所有權

智能指針一個很關鍵的一個點就是是否擁有一個對象的所有權,當我們通過std::make_xxx或者new一個對象,那么就擁有了這個對象的所有權。

所有權分為獨占所有權共享所有權以及弱共享所有權三種。

獨占所有權

顧名思義,獨占該對象。獨占的意思就是不共享,所有權可以轉移,但是轉移之后,所有權也是獨占。auto_ptr和unique_ptr就是一種獨占所有權方式的智能指針。

假設有個Object對象,如果A擁有該對象的話,就需要保證其在不使用該對象的時候,將該對象釋放;而此時如果B也想擁有Object對象,那么就必須先讓A放棄該對象所有權,然后B獨享該對象,那么該對象的使用和釋放就只歸B所有,跟A沒有關系了。

獨占所有權具有以下幾個特點:

  • 如果創建或者復制了某個對象,就擁有了該對象
  • 如果沒有創建對象,而是將對象保留使用,同樣擁有該對象的所有權
  • 如果你擁有了某個對象的所有權,在不需要某一個對象時,需要釋放它們

共享所有權

共享所有權,與獨占所有權正好相反,對某個對象的所有全可以共享。shared_ptr就是一種共享所有權方式的智能指針。

假設此時A擁有對象Object,在沒有其它擁有該對對象的情況下,對象的釋放由A來負責;如果此時B也想擁有該對象,那么對象的釋放由最后一個擁有它的來負責。

舉一個我們經常遇到的例子,socket連接,多個發送端(sender)可以使用其發送和接收數據。

弱共享所有權

弱共享所有權,指的是可以使用該對象,但是沒有所有權,由真正擁有其所有權的來負責釋放。weak_ptr就是一種弱共享所有權方式的智能指針。

分類

在C++11中,有unique_ptrshared_ptr以及weak_ptr三種,auto_ptr因為自身轉移所有權的原因,在C++11中被廢棄(本節最后,將簡單說下被廢棄的原因)。

  • unique_ptr

    • 使用上限制最多的一種智能指針,被用來取代之前的auto_ptr,一個對象只能被一個unique_ptr所擁有,而不能被共享,如果需要將其所擁有的對象轉移給其他unique_ptr,則需要使用move語義
  • shared_ptr

    • 與unique_ptr不同的是,unique_ptr是獨占管理權,而shared_ptr則是共享管理權,即多個shared_ptr可以共用同一塊關聯對象,其內部采用的是引用計數,在拷貝的時候,引用計數+1,而在某個對象退出作用域或者釋放的時候,引用計數-1,當引用計數為0的時候,會自動釋放其管理的對象。
  • weak_ptr

    • weak_ptr的出現,主要是為了解決shared_ptr的循環引用,其主要是與shared_ptr一起來私用。和shared_ptr不同的地方在於,其並不會擁有資源,也就是說不能訪問對象所提供的成員函數,不過,可以通過weak_ptr.lock()來產生一個擁有訪問權限的shared_ptr。

auto_ptr

auto_ptr自C++98被引入,因為其存在較多問題,所以在c++11中被廢棄,自C++17開始正式從STL中移除。

首先我們看下auto_ptr的簡單實現(為了方便閱讀,進行了修改,基本功能類似於std::auto_ptr):

template<class T> 
class auto_ptr 
{ 
    T* p; 
public: 
    auto_ptr(T* s) :p(s) {} 
    ~auto_ptr() { delete p; } 
     
    auto_ptr(auto_ptr& a) { 
      p = a.p; 
      a.p = NULL; 
    } 
    auto_ptr& operator=(auto_ptr& a) { 
      delete p; 
      p=a.p; 
      a.p = NULL; 
      return *this; 
    } 
 
    T& operator*() const { return *p; } 
    T* operator->() const { return p; } 
}; 

從上面代碼可以看出,auto_ptr采用copy語義來轉移所有權,轉移之后,其關聯的資源指針設置為NULL,而這跟我們理解上copy行為不一致。

在<< Effective STL >>第8條,作者提出永不建立auto_ptr的容器,並以一個例子來說明原因,感興趣的可以去看看這本書,還是不錯的。

實際上,auto_ptr被廢棄的直接原因是拷貝造成所有權轉移,如下代碼:

auto_ptr<ClassA> a(new ClassA);
auto_ptr<ClassA> b = a;
a->Method();

在上述代碼中,因為b = a導致所有權被轉移,即a關聯的對象為NULL,如果再調用a的成員函數,顯然會造成coredump。

正是因為拷貝導致所有權被轉移,所以auto_ptr使用上有很多限制:

  • 不能在STL容器中使用,因為復制將導致數據無效
  • 一些STL算法也可能導致auto_ptr失效,比如std::sort算法
  • 不能作為函數參數,因為這會導致復制,並且在調用后,導致原數據無效
  • 如果作為類的成員變量,需要注意在類拷貝時候導致的數據無效

正是因為auto_ptr的諸多限制,所以自C++11起,廢棄了auto_ptr,引入unique_ptr。

unique_ptr

unique_ptr是C++11提供的用於防止內存泄漏的智能指針中的一種實現(用來替代auto_ptr),獨享被管理對象指針所有權的智能指針。

unique_ptr對象包裝一個原始指針,並負責其生命周期。當該對象被銷毀時,會在其析構函數中刪除關聯的原始指針。具有->和*運算符重載符,因此它可以像普通指針一樣使用。

分類

unique_ptr分為以下兩種:

  • 指向單個對象
std::unique_ptr<Type> p1; // p1關聯Type對象
  • 指向一個數組
unique_ptr<Type[]> p2; // p2關聯Type對象數組

特點

在前面的內容中,我們已經提到了unique_ptr的特點,主要具有以下:

  • 獨享所有權,在作用域結束時候,自動釋放所關聯的對象
void fun() {
  unique_ptr<int> a(new int(1));
}
  • 無法進行拷貝與賦值操作
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1(ptr) ; // error
unique_ptr<int> ptr2 = ptr; //error
  • 顯示的所有權轉移(通過move語義)
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1 = std::move(ptr) ; // ok
  • 作為容器元素存儲在容器中
unique_ptr<int> ptr(new int(1));
std::vector<unique_ptr<int>> v;

v.push_back(ptr); // error
v.push_back(std::move(ptr)); // ok

std::cout << *ptr << std::endl;// error

需要注意的是,自c++14起,可以使用下面的方式對unique_ptr進行初始化:

auto p1 = std::make_unique<double>(3.14);
auto p2 = std::make_unique<double[]>(n);

如果在c++11中使用上述方法進行初始化,會得到下面的錯誤提示:

error: ‘make_unique’ is not a member of ‘std’

因此,如果為了使得c++11也可以使用std::make_unique,我們可以自己進行封裝,如下:

namespace details {

#if __cplusplus >= 201402L // C++14及以后使用STL實現的
using std::make_unique;
#else
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
} // namespace details

使用

為了盡可能了解unique_ptr的使用姿勢,我們使用以下代碼為例:

#include <memory>
#include <utility> // std::move

void fun1(double *);
void fun2(std::unique<double> *);
void fun3(std::unique<double> &);
void fun4(std::unique<double> );

int main() {
  std::unique_ptr<double> p(new double(3.14));
  
  fun1(p.get());
  fun2(&p);
  fun3(p);
  
  if (p) {
    std::cout << "is valid" << std::endl;
  }
  auto p2(p.release()); // 轉移所有權
  auto p2.reset(new double(1.0));
  fun4(std::move(p2));
  
  return 0;
}

上述代碼,基本覆蓋了常見的unique_ptr用法:

  • 第10行,通過new創建一個unique_ptr對象
  • 第11行,通過get()函數獲取其關聯的原生指針
  • 第12行,通過unique_ptr對象的指針進行訪問
  • 第13行,通過unique_ptr對象的引用進行訪問
  • 第16行,通過if(p)來判斷其是否有效
  • 第18行,通過release函數釋放所有權,並將所有權進行轉移
  • 第19行,通過reset釋放之前的原生指針,並重新關聯一個新的指針
  • 第20行,通過std::move轉移所有權

簡單實現

本部分只是基於源碼的一些思路,便於理解,實現的一個簡單方案,如果想要閱讀源碼,請點擊unique_ptr查看。

基本代碼如下:

template<class T> 
class unique_ptr 
{ 
   T* p; 
public: 
   unique_ptr() :p() {} 
   unique_ptr(T* s) :p(s) {} 
   ~unique_ptr() { delete p; } 
  
    unique_ptr(const unique_ptr&) = delete;
     unique_ptr& operator=(const unique_ptr&) = delete;
 
   unique_ptr(unique_ptr&& s) :p(s.p) { s.p = nullptr } 
 
   unique_ptr& operator=(unique_ptr s) 
   { delete p; p = s.p; s.p=nullptr; return *this; } 
 
   T* operator->() const { return p; } 
   T& operator*() const { return *p; } 
}; 

從上面代碼基本可以看出,unique_ptr的控制權轉移是通過move語義來實現的,相比於auto_ptr的拷貝語義轉移所有權,更為合理。

shared_ptr

unique_ptr因為其局限性(獨享所有權),一般很少用於多線程操作。在多線程操作的時候,既可以共享資源,又可以自動釋放資源,這就引入了shared_ptr。

shared_ptr為了支持跨線程訪問,其內部有一個引用計數(線程安全),用來記錄當前使用該資源的shared_ptr個數,在結束使用的時候,引用計數為-1,當引用計數為0時,會自動釋放其關聯的資源。

特點

相對於unique_ptr的獨享所有權,shared_ptr可以共享所有權。其內部有一個引用計數,用來記錄共享該資源的shared_ptr個數,當共享數為0的時候,會自動釋放其關聯的資源。

shared_ptr不支持數組,所以,如果用shared_ptr指向一個數組的話,需要自己手動實現deleter,如下所示:

std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});

使用

仍然以一段代碼來說明,畢竟代碼更有說服力。

#include <iostream>
#include  <memory> 

int main() {
    // 創建shared_ptr對象
    std::shared_ptr<int> p1 = std::make_shared<int>();
    *p1 = 78;
    std::cout << "p1 = " << *p1 << std::endl;
    // 打印引用計數
    std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
    
    std::shared_ptr<int> p2(p1);
    // 打印引用計數
    std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
    std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
    
    if (p1 == p2)
    {
        std::cout << "p1 and p2 are pointing to same pointer\n";
    }
    std::cout<<"Reset p1 "<<std::endl;
    // 引用計數-1
    p1.reset();
    
    std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
    
    // 重置
    p1.reset(new int(11));
    std::cout << "p1  Reference Count = " << p1.use_count() << std::endl;
    
    p1 = nullptr;
    std::cout << "p1  Reference Count = " << p1.use_count() << std::endl;
    if (!p1) // 通過此種方式來判斷關聯的資源是否為空
    {
        std::cout << "p1 is NULL" << std::endl;
    }
    return 0;
}

輸出如下:

p1 = 78
p1 Reference count = 1
p2 Reference count = 2
p1 Reference count = 2
p1 and p2 are pointing to same pointer
Reset p1 
p1 Reference Count = 0
p1  Reference Count = 1
p1  Reference Count = 0
p1 is NULL

上面代碼基本羅列了shared_ptr的常用方法,對於其他方法,可以參考源碼或者官網。

線程安全

可能很多人都對shared_ptr是否線程安全存在疑惑,借助本節,對線程安全方面的問題進行分析和解釋。

shared_ptr的線程安全問題主要有以下兩種:

  • 引用計數的加減操作是否線程安全
  • shared_ptr修改指向時是否線程安全

引用計數

shared_ptr中有兩個指針,一個指向所管理數據的地址,另一個一個指向執行控制塊的地址

執行控制塊包括對關聯資源的引用計數以及弱引用計數等。在前面我們提到shared_ptr支持跨線程操作,引用計數變量是存儲在堆上的,那么在多線程的情況下,指向同一數據的多個shared_ptr在進行計數的++或--時是否線程安全呢?

引用計數在STL中的定義如下:

_Atomic_word  _M_use_count;     // #shared
_Atomic_word  _M_weak_count;    // #weak + (#shared != 0)

當對shared_ptr進行拷貝時,引入計數增加,實現如下:

template<> 
  inline void
 _Sp_counted_base<_S_atomic>::
 _M_add_ref_lock() {
       // Perform lock-free add-if-not-zero operation.
       _Atomic_word __count;
       do
     {
       __count = _M_use_count;
       if (__count == 0)
         __throw_bad_weak_ptr(); 
     }
       while (!__sync_bool_compare_and_swap(&_M_use_count, __count,
                        __count + 1));
     }

最終,計數的增加,是調用__sync_bool_compare_and_swap實現的,而該函數是線程安全的,因此我們可以得出結論:在多線程環境下,管理同一個數據的shared_ptr在進行計數的增加或減少的時候是線程安全的,這是一波原子操作

修改指向

修改指向分為操作同一個對象和操作不同對象兩種。

同一對象

以下面代碼為例:

void fun(shared_ptr<Type> &p) {
  if (...) {
    p = p1;
  } else {
    p = p2;
  }
}

當在多線程場景下調用該函數時候,p之前的引用計數要進行-1操作,而p1對象的引用計數要進行+1操作,雖然這倆的引用計數操作都是線程安全的,但是對這倆對象的引用計數的操作在一起時候卻不是線程安全的。這是因為當對p1的引用計數進行+1時候,恰恰前一時刻,p1的對象被釋放,后面再進行+1操作,會導致segment fault

不同對象

代碼如下:

void fun1(std::shared_ptr<Type> &p) {
  p = p1;
}

void fun2(std::shared_ptr<Type> &p) {
  p = p2;
}

int main() {
  std::shared_ptr<Type> p = std::make_shared<Type>();
  auto p1 = p;
  auto p2 = p;
  std::thread t1(fun1, p1);
  std::thread t2(fun2, p2);
  
  t1.join();
  t2.join();
  
  return 0;
}

在上述代碼中,p、p1、p2指向同一個資源,分別有兩個線程操作不同的shared_ptr對象(雖然關聯的底層資源是同一個),這樣在多線程下,只對p1和p2的引用計數進行操作,不會引起segment fault,所以是線程安全的。

同一個shared_ptr被多個線程同時讀是安全的

同一個shared_ptr被多個線程同時讀寫是不安全的

簡單實現

本部分只是基於源碼的一些思路,便於理解,實現的一個簡單方案,如果想要閱讀源碼,請點擊shared_ptr查看。

記得之前看過一個問題為什么引用計數要new,這個問題我在面試的時候也問過,很少有人能夠回答出來,其實,很簡單,因為要支持多線程訪問,所以只能要new呀😁。

代碼如下:

template <class T>
class weak_ptr;

class Counter {
 public:
  Counter() = default;
  int s_ = 0; // shared_ptr的計數
  int w_ = 0; // weak_ptr的計數
};

template <class T>
class shared_ptr {
 public:
  shared_ptr(T *p = 0) : ptr_(p) {
   cnt_ = new Counter();
   if (p) {
     cnt_->s_ = 1;
   }
 }

  ~shared_ptr() {
    release();
  }

  shared_ptr(shared_ptr<T> const &s) {
    ptr_ = s.ptr_;
    (s.cnt)->s_++;
    cnt_ = s.cnt_;
  }

  shared_ptr(weakptr_<T> const &w) {
    ptr_ = w.ptr_;
    (w.cnt_)->s_++;
    cnt_ = w.cnt_;
  }

  shared_ptr<T> &operator=(shared_ptr<T> &s) {
    if (this != &s) {
      release();
      (s.cnt_)->s_++;
      cnt_ = s.cnt_;
      ptr_ = s.ptr_;
    }
      return *this;
  }

  T &operator*() {
    return *ptr_;
  }

  T *operator->() {
    return ptr_;
  }

  friend class weak_ptr<T>;

 protected:
  void release() {
    cnt_->s_--;
    if (cnt_->s_ < 1)
    {
      delete ptr_;
      if (cnt_->w_ < 1)
      {
          delete cnt_;
          cnt_ = NULL;
      }
    }
  }

private:
  T *ptr_;
  Counter *cnt_;
};

weak_ptr

在三個智能指針中,weak_ptr是存在感最低的一個,也是最容易被大家忽略的一個智能指針。它的引入是為了解決shared_ptr存在的一個問題循環引用

特點

  1. 不具有普通指針的行為,沒有重載operator*和operator->
  2. 沒有共享資源,它的構造不會引起引用計數增加
  3. 用於協助shared_ptr來解決循環引用問題
  4. 可以從一個shared_ptr或者另外一個weak_ptr對象構造,進而可以間接獲取資源的弱共享權。

使用

int main() {
    std::shared_ptr<int> p1 = std::make_shared<Entity>(14);
    {
        std::weak_ptr<int> weak = p1;
        std::shared_ptr<Entity> new_shared = weak.lock();
 
        shared_e1 = nullptr;
       
        new_shared = nullptr;
        if (weak.expired()) {
            std::cout << "weak pointer is expired" << std::endl;
        }
        
        new_shared = weak.lock();
        std::cout << new_shared << std::endl;
   }
  
  return 0;
}

上述代碼輸出如下:

weak pointer is expired
0
  1. 使用成員函數use_count()和expired()來獲取資源的引用計數,如果返回為0或者false,則表示關聯的資源不存在
  2. 使用lock()成員函數獲得一個可用的shared_ptr對象,進而操作資源
  3. 當expired()為true的時候,lock()函數將返回一個空的shared_ptr

簡單實現

template <class T>
class weak_ptr
{
 public:
  weak_ptr() = default;

  weak_ptr(shared_ptr<T> &s) : ptr_(s.ptr_), cnt(s.cnt_) {
    cnt_->w_++;
  }

  weak_ptr(weak_ptr<T> &w) : ptr_(w.ptr_), cnt_(w.cnt_) {
    cnt_->w_++;
  }
  ~weak_ptr() {
    release();
  }
  weak_ptr<T> &operator=(weak_ptr<T> &w) {
    if (this != &w) {
      release();
      cnt_ = w.cnt_;
      cnt_->w_++;
      ptr_ = w.ptr_;
    }
    return *this;
  }
  weak_ptr<T> &operator=(shared_ptr<T> &s)
  {
    release();
    cnt_ = s.cnt_;
    cnt_->w_++;
    ptr_ = s.ptr_;
    return *this;
  }

  shared_ptr<T> lock() {
    return shared_ptr<T>(*this);
  }

  bool expired() {
    if (cnt) {
      if (cnt->s_ > 0) {
        return false;
      }
    }
    return true;
  }

  friend class shared_ptr<T>;

protected:
  void release() {
    if (cnt_) {
      cnt_->w_--;
      if (cnt_->w_ < 1 && cnt_->s_ < 1) {
        cnt_ = nullptr;
      }
    }
  }

private:
    T *ptr_ = nullptr;
    Counter *cnt_ = nullptr;
};

循環引用

在之前的文章內存泄漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決循環引用的問題,借助本文,我們深入說明下如何來解決循環引用的問題。

代碼如下:

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::shared_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

在上述代碼中,因為controller和sub_controller之間都有一個指向對方的shared_ptr,這樣就導致任意一個都因為對方有一個指向自己的對象,進而引用計數不能為0。

為了解決std::shared_ptr循環引用導致的內存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環。

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::weak_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr。

那么,為什么將SubController中的shared_ptr換成weak_ptr就能解決這個問題呢?我們看下源碼:

template<typename _Tp1>
         __weak_ptr&
         operator=(const __shared_ptr<_Tp1, _Lp>& __r) // never throws
         {
       _M_ptr = __r._M_ptr;
       _M_refcount = __r._M_refcount;
       return *this;
     }

在上面代碼中,我們可以看到,將一個shared_ptr賦值給weak_ptr的時候,其引用計數並沒有+1,所以也就解決了循環引用的問題。

那么,如果我們想要使用shared_ptr關聯的對象進行操作時候,該怎么做呢?使用weak_ptr::lock()函數來實現,源碼如下:

 __shared_ptr<_Tp, _Lp>
 lock() const {
   return expired() ? __shared_ptr<element_type, _Lp>() : __shared_ptr<element_type, _Lp>(*this);
 }

從上面代碼可看出,使用lock()函數生成一個shared_ptr供使用,如果之前的shared_ptr已經被釋放,那么就返回一個空shared_ptr對象,否則生成shared_ptr對象的拷貝(這樣即使之前的釋放也不會存在問題)。

經驗之談

不要混用

指針之間的混用,有時候會造成不可預知的錯誤,所以建議盡量不要混用。包括裸指針和智能指針以及智能指針之間的混用

裸指針和智能指針混用

代碼如下:

void fun() {
  auto ptr = new Type;
  std::shared_ptr<Type> t(ptr);
  
  delete ptr;
}

在上述代碼中,將ptr所有權歸shared_ptr所擁有,所以在出fun()函數作用域的時候,會自動釋放ptr指針,而在函數末尾有主動調用delete來釋放,這就會造成double delete,會造成segment fault

智能指針混用

代碼如下:

void fun() {
  std::unique_ptr<Type> t(new Type);
  std::shared_ptr<Type> t1(t.get());
}

在上述代碼中,將t關聯的對象又給了t1,也就是說同一個對象被兩個智能指針所擁有,所以在出fun()函數作用域的時候,二者都會釋放其關聯的對象,這就會造成double delete,會造成segment fault

需要注意的是,下面代碼在STL中是支持的:

void fun() {
  std::unique_ptr<Type> t(new Type);
  std::shared_ptr<Type> t1(std::move(t));
}

不要管理同一個裸指針

代碼如下:

void fun() {
  auto ptr = new Type;
  std::unique_ptr<Type> t(ptr);
  std::shared_ptr<Type> t1(ptr);
}

在上述代碼中,ptr所有權同時給了t和t1,也就是說同一個對象被兩個智能指針所擁有,所以在出fun()函數作用域的時候,二者都會釋放其關聯的對象,這就會造成double delete,會造成segment fault

避免使用get()獲取原生指針

void fun(){
  auto ptr = std::make_shared<Type>();

  auto a= ptr.get();

  std::shared_ptr<Type> t(a);
  delete a;
}

一般情況下,生成的指針都要顯示調用delete來進行釋放,而上述這種,很容易稍不注意就調用delete;非必要不要使用get()獲取原生指針

不要管理this指針

class Type {
 private:
 	void fun() {
 	  std::shared_ptr<Type> t(this);
 	}
};

在上述代碼中,如果Type在棧上,則會導致segment fault,堆上視實際情況(如果在對象在堆上生成,那么使用合理的話,是允許的)。

只管理堆上的對象

void fun() {
   Type t;
 	 std::shared_ptr<Type> ptr(&t);
};

在上述代碼中,t在棧上進行分配,在出作用域的時候,會自動釋放。而ptr在出作用域的時候,也會調用delete釋放t,而t本身在棧上,delete一個棧上的地址,會造成segment fault

優先使用unique_ptr

根據業務場景,如果需要資源獨占,那么建議使用unique_ptr而不是shared_ptr,原因如下:

  • 性能優於shared_ptr
    • 因為shared_ptr在拷貝或者釋放時候,都需要操作引用計數
  • 內存占用上小於shared_ptr
    • shared_ptr需要維護它指向的對象的線程安全引用計數和一個控制塊,這使得它比unique_ptr更重量級

使用make_shared初始化

我們看下常用的初始化shared_ptr兩種方式,代碼如下:

std::shared_ptr<Type> p1 = new Type;
std::shared_ptr<Type> p2 = std::make_shared<Type>();

那么,上述兩種方法孰優孰劣呢?我們且從源碼的角度進行分析。

第一種初始化方法,有兩次內存分配:

  • new Type分配對象
  • 為p1分配控制塊(control block),控制塊用於存放引用計數等信息

我們再看下make_shared源碼:

template<class _Ty,
  class... _Types> inline
    shared_ptr<_Ty> make_shared(_Types&&... _Args)
  {  // make a shared_ptr
  _Ref_count_obj<_Ty> *_Rx =
    new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);

  shared_ptr<_Ty> _Ret;
  _Ret._Resetp0(_Rx->_Getptr(), _Rx);
  return (_Ret);
  }

這里的_Ref_count_obj類包含成員變量:

  • 控制塊
  • 一個內存塊,用於存放智能指針管理的資源對象

再看看_Ref_count_obj的構造函數:

template<class... _Types>
  _Ref_count_obj(_Types&&... _Args)
  : _Ref_count_base()
  {  // construct from argument list
  ::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
  }

此處雖然也有一個new操作,但是此處是placement new,所以不存在內存申請。

從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次內存分配操作,而第二種初始化方式(make_shared)只有一次內存申請,所以建議使用make_shared方式進行初始化。

結語

智能指針的出現,能夠使得開發者不需要關心內存的釋放,進而使得開發者能夠將更多的精力投入到業務上。但是,因為智能指針本身也有其局限性,如果使用不當,會造成意想不到的后果,所以,在使用之前,需要做一些必要的檢查,為了更好的用好智能指針,建議看下源碼實現,還是比較簡單的😁。

好了,今天的分享就到這,我們下期見。

參考

https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/hh279676(v=vs.110)?redirectedfrom=MSDN
https://rufflewind.com/2016-03-05/unique-ptr
https://www.nextptr.com/tutorial/ta1450413058/unique_ptr-shared_ptr-weak_ptr-or-reference_wrapper-for-class-relationships
https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01099_source.html
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01327.html
https://www.nextptr.com/tutorial/ta1358374985/shared_ptr-basics-and-internals-with-examples

作者:高性能架構探索
本文首發於公眾號【高性能架構探索】
個人技術博客:高性能架構探索


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM