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_ptrshared_ptrweak_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 對象的生命週期。由於 ProductAProductBIProduct 的子類別,所以可以透過父類別指標來操作不同的子類別物件,


簡易實作

簡易版 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

參考資料