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 等
而控制區塊會在以下幾種情況被建立:
- 使用
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
|
在此範例中,sptr1
及 sptr2
皆是以原始指標建立 shared_ptr
,所以他們都會建立對於 raw_ptr
指向的記憶體的控制區塊。也就是說同一塊記憶體會有兩個控制區塊進行管理。
當 sptr1
的生命週期結束後,sptr1
所指向的控制區塊中的引用計數歸零,並自動清除 raw_ptr
所指向的記憶體。而 sptr2
所指向的控制區塊中的引用計數也會因為生命週期結束而歸零,但要清除記憶體時,該記憶體已被 sptr1
清除,故會造成重複清除錯誤 (Double Free Error)。
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_ptr
,shared_ptr
是透過更改 *
及 ->
這兩個操作符,使得 shared_ptr
可以像原始指標一樣操作。
參考資料#