【C++】unique_ptr獨佔型智能指針詳解

指針是C/C++區別於其他語言的最強大的語法特性,藉助指針,C/C++可以直接操縱內存內容。但是,指針的引入也帶來了一些使用上的困難,這要求程序員自己必須手動地對分配申請的內存區進行管理

本文實例源碼github地址https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200424


unique_ptr基本用法

unique_ptr是C++11提供的用於防止內存泄漏的智能指針中的一種實現,獨享被管理對象指針所有權的智能指針。unique_ptr對象包裝一個原始指針,並負責其生命週期。當該對象被銷燬時,會在其析構函數中刪除關聯的原始指針

unique_ptr對象始終是關聯的原始指針的唯一所有者,實現了獨享所有權的語義。一個非空的unique_ptr總是擁有它所指向的資源。轉移一個unique_ptr將會把所有權也從源指針轉移給目標指針(源指針被置空)。拷貝一個unique_ptr將不被允許,因爲如果你拷貝一個unique_ptr,那麼拷貝結束後,這兩個unique_ptr都會指向相同的資源,它們都認爲自己擁有這塊資源(所以都會企圖釋放)。因此unique_ptr是一個僅能移動的類型。當指針析構時,它所擁有的資源也被銷燬。默認情況下,資源的析構是伴隨着調用unique_ptr內部的原始指針的delete操作的

unique_ptr具有->*運算符重載符,因此它可以像普通指針一樣使用。

初始化方式

unique_ptr有如下幾種初始化方式:

  1. 裸指針直接初始化,但不能通過隱式轉換來構造,因爲unique_ptr構造函數被聲明爲explicit;
  2. 允許移動構造,但不允許拷貝構造,因爲unique_ptr是個只移動類型;
  3. 通過make_unique構造,但這是C++14才支持的語法。需要注意的是:make_unique不支持添加刪除器,或者初始化列表。

例如:

#include <iostream>
#include <memory>

class Frame {};

int main()
{
  std::unique_ptr<Frame> f(new Frame());              // 裸指針直接初始化
  std::unique_ptr<Frame> f1 = new Frame();            // Error,explicit禁止隱式初始化
  std::unique_ptr<Frame> f2(f);                       // Error,禁止拷貝構造函數
  std::unique_ptr<Frame> f3 = f;                      // Error,禁止拷貝構造函數
  f1 = f;                                             // Error,禁止copy賦值運算符重載

  std::unique_ptr<Frame> f4(std::move(new Frame()));      // 移動構造函數
  std::unique_ptr<Frame> f5 = std::move(new Frame());     // Error,explicit禁止隱式初始化
  std::unique_ptr<Frame> f6(std::move(f4));               // 移動構造函數
  std::unique_ptr<Frame> f7 = std::move(f6);              // move賦值運算符重載

  std::unique_ptr<Frame[]> f8(new Frame[10]());       // 指向數組

  auto f9 = std::make_unique<Frame>();                // std::make_unique來創建,C++14後支持

  return 0;
}

需要格外關注,unique_ptr創建數組對象的方法。

瞭解了這些,運用剛瞭解的這些特性,試試下面的代碼:

#include <iostream>
#include <memory>

class Frame {};

void fun(std::unique_ptr<Frame> f) {}

std::unique_ptr<Frame> getfun() {
  return std::unique_ptr<Frame>(new Frame());       // 右值,被移動構造
                                                    // 就算不是右值,也會被編譯器RVO優化掉
}

int main()
{
  std::unique_ptr<Frame> f1(new Frame());
  Frame* f2 = new Frame();
  fun(f1);                    // Error,禁止拷貝構造函數
  fun(f2);                    // Error,explit禁止隱式轉換
  fun(std::move(f1));         // 移動構造函數

  std::unique_ptr<Frame> f3 = getfun();       // 移動構造函數

  return 0;
}

刪除器

根據unique_ptr的模板類型來看:

template <typename _Tp, typename _Dp = default_delete<_Tp> >
class unique_ptr {...}

模板參數上,前者爲unique_ptr需要關聯的原始指針的類型,後者爲刪除器,默認值爲default_delete。也就是說,刪除器是unique_ptr類型的組成部分,可以是普通函數指針或lambda表達式。注意,當指定刪除器時需要同時指定其類型,即_Dp不可省略,可通過decltype獲得

刪除器的作用就是規定:當unique_ptr對象被銷燬時,在其析構函數中釋放關聯的原始指針的方式。一般情況下,都是通過delete進行釋放操作。也就是說,一般情況下,不需要進行指定,使用默認的即可。例如:

#include <iostream>
#include <memory>

class Frame {};

void myDeleter(Frame* p)
{
  std::cout << "invoke deleter(Frame*)"<< std::endl;
  delete p;
}

int main()
{
  std::unique_ptr<Frame, decltype(&myDeleter)> f1(new Frame(), myDeleter);
  auto del = [](Frame* p) {
    std::cout << "invoke deleter([](Frame *))"<< std::endl;
    delete p;
  };
  std::unique_ptr<Frame, decltype(del)> f2(new Frame(), del);

  return 0;
}

使用默認的刪除器時,unique_ptr對象和原始指針的大小是一樣的。當自定義刪除器時,如果刪除器是函數指針,則unique_ptr對象的大小爲8字節。對於函數對象的刪除器,unique_ptr對象的大小依賴於存儲狀態的多少,無狀態的函數對象(如不捕獲變量的lambda表達式),其大小爲4字節。

常用操作

  • u.get():返回unique_ptr中保存的裸指針;
  • u.reset(…):重置unique_ptr;
  • u.release():放棄對指針的控制權,返回裸指針,並將unique_ptr自身置空。需要注意,此函數放棄了控制權但不會釋放內存,如果不獲取返回值,就丟失了指針,造成內存泄露
  • u.swap(…):交換兩個unique_ptr所指向的對象。
#include <iostream>
#include <memory>

class Frame {};

int main()
{
  std::unique_ptr<Frame> f1(new Frame());
  Frame* f = f1.get();

  std::unique_ptr<Frame> f2;
  f2.reset(f1.release());
  f2.swap(f1);

  return 0;
}

額外需要注意的是:儘管unique_ptr禁止了拷貝構造和拷貝賦值,但是,nullptr是可以用來賦值的

u = nullptr;       //釋放u所指向的對象,將u置爲空
u.reset(nullptr);    // u置爲空

特點

與auto_ptr相比unique_ptr有如下特點:

  1. unique_ptr是一個獨享所有權的智能指針,無法進行復制構造、copy賦值操作,只能進行移動操作。無法使兩個unique_ptr指向同一個對象;
  2. unique_ptr智能指向一個對象,如果當它指向其他對象時,之前所指向的對象會被摧毀;
  3. unique_ptr對象會在它們自身被銷燬時使用刪除器自動刪除它們管理的對象;
  4. unique_ptr支持創建數組對象方法。

unique_ptr源碼剖析

unique_ptr的源碼部分分成指向單個類型對象和指向數組類型兩部分,其中主要源碼內容如下:

// 指向單個類型對象
template <typename _Tp, typename _Dp = default_delete<_Tp> >
class unique_ptr
{
  class _Pointer {};

  typedef std::tuple<typename _Pointer::type, _Dp>  __tuple_type;
  __tuple_type                                      _M_t;

  public:
    typedef typename _Pointer::type   pointer;
    typedef _Tp                       element_type;
    typedef _Dp                       deleter_type;

    constexpr unique_ptr() noexcept : _M_t()
    { static_assert(!is_pointer<deleter_type>::value,
        "constructed with null function pointer deleter"); }

    explicit unique_ptr(pointer __p) noexcept : _M_t(__p, deleter_type())     // 裸指針構造函數,explicit阻止隱式構造
    { static_assert(!is_pointer<deleter_type>::value,
        "constructed with null function pointer deleter"); }

    unique_ptr(unique_ptr&& __u) noexcept                 // 移動構造函數
    : _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { }

    ~unique_ptr() noexcept                                // 析構函數
    {
      auto& __ptr = std::get<0>(_M_t);
      if (__ptr != nullptr)
        get_deleter()(__ptr);
      __ptr = pointer();
    }

    unique_ptr& operator=(unique_ptr&& __u) noexcept      // move賦值運算符重載
    {
      reset(__u.release());
      get_deleter() = std::forward<deleter_type>(__u.get_deleter());
      return *this;
    }

    typename add_lvalue_reference<element_type>::type operator*() const   // 解引用
    {
      _GLIBCXX_DEBUG_ASSERT(get() != pointer());
      return *get();
    }

    pointer operator->() const noexcept                   // 智能指針->運算符
    {
      _GLIBCXX_DEBUG_ASSERT(get() != pointer());
      return get();
    }

    pointer get() const noexcept                          // 獲得裸指針
    { return std::get<0>(_M_t); }

    deleter_type& get_deleter() noexcept                  // 獲取刪除器
    { return std::get<1>(_M_t); }

    explicit operator bool() const noexcept               // 類型轉換函數,用於條件語句,如if(uniptr)之類
    { return get() == pointer() ? false : true; }

    pointer release() noexcept                            // 釋放指針
    {
      pointer __p = get();
      std::get<0>(_M_t) = pointer();
      return __p;
    }

    void reset(pointer __p = pointer()) noexcept          // 重置指針
    {
      using std::swap;
      swap(std::get<0>(_M_t), __p);
      if (__p != pointer())
        get_deleter()(__p);
    }

    void swap(unique_ptr& __u) noexcept                   // 交換指針
    {
      using std::swap;
      swap(_M_t, __u._M_t);
    }

    unique_ptr(const unique_ptr&) = delete;               // 禁止拷貝構造函數
    unique_ptr& operator=(const unique_ptr&) = delete;    // 禁止copy賦值運算符重載
};

// 指向數組類型
template<typename _Tp, typename _Dp>
class unique_ptr<_Tp[], _Dp>
{
  ...           // 與上文代碼類似,省略

  public:
    typename std::add_lvalue_reference<element_type>::type operator[](size_t __i) const     // 數組[]操作符
    {
      _GLIBCXX_DEBUG_ASSERT(get() != pointer());
      return get()[__i];
    }
  1. unique_ptr的構造函數被聲明爲explicit,禁止隱式類型轉換的行爲。可避免將一個普通指針傳遞給形參爲智能指針的函數。假設,如果允許將裸指針傳給void foo(std::unique_ptr<T>)函數,則在函數結束後會因形參超出作用域,裸指針將被delete的誤操作;
  2. unique_ptr的拷貝構造和拷貝賦值均被聲明爲delete。因此無法實施拷貝和賦值操作,但可以移動構造和移動賦值;
  3. 刪除器是unique_ptr類型的一部分。默認爲std::default_delete,內部是通過調用delete來實現;
  4. unique_ptr可以指向數組,並重載了operator []運算符。

使用場景

工廠函數

作爲工廠函數的返回類型:

  1. 工廠函數負責在堆上創建對象,但是調用工廠函數的用戶纔會真正去使用這個對象,並且要負責這個對象生命週期的管理。所以使用unique_ptr是最好的選擇。這正好是std::unique_ptr擅長的地方,因爲調用者獲得了工廠返回的資源的所有權,當unique_ptr析構時,它會自動銷燬所擁有的指針;
  2. unique_ptr轉爲shared_ptr很容易,作爲工廠函數本身並不知道用戶希望所創建的對象的所有權是專有的還是共享的,返回unique_ptr時調用者可以按照需要做變換。

PImpl機制

Pimpl,英文pointer to implementation,即指向實現的指針。主要思想是將私有數據和函數放入一個單獨的類中,並保存在一個實現文件中,然後在頭文件中對這個類進行前向聲明並保存一個指向該實現類的指針

也就是說,將曾經放在主類中的數據成員放到實現類中去,然後通過指針間接地訪問那些數據成員。此時主類中存在只有聲明而沒有定義的類型,也叫非完整類型。

Pimpl的優點:

  1. 信息隱藏,將具體類的實現封裝到另一個類裏面,使用者只能看到一個向前的聲明和對應的指針。除非使用者去修改對應的實現,否則,它將無法知道具體的實現,也就無法通過一些非法的方式去訪問。從一定程度上防止了封裝的泄漏;
  2. 降低耦合,包含該類聲明的文件也不會因爲類實現的改變而重新編譯,節約編譯時間

Pimpl的缺點:

  1. 需要手動釋放資源,可以使用unique_ptr來解決這個缺點;
  2. 真正執行的操作,需要中間增加一層指針的間接調用,增加開銷;通過間接訪問,增加了閱讀代碼的難度,程序員書寫代碼也變得複雜。

相關閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章