TL;DR#
在 C++ 中,std::unique_ptr
是一種很常用的智能指標 (Smart Pointer),其主要目的是自動管理動態記憶體,避免記憶體流失 (Memory Leak)。
在本篇筆記中,我會先由記憶體流失及智能指標 (Smart Pointer) 開始簡介,再逐步介紹 std::unique_ptr
的基本概念、用法、及簡易實作。
記憶體流失簡介#
如果要用一句話簡介記憶體流失 (Memory Leak) 的話,可以說:「當一個程式中分配的動態記憶體,在使用完畢後並未將其釋放,則會造成記憶體流失,減少可使用的主記憶體容量」。
如以下範例所示:
1
2
3
|
void func_with_memory_leak() {
int* ptr = new int;
}
|
在 func_with_memory_leak()
這個函式中,此函式分配了一塊記憶體,並用 ptr
指向這塊記憶體。當此函式結束後,分配的這個記憶體尚未進行釋放,由於 ptr
這個指標變數的生命週期結束了,導致沒有變數指向這塊已經分配好的記憶體,所以此塊記憶體在整個程式運行期間就沒有辦法進行釋放。
如果只有一小塊記憶體流失的話對整個程式的影響不大,但如果這類函式是放在迴圈當中 (如下列程式碼所示),或者是需要常常被呼叫的函式中,就很有可能因主記憶體的容量不夠,進而降低電腦的效能。
1
2
3
|
while (some_condition()) {
func_with_memory_leak();
}
|
因此,若是分配出來的動態記憶體,在生命週期結束後,都要記得進行釋放,如下列程式碼所示:
1
2
3
4
5
6
7
|
void func_without_memory_leak() {
int* ptr = new int;
// ...
delete ptr;
}
|
另外,使用 exception
時也會造成記憶體流失的風險。如下列程式碼所示,當例外情況發生時,如果在記憶體釋放前拋出了例外,則記憶體可能會無法正常釋放,導致記憶體流失。
1
2
3
4
5
6
7
8
|
void func_with_potential_leak() {
int* ptr = new int;
if (some_condition())
throw std::runtime_error("An error occurred, the memory hasn't freed");
delete ptr;
}
|
因此,會需要在例外部分釋放記憶體,如下所示:
1
2
3
4
5
6
7
8
9
10
|
void func_with_potential_leak() {
int* ptr = new int;
if (some_condition()) {
delete ptr;
throw std::runtime_error("An error occurred");
}
delete ptr;
}
|
智能指標簡介#
在 C++ 中,我們可以透過智能指標 (Smart Pointer) 避免記憶體流失的問題。
智能指標是透過 RAII (維基百科) 的概念,將原始指標 (Raw Pointer) 包裝起來,並在智能指標的生命週期 (Lifetime) 結束時,透過解構子 (Destructor) 自動釋放該原始指標所指向的動態記憶體。
有點像是下列程式碼的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
template <typename T>
class simple_smart_ptr {
public:
simple_smart_ptr(T* ptr)
: p_raw_ptr(ptr) {}
~simple_smart_ptr() {
delete p_raw_ptr;
std::cout << "raw pointer deleted" << std::endl;
}
private:
T* p_raw_ptr;
};
void func() {
simple_smart_ptr<int> sptr(new int(0));
} // 離開這個 `func()` 的 scope 後,記憶體將會自動被釋放
|
這樣就簡化使用原始指標的方式,不需要額外透過 delete
釋放記憶體,進而降低記憶體流失的風險。
而在 C++ 中有 3 種智能指標,分別為 unique_ptr
、shared_ptr
、weak_ptr
。本篇筆記主要探討的是 unique_ptr
,其他種智能指標我會在後續筆記中進行介紹。
unique_ptr 簡介#
std::unique_ptr
是 C++11 中引入的智能指標,其具有 move-only 的特性,這代表 unique_ptr
所管理的資源只會有一個擁有者,也代表該動態記憶體只會有一個負責管理其生命週期的變數,這樣可以避免多重釋放 (Double Deletion) 的問題。 若是要轉換擁有權的話,只能透過移動語意 (Move Semantics) 的方式。
與原始指標的差異#
std::unique_ptr
是一種輕量且快速的智能指標,其大小與原始指標相同,使用上也沒有太多差異。
使用時機#
unique_ptr
通常用於那些需要唯一擁有權 (exclusive ownership) 的資源管理場景。
基本用法#
基本語法#
1
2
3
4
5
6
|
{
int* raw_ptr = new int();
std::unique_ptr<int> uptr(raw_ptr);
*uptr = some_random_value();
uptr->member_func();
} // 這個 scope 結束後,記憶體將自動釋放
|
注意:雖然可以像上述程式碼一樣使用原始指標創建 unique_ptr
,但應盡量避免這種做法,以避免多個智能指標指向同一塊記憶體。更好的做法是透過 std::make_unique
來創建 unique_ptr
。
搭配 make_unique 創建#
std::make_unique
是 C++14 中引入的函式,用來簡化 unique_ptr
的創建過程。如以下範例所示:
1
|
std::unique_ptr<int> ptr = std::make_unique<int>(some_random_val());
|
也可以透過 auto
進行簡化:
1
|
auto ptr = std::make_unique<int>(some_random_val());
|
使用於函式參數#
當 unique_ptr
作為函式參數時,可以通過移動語意來傳遞所有權。以下範例展示了正確和錯誤的用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void process_data(std::unique_ptr<int> data) {
// ...
}
void wrong_usage_with_error() {
std::unique_ptr<int> uptr = std::make_unique<int>(58);
process_data(uptr); // ERROR: `unique_ptr` can't be copied
}
void use_by_move_semantics() {
std::unique_ptr<int> uptr = std::make_unique<int>(58);
process_data(std::move(uptr));
// `uptr` will be `nullptr` after transferring ownership to `process_data()`
}
|
如果只需要讀取數據而不改變所有權,可以使用 const 及 call-by-reference 來傳遞,如以下範例所示:
1
2
3
4
5
6
7
8
|
void process_data_with_access_only(const std::unique_ptr<int>& read_only_data) {
// ...
}
void use_with_const_ref() {
std::unique_ptr<int> uptr = std::make_unique<int>(58);
process_data_with_access_only(uptr);
}
|
這樣可以確保 unique_ptr 仍然保持所有權,同時允許函式內部讀取其指向的對象。
所有權轉移 (使用移動語意 Move Semantics)#
unique_ptr
是 move-only 型別,因此我們可以通過 std::move
來轉移其管理資源的所有權。在以下範例中,ptr1
的所有權被轉移給 ptr2
,此時 ptr1
將不再指向任何有效的記憶體。
1
2
|
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::move(ptr1); // transfer ownership from `ptr1` to `ptr2`
|
轉移所有權後,ptr1 變為 nullptr
,不再指向有效的記憶體。
陣列用法#
unique_ptr
也可以用來管理動態分配的陣列,如以下範例所示:
1
|
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
|
在這個範例中,arr
是一個 unique_ptr
,它管理一個包含 10 個 int
元素的動態陣列。當 arr
生命週期結束後,整個陣列會自動被正確釋放。
使用自訂的刪除器 (Custom Deleter)#
unique_ptr
可以使用自訂的刪除器,讓他在釋放記憶體時執行特定操作,如以下範例所示:
1
|
std::unique_ptr<FILE, decltype(&fclose)> file_ptr(fopen("temp.txt", "r"), &fclose);
|
在這個範例中,使用 fclose
作為自訂刪除器,當釋放 FILE*
記憶體時,會自動使用 fclose
關閉文件。
搭配工廠函式 (Factory Function) 使用#
std::unique_ptr
的一個常見的用法,是搭配工廠函式 (Factory Function) 一起使用,範例如下:
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
|
class IProduct {
public:
virtual void say_hello() const = 0;
virtual ~IProduct() = default;
};
class ProductA : public IProduct {
public:
void say_hello() const override {
std::cout << "Hello from Product A!" << std::endl;
}
};
class ProductB : public IProduct {
public:
void say_hello() const override {
std::cout << "Hello from Product B!" << std::endl;
}
};
std::unique_ptr<IProduct> create_product(const std::string& type) {
if (type == "A")
return std::make_unique<ProductA>();
else if (type == "B")
return std::make_unique<ProductB>();
return nullptr;
}
int main() {
//
auto productA = create_product("A");
auto productB = create_product("B");
//
if (productA != nullptr)
productA->say_hello();
if (productB != nullptr)
productB->say_hello();
return 0;
}
|
在這個範例中,create_product
函式返回一個 std::unique_ptr<IProduct>
,它負責自動管理 Product 對象的生命週期。由於 ProductA
和 ProductB
是 IProduct
的子類別,所以可以透過父類別指標來操作不同的子類別物件,
簡易實作#
簡易版 unique_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
|
template <typename T>
class unique_ptr {
private:
using element_type = T;
using pointer = T*;
private:
pointer p_ptr;
public:
constexpr unique_ptr()
: p_ptr(nullptr) {}
explicit unique_ptr(pointer p)
: p_ptr(p) {}
unique_ptr(const unique_ptr& other) = delete;
unique_ptr& operator=(const unique_ptr& other) = delete;
unique_ptr(unique_ptr&& other) noexcept
: p_ptr(other.p_ptr)
{
other.p_ptr = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (other != *this) {
delete p_ptr;
p_ptr = other.p_ptr;
other.p_ptr = nullptr;
}
return *this;
}
~unique_ptr() {
delete p_ptr;
}
public:
element_type& operator*() const noexcept {
return *p_ptr;
}
pointer operator->() const noexcept {
return p_ptr;
}
explicit operator bool() const noexcept {
return p_ptr != nullptr;
}
public:
pointer get() const noexcept {
return p_ptr;
}
void reset(pointer ptr = pointer()) noexcept {
pointer old_ptr = p_ptr;
p_ptr = ptr;
if (old_ptr != nullptr)
delete old_ptr;
}
};
|
- 因為
unique_ptr
是 move-only,所以要透過 delete
的方式刪除 copy constructor 及 copy operator。
- operator
*
及 operator ->
對於此物件蠻重要的,因為透過更改這兩個 operator,就可以像使用原始指標一樣操作 unique_ptr
。
參考資料#