TL;DR

在上一篇 std::unique_ptr筆記中,已經簡短介紹智能指標的目的及用法。

在本篇筆記中,會逐步介紹 std::shared_ptr 的基本概念、用法、及簡易實作。


shared_ptr

簡介

unique_ptr 類似,shared_ptr 也是 C++11 中引入的一種智能指標,其目的是為了減少記憶體流失 (Memory Leak) 的風險。

unique_ptr 不同的地方在於,shared_ptr 允許多個 shared_ptr 實例指向同一塊動態記憶體。只有當最後一個 shared_ptr 被銷毀或重置後,該記憶體才會被釋放。

shared_ptr 會維護一個引用計數 (Reference Counting) 來管理記憶體的所有權。每當一個新的 shared_ptr 被創建或複製時,引用計數會增加;而當一個 shared_ptr 被銷毀或重置時,引用計數會減少。當引用計數歸零時,表示不再有指標指向該記憶體,此時記憶體將被自動釋放。

引用計數改變的情況

shared_ptr 的引用計數會在創建或被複製時增加,並在銷毀或重置時減少,主要會在以下幾種情況改變:

  • 建構子 (Constructor)
  • 解構子 (Destructor)
  • 拷貝建構子 (Copy Constructor)
  • 拷貝賦值運算子 (Copy Assignment Operator)

要注意的是,移動建構子 (Move Constructor) 及移動運算子 (Move Assignment Operator) 會有所有權轉移的步驟,在轉移後原本的指標會被設定為空指標,所以引用計數並不會改變。

成員變數與控制區塊 (Control Block)

shared_ptr 本身除了指向動態記憶體區塊的指標之外,還有另外一個指標指向一個控制區塊 (Control Block),如下圖所示。其中控制區塊包含以下內容:

  • 引用計數
  • 弱引用計數: 此與 std::weak_ptr 有關,這邊先略過,會在後續筆記中做介紹
  • 其他資料: 包含 Deletor 及 Allocator 等

控制區塊 (Control Block)

而控制區塊會在以下幾種情況被建立:

  • 使用 make_shared 建立 shared_ptr
  • 使用 unique_ptr 建立 shared_ptr
  • 使用 shared_ptr 的原始指標建構子時 (盡量避免此做法)

使用 make_shared 的重要性

創建 shared_ptr 時要盡量使用 make_shared 進行創建,避免同一個動態記憶體區塊有多個控制區塊進行管理。如以下範例:

1
2
3
4
5
{
    int* raw_ptr = new int(1);
    std::shared_ptr<int> sptr1(raw_ptr);
    std::shared_ptr<int> sptr2(raw_ptr);
} // double free error

在此範例中,sptr1sptr2 皆是以原始指標建立 shared_ptr,所以他們都會建立對於 raw_ptr 指向的記憶體的控制區塊。也就是說同一塊記憶體會有兩個控制區塊進行管理。

sptr1 的生命週期結束後,sptr1 所指向的控制區塊中的引用計數歸零,並自動清除 raw_ptr 所指向的記憶體。而 sptr2 所指向的控制區塊中的引用計數也會因為生命週期結束而歸零,但要清除記憶體時,該記憶體已被 sptr1 清除,故會造成重複清除錯誤 (Double Free Error)。

特性

  • 大小:

    • shared_ptr 的大小比原始指標 (raw pointer) 還大,因為裡面多一個指向控制區塊的指標。
  • 執行緒安全 (Thread-Safe):

    • shared_ptr 中引用計數的增減是原子操作,因此是執行緒安全的,這代表可以多個執行緒安全的使用 shared_ptr
    • shared_ptr 指向的記憶體本身並不是執行緒安全的,因此會需要額外的同步機制。
  • 循環引用的問題 (Circular Reference)

    • shared_ptr 有時可能會有循環引用的問題,可搭配 weak_ptr 解決問題。這部分我會在後續的筆記中介紹。
  • 陣列

    • shared_ptr 是在 C++17 才引入動態陣列的用法。

shared_ptr 用法

基本用法

1
2
3
4
5
6
7
8
std::shared_ptr<int> sp1 = std::make_shared<int>(0); // 使用 make_shared 創建
std::shared_ptr<int> sp2(sp1);                       // 複製建構,引用計數增加
std::shared_ptr<int> sp3 = sp1;                      // 複製,引用計數增加
std::shared_ptr<int> sp4(std::move(sp2));            // 移動建構,所有權轉移,引用計數不變,`sp2` 變 `nullptr`
std::shared_ptr<int> sp5 = std::move(sp3);           // 所有權轉移,引用計數不變,`sp3` 變 `nullptr`
{
    std::shared_ptr<int> sp6 = sp1;                  // 引用計數增加
} // 離開此 scope 後,引用計數減少

使用 unique_ptr 建立

shared_ptr 可以透過 unique_ptr 建立,此時會建立一個控制區塊 (Control Block)。

1
2
3
std::unique_ptr<int> uptr = std::make_unique<int>(10);
std::shared_ptr<int> sptr = std::move(uptr);
// `uptr` 變成 `nullptr`,並由 `sptr` 管理該記憶體

自訂刪除器 (Custom Deleter)

unique_ptr 相同,皆可以使用自訂的刪除器

1
std::shared_ptr<FILE> file_ptr(fopen("temp.txt", "w"), &fclose);

在這個範例中,使用 fclose 作為自訂刪除器,當釋放 FILE* 記憶體時,會自動使用 fclose 關閉文件。

使用於函式參數

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void call_by_val(std::shared_ptr<int> ptr) {
    std::cout << "call_by_val, use_count: " << ptr.use_count() << std::endl;
}

void call_by_ref(std::shared_ptr<int>& ptr) {
    std::cout << "call_by_ref, use_count: " << ptr.use_count() << std::endl;
}

void call_by_const_ref(const std::shared_ptr<int>& ptr) {
    std::cout << "call_by_const_ref, use_count: " << ptr.use_count() << std::endl;
}

int main() {
    auto sptr = std::make_shared<int>(10); // Initial shared_ptr with reference count = 1

    std::cout << "Initial use_count: " << sptr.use_count() << std::endl;

    call_by_val(sptr);
    std::cout << "After, use_count: " << sptr.use_count() << std::endl;

    call_by_ref(sptr);
    std::cout << "After, use_count: " << sptr.use_count() << std::endl;

    call_by_const_ref(sptr);
    std::cout << "After, use_count: " << sptr.use_count() << std::endl;

    return 0;
}

其輸出結果如下

Initial use_count: 1
call_by_val, use_count: 2
After, use_count: 1
call_by_ref, use_count: 1
After, use_count: 1
call_by_const_ref, use_count: 1
After, use_count: 1

實作

簡易實作 (不含控制區塊及 Atomic 操作)

實作 shared_ptr 需注意引用計數改變的情況。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
template <typename T>
class shared_ptr {
private:
    using element_type = T;
    using pointer      = T*;
    using reference    = T&;

private:
    pointer p_ptr;
    int*    p_ref_count;

public:
    /**
     * \brief Constructor
     */
    constexpr shared_ptr() noexcept 
        : p_ptr(nullptr), p_ref_count(nullptr) {}

    /**
     * \brief Constructor
     */
    explicit shared_ptr(pointer p)
        : p_ptr(p), p_ref_count(new int(1)) 
    {
        if (p == nullptr)
            *p_ref_count = 0;
    }

    /**
     * \brief Destructor
     */
    ~shared_ptr() {
        reset();
    }

    /**
     * \brief Copy constructor
     */
    shared_ptr(const shared_ptr<element_type>& other) 
        : p_ptr(other.p_ptr), p_ref_count(other.p_ref_count) 
    {
        if (p_ptr != nullptr)
            ++(*p_ref_count);
    }

    /**
     * \brief Copy assignment operator
     */
    shared_ptr<element_type>& operator=(const shared_ptr<element_type>& other) {
        if (this != &other) {
            // 
            reset();

            // 
            p_ptr = other.p_ptr;
            p_ref_count = other.p_ref_count;
            if (p_ptr != nullptr)
                ++(*p_ref_count);
        }
        return *this;
    }

    /**
     * \brief Move Constructor
     */
    shared_ptr(shared_ptr<element_type>&& other)
        : p_ptr(other.p_ptr), p_ref_count(other.p_ref_count)
    {
        other.p_ptr = nullptr;
        other.p_ref_count = nullptr;
    }

    /**
     * \breif Move Assignment Operator
     */
    shared_ptr<element_type>& operator=(shared_ptr<element_type>&& other) {
        if (this != &other) {
            // 
            reset();

            // 
            p_ptr = other.p_ptr;
            p_ref_count = other.p_ref_count;

            //
            other.p_ptr = nullptr;
            other.p_ref_count = nullptr;
        }
        return *this;
    }

public:
    /**
     * \brief Dereference operator
     */
    reference operator*() const {
        return *p_ptr;
    }

    /**
     * \brief Arrow operator
     */
    pointer operator->() const {
        return p_ptr;
    }

/* Modifiers */
public:
    /**
     * \brief Replaces the managed object
     */
    void reset() noexcept {
        if (p_ref_count != nullptr && --(*p_ref_count) == 0) {
            delete p_ptr;
            delete p_ref_count;
        }
        p_ptr = nullptr;
        p_ref_count = nullptr;
    }


/* Observers */
public:
    /**
     * \brief Get the underlying pointer
     */
    pointer get() const {
        return p_ptr;
    }

    /**
     * \brief Get the reference count
     */
    int use_count() const {
        return p_ref_count != nullptr ? *p_ref_count : 0;
    }
};
  • 此實作是簡化版的 shared_ptr,只使用了 p_ref_count 計算引用計數,並未建立控制區塊。
  • 此實作並未在引用計數增減的部分使用原子化 (Atomic) 操作,因此並不是執行緒安全的。
  • 此實作並未使用自訂刪除器的功能。
  • unique_ptrshared_ptr 是透過更改 *-> 這兩個操作符,使得 shared_ptr 可以像原始指標一樣操作。

參考資料