c++11新特性實戰(二):智慧指標

鬼谷子com發表於2021-02-04

c++11新特性實戰(二):智慧指標

c++11新增了新的智慧指標,unique_ptr、shared_ptr和weak_ptr,同時也將auto_ptr置為廢棄(deprecated)。

但是在實際的使用過程中,很多人都會有這樣的問題:

  1. 不知道三種智慧指標的具體使用場景

  2. 無腦只使用shared_ptr

  3. 認為應該禁用raw pointer(裸指標,即Widget*這種形式),全部使用智慧指標

初始化方法

    
class A
{
public:
    A(int size){ this->size = size; }
    A(){}
    void Show()
    {
        std::cout << "A::" << size << __FUNCTION__ << std::endl;
    }
private:
    int size = 5;
};
...
    //[1]
	auto p1 = std::make_shared<int>();
    auto p2 = std::make_shared<A>();
    //[2]
    std::shared_ptr<int> p3(new int(5));
    std::shared_ptr<A> p4(new A());
    //[3]
    std::shared_ptr<int> p5;
    p5.reset(new int(5));
    std::shared_ptr<A> p6;
    p6.reset(new A());

推薦使用第一種方法~

使用場景

  1. unique_ptr

    1. 忘記delete

      class Box{
      public:
          Box() : w(new Widget())
          {}
          ~Box()
          {
              // 忘記delete w
          }
      private:
          Widget* w;
      };
      
    2. 異常安全

      void process()
      {
          Widget* w = new Widget();
          w->do_something(); // 可能會發生異常
          delete w;
      }
      
  2. shared_ptr

    1. shared_ptr通常使用在共享權不明的場景。有可能多個物件同時管理同一個記憶體時。
    2. 物件的延遲銷燬。陳碩在《Linux多執行緒伺服器端程式設計》中提到,當一個物件的析構非常耗時,甚至影響到了關鍵執行緒的速度。可以使用BlockingQueue<std::shared_ptr<void>>將物件轉移到另外一個執行緒中釋放,從而解放關鍵執行緒。
  3. weak_ptr

    weak_ptr是為了解決shared_ptr雙向引用的問題。即:

    class B;
    struct A{
        shared_ptr<B> b;
    };
    struct B{
        shared_ptr<A> a;
    };
    auto pa = make_shared<A>();
    auto pb = make_shared<B>();
    pa->b = pb;
    pb->a = pa;
    

    pa和pb存在著迴圈引用,根據shared_ptr引用計數的原理,pa和pb都無法被正常的釋放。

    對於這種情況, 我們可以使用weak_ptr:

    class B;
    struct A{
        shared_ptr<B> b;
    };
    struct B{
        weak_ptr<A> a;
    };
    auto pa = make_shared<A>();
    auto pb = make_shared<B>();
    pa->b = pb;
    pb->a = pa;
    

    weak_ptr不會增加引用計數,因此可以打破shared_ptr的迴圈引用。

    通常做法是parent類持有child的shared_ptr, child持有指向parent的weak_ptr。這樣也更符合語義。

效能

  1. unique_ptr

    因為C++的zero cost abstraction的特點,unique_ptr在預設情況下和裸指標的大小是一樣的。

    所以記憶體上沒有任何的額外消耗,效能是最優的。

  2. shared_ptr

    1. 存佔用高 shared_ptr的記憶體佔用是裸指標的兩倍。因為除了要管理一個裸指標外,還要維護一個引用計數。 因此相比於unique_ptr, shared_ptr的記憶體佔用更高
    2. 原子操作效能低 考慮到執行緒安全問題,引用計數的增減必須是原子操作。而原子操作一般情況下都比非原子操作慢。
    3. 使用移動優化效能 shared_ptr在效能上固然是低於unique_ptr。而通常情況,我們也可以儘量避免shared_ptr複製。 如果,一個shared_ptr需要將所有權共享給另外一個新的shared_ptr,而我們確定在之後的程式碼中都不再使用這個shared_ptr,那麼這是一個非常鮮明的移動語義。 對於此種場景,我們儘量使用std::move,將shared_ptr轉移給新的物件。因為移動不用增加引用計數,因此效能比複製更好。

物件所有權

首先需要理清楚的概念就是物件所有權的概念。所有權在rust語言中非常嚴格,寫rust的時候必須要清楚自己建立的每個物件的所有權。

但是C++比較自由,似乎我們不需要明白物件的所有權,寫的程式碼也能正常執行。但是明白了物件所有權,我們才可以正確管理好物件生命週期和記憶體問題。

C++引入了智慧指標,也是為了更好的描述物件所有權,簡化記憶體管理,從而大大減少我們C++記憶體管理方面的犯錯機會。

  1. unique_ptr

    我們大多數場景下用到的應該都是unique_ptr

    unique_ptr代表的是專屬所有權,即由unique_ptr管理的記憶體,只能被一個物件持有。

    所以,unique_ptr不支援複製和賦值,如下:

    auto w = std::make_unique<Widget>();
    auto w2 = w; // 編譯錯誤
    

    如果想要把w複製給w2, 是不可以的。因為複製從語義上來說,兩個物件將共享同一塊記憶體。

    因此,unique_ptr只支援移動, 即如下:

    auto w = std::make_unique<Widget>();
    auto w2 = std::move(w); // w2獲得記憶體所有權,w此時等於nullptr
    

    unique_ptr代表的是專屬所有權,如果想要把一個unique_ptr的記憶體交給另外一個unique_ptr物件管理。只能使用std::move轉移當前物件的所有權。轉移之後,當前物件不再持有此記憶體,新的物件將獲得專屬所有權。

    如上程式碼中,將w物件的所有權轉移給w2後,w此時等於nullptr,而w2獲得了專屬所有權。

  2. shared_ptr

    在使用shared_ptr之前應該考慮,是否真的需要使用shared_ptr, 而非unique_ptr。

    shared_ptr代表的是共享所有權,即多個shared_ptr可以共享同一塊記憶體。

    因此,從語義上來看,shared_ptr是支援複製的。如下:

    auto w = std::make_shared<Widget>();
    {
        auto w2 = w;
        cout << w.use_count() << endl;  // 2
    }  
    cout << w.use_count() << endl;  // 1
    

    shared_ptr內部是利用引用計數來實現記憶體的自動管理,每當複製一個shared_ptr,引用計數會+1。當一個shared_ptr離開作用域時,引用計數會-1。當引用計數為0的時候,則delete記憶體。

    同時,shared_ptr也支援移動。從語義上來看,移動指的是所有權的傳遞。如下:

    auto w = std::make_shared<Widget>();
    auto w2 = std::move(w); // 此時w等於nullptr,w2.use_count()等於1
    

    我們將w物件move給w2,意味著w放棄了對記憶體的所有權和管理,此時w物件等於nullptr。

    而w2獲得了物件所有權,但因為此時w已不再持有物件,因此w2的引用計數為1。

指標作為函式傳參

  1. 只在函式使用指標,但並不儲存

    假如我們只需要在函式中,用這個物件處理一些事情,但不打算涉及其生命週期的管理,不打算通過函式傳參延長shared_ptr的生命週期。

    對於這種情況,可以使用raw pointer或者const shared_ptr&。

    即:

    void func(Widget*);
    void func(const shared_ptr<Widget>&)
    

​ 實際上第一種裸指標的方式可能更好,從語義上更加清楚,函式也不用關心智慧指標的型別。

  1. 在函式中儲存智慧指標

    假如我們需要在函式中把這個智慧指標儲存起來,這個時候建議直接傳值。void func(std::shared_ptr ptr);這樣的話,外部傳過來值的時候,可以選擇move或者賦值。函式內部直接把這個物件通過move的方式儲存起來。 這樣效能更好,而且外部呼叫也有多種選擇。

為什麼要用shared_from_this

我們往往會需要在類內部使用自身的shared_ptr,例如:

class A
{
public:
private:
    std::shared_ptr<widget> widget;
}

class Widget
{
public:
    void do_something(A& a)
    {
        a.widget = std::make_shared<Widget>(this);
    }
}

我們需要把當前shared_ptr物件同時交由物件a進行管理。意味著,當前物件的生命週期的結束不能早於物件a。因為物件a在析構之前還是有可能會使用到a.widget

如果我們直接a.widget = this;, 那肯定不行, 因為這樣並沒有增加當前shared_ptr的引用計數。shared_ptr還是有可能早於物件a釋放。

如果我們使用a.widget = std::make_shared<Widget>(this);,肯定也不行,因為這個新建立的shared_ptr,跟當前物件的shared_ptr毫無關係。當前物件的shared_ptr生命週期結束後,依然會釋放掉當前記憶體,那麼之後a.widget依然是不合法的。

對於這種,需要在物件內部獲取該物件自身的shared_ptr, 那麼該類必須繼承std::enable_shared_from_this<T>。程式碼如下:

class Widget : public std::enable_shared_from_this<Widget>
{
public:
    void do_something(A& a)
    {
        a.widget = shared_from_this();
    }    
}
  • 這樣才是合法的做法。

總結

  1. 重點理解三種智慧指標的使用場景,

    • unique_ptr效能高,沒有特殊要求的話可以直接用來取代raw pointer(原始指標)。
    • shared_ptr開銷大,在前者不能滿足的場景例如需要多個智慧指標同時擁有同一個控制元件的所有權的時候使用。
    • weak_ptr不單獨使用,通常用來配合shared_ptr使用,避免迴圈引用的問題。
  2. 優點:不用手動管理記憶體,尤其是根本不知道釋放時機的時候

  3. 缺點:

    • shared_ptr的記憶體佔用高(多了一個引用計數),對多執行緒不友好(對引用計數的操作要原子性)

    • 寫起來麻煩

相關文章