現代 C++:一文讀懂智能指針

智能指針

C++11 引入了 3 個智能指針類型:git

  1. std::unique_ptr<T> :獨佔資源全部權的指針。
  2. std::shared_ptr<T> :共享資源全部權的指針。
  3. std::weak_ptr<T> :共享資源的觀察者,須要和 std::shared_ptr 一塊兒使用,不影響資源的生命週期。

std::auto_ptr 已被廢棄。github

std::unique_ptr

簡單說,當咱們獨佔資源的全部權的時候,可使用 std::unique_ptr 對資源進行管理——離開 unique_ptr 對象的做用域時,會自動釋放資源。這是很基本的 RAII 思想。數組

std::unique_ptr 的使用比較簡單,也是用得比較多的智能指針。這裏直接看例子。bash

  1. 使用裸指針時,要記得釋放內存。
{
    int* p = new int(100);
    // ...
    delete p;  // 要記得釋放內存
}
複製代碼
  1. 使用 std::unique_ptr 自動管理內存。
{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    //...
    // 離開 uptr 的做用域的時候自動釋放內存
}
複製代碼
  1. std::unique_ptr 是 move-only 的。
{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    std::unique_ptr<int> uptr1 = uptr;  // 編譯錯誤,std::unique_ptr<T> 是 move-only 的

    std::unique_ptr<int> uptr2 = std::move(uptr);
    assert(uptr == nullptr);
}
複製代碼
  1. std::unique_ptr 能夠指向一個數組。
{
    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; i++) {
        uptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << uptr[i] << std::endl;
    }   
}
複製代碼
  1. 自定義 deleter。
{
    struct FileCloser {
        void operator()(FILE* fp) const {
            if (fp != nullptr) {
                fclose(fp);
            }
        }   
    };  
    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}
複製代碼
  1. 使用 Lambda 的 deleter。
{
    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            fclose(fp);
        });
}
複製代碼

std::shared_ptr

std::shared_ptr 其實就是對資源作引用計數——當引用計數爲 0 的時候,自動釋放資源。函數

{
    std::shared_ptr<int> sptr = std::make_shared<int>(200);
    assert(sptr.use_count() == 1);  // 此時引用計數爲 1
    {   
        std::shared_ptr<int> sptr1 = sptr;
        assert(sptr.get() == sptr1.get());
        assert(sptr.use_count() == 2);   // sptr 和 sptr1 共享資源,引用計數爲 2
    }   
    assert(sptr.use_count() == 1);   // sptr1 已經釋放
}
// use_count 爲 0 時自動釋放內存
複製代碼

和 unique_ptr 同樣,shared_ptr 也能夠指向數組和自定義 deleter。ui

{
    // C++20 才支持 std::make_shared<int[]>
    // std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
    std::shared_ptr<int[]> sptr(new int[10]);
    for (int i = 0; i < 10; i++) {
        sptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << sptr[i] << std::endl;
    }   
}

{
    std::shared_ptr<FILE> sptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            std::cout << "close " << fp << std::endl;
            fclose(fp);
        });
}
複製代碼

std::shared_ptr 的實現原理

一個 shared_ptr 對象的內存開銷要比裸指針和無自定義 deleter 的 unique_ptr 對象略大。this

std::cout << sizeof(int*) << std::endl;  // 輸出 8
  std::cout << sizeof(std::unique_ptr<int>) << std::endl;  // 輸出 8
  std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)
            << std::endl;  // 輸出 40

  std::cout << sizeof(std::shared_ptr<int>) << std::endl;  // 輸出 16
  std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
    std::cout << "close " << fp << std::endl;
    fclose(fp);
  }); 
  std::cout << sizeof(sptr) << std::endl;  // 輸出 16
複製代碼

無自定義 deleter 的 unique_ptr 只須要將裸指針用 RAII 的手法封裝好就行,無需保存其它信息,因此它的開銷和裸指針是同樣的。若是有自定義 deleter,還須要保存 deleter 的信息。spa

shared_ptr 須要維護的信息有兩部分:3d

  1. 指向共享資源的指針。
  2. 引用計數等共享資源的控制信息——實現上是維護一個指向控制信息的指針。

因此,shared_ptr 對象須要保存兩個指針。shared_ptr 的 的 deleter 是保存在控制信息中,因此,是否有自定義 deleter 不影響 shared_ptr 對象的大小。指針

當咱們建立一個 shared_ptr 時,其實現通常以下:

std::shared_ptr<T> sptr1(new T);
複製代碼

image

複製一個 shared_ptr :

std::shared_ptr<T> sptr2 = sptr1;
複製代碼

image
爲何控制信息和每一個 shared_ptr 對象都須要保存指向共享資源的指針?可不能夠去掉 shared_ptr 對象中指向共享資源的指針,以節省內存開銷?

答案是:不能。 由於 shared_ptr 對象中的指針指向的對象不必定和控制塊中的指針指向的對象同樣。

來看一個例子。

struct Fruit {
    int juice;
};

struct Vegetable {
    int fiber;
};

struct Tomato : public Fruit, Vegetable {
    int sauce;
};

 // 因爲繼承的存在,shared_ptr 可能指向基類對象
std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> vegetable = tomato;
複製代碼

image

另外,std::shared_ptr 支持 aliasing constructor。

template< class Y > shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
複製代碼

Aliasing constructor,簡單說就是構造出來的 shared_ptr 對象和參數 r 指向同一個控制塊(會影響 r 指向的資源的生命週期),可是指向共享資源的指針是參數 ptr。看下面這個例子。

using Vec = std::vector<int>;
std::shared_ptr<int> GetSPtr() {
    auto elts = {0, 1, 2, 3, 4};
    std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);
    return std::shared_ptr<int>(pvec, &(*pvec)[2]);
}

std::shared_ptr<int> sptr = GetSPtr();
for (int i = -2; i < 3; ++i) {
    printf("%d\n", sptr.get()[i]);
}
複製代碼

image

看上面的例子,使用 std::shared_ptr 時,會涉及兩次內存分配:一次分配共享資源對象;一次分配控制塊。C++ 標準庫提供了 std::make_shared 函數來建立一個 shared_ptr 對象,只須要一次內存分配。

image

這種狀況下,不用經過控制塊中的指針,咱們也能知道共享資源的位置——這個指針也能夠省略掉。

image

std::weak_ptr

std::weak_ptr 要與 std::shared_ptr 一塊兒使用。 一個 std::weak_ptr 對象看作是 std::shared_ptr 對象管理的資源的觀察者,它不影響共享資源的生命週期:

  1. 若是須要使用 weak_ptr 正在觀察的資源,能夠將 weak_ptr 提高爲 shared_ptr。
  2. 當 shared_ptr 管理的資源被釋放時,weak_ptr 會自動變成 nullptr。\
void Observe(std::weak_ptr<int> wptr) {
    if (auto sptr = wptr.lock()) {
        std::cout << "value: " << *sptr << std::endl;
    } else {
        std::cout << "wptr lock fail" << std::endl;
    }
}

std::weak_ptr<int> wptr;
{
    auto sptr = std::make_shared<int>(111);
    wptr = sptr;
    Observe(wptr);  // sptr 指向的資源沒被釋放,wptr 能夠成功提高爲 shared_ptr
}
Observe(wptr);  // sptr 指向的資源已被釋放,wptr 沒法提高爲 shared_ptr
複製代碼

image

當 shared_ptr 析構並釋放共享資源的時候,只要 weak_ptr 對象還存在,控制塊就會保留,weak_ptr 能夠經過控制塊觀察到對象是否存活。

image

enable_shared_from_this

一個類的成員函數如何得到指向自身(this)的 shared_ptr? 看看下面這個例子有沒有問題?

class Foo {
 public:
  std::shared_ptr<Foo> GetSPtr() {
    return std::shared_ptr<Foo>(this);
  }
};

auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);
複製代碼

上面的代碼其實會生成兩個獨立的 shared_ptr,他們的控制塊是獨立的,最終致使一個 Foo 對象會被 delete 兩次。

image

成員函數獲取 this 的 shared_ptr 的正確的作法是繼承 std::enable_shared_from_this。

class Bar : public std::enable_shared_from_this<Bar> {
 public:
  std::shared_ptr<Bar> GetSPtr() {
    return shared_from_this();
  }
};

auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);
複製代碼

通常狀況下,繼承了 std::enable_shared_from_this 的子類,成員變量中增長了一個指向 this 的 weak_ptr。這個 weak_ptr 在第一次建立 shared_ptr 的時候會被初始化,指向 this。

image

彷佛繼承了 std::enable_shared_from_this 的類都被強制必須經過 shared_ptr 進行管理。

auto b = new Bar;
auto sptr = b->shared_from_this();
複製代碼

在個人環境下(gcc 7.5.0)上面的代碼執行的時候會直接 coredump,而不是返回指向 nullptr 的 shared_ptr:

terminate called after throwing an instance of 'std::bad_weak_ptr'
 what():  bad_weak_ptr
複製代碼

小結

智能指針,本質上是對資源全部權和生命週期管理的抽象:

  1. 當資源是被獨佔時,使用 std::unique_ptr 對資源進行管理。
  2. 當資源會被共享時,使用 std::shared_ptr 對資源進行管理。
  3. 使用 std::weak_ptr 做爲 std::shared_ptr 管理對象的觀察者。
  4. 經過繼承 std::enable_shared_from_this 來獲取 this 的 std::shared_ptr 對象。

參考資料

  1. Back to Basics: Smart Pointers
  2. unique_ptr
  3. shared_ptr
  4. weak_ptr
  5. enable_shared_from_this
相關文章
相關標籤/搜索