iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

前情提要

C++ 記憶體共分為三區:
1. 靜態(static):儲存「全域變數」與「靜態變數」,會存活至整段程式結束為止。
2. 堆疊(stack):儲存「區域變數」與「區塊變數」,離開如函數的作用域後就消失,如 int* ptr = new int;ptr
3. 堆積(heap):儲存人為手動以 newdelete 配置記憶體空間的變數,如 int* ptr = new int;new int

正文開始

試觀察以下 C++ 程式碼:

#include <iostream>
using namespace std;

int main() {
    while (1) {
        int* ptr = new int;
    }
    return 0;
}

有沒有發現它非常地恐怖?

  • 首先,while(1) 表示會永不停歇地持續進行。
  • int* ptr = new int; 透過 new 關鍵字不斷產生新的 int ,這些新的 int 不斷吃掉堆積(heap)區的空間,卻沒有釋放出來。

這會造成什麼結果?每輪迴圈結束後,ptr 指標都從堆疊空間釋出、但 int 在堆積區佔的空間並沒有被釋出,形成「記憶體洩漏(memory leak)」。

讓指標幫忙自動化清理

既然人類有可能會忘記去 delete 釋放堆積區中的記憶體,那何不請指標來幫忙處理?

於是,「聰明的指標」(國家教育研究院的樂詞網翻譯成「智慧型指標」)誕生了,它能夠自動幫忙 delete,但要說是怎麼達成的,其實只是把指標包在一個含有解構子(destructor)的類別(class)而已:

template <typename T>
class MySmartPointer {
private:
    T* raw_ptr; // class 裡面包的仍是傳統指標
public:
    // 建構子:拿到傳統指標 p 後,存進 class 裡面的 raw_ptr 變數
    MySmartPointer(T* p) : raw_ptr(p) {} 

    ~MySmartPointer() {
        // 把 delete 寫在解構子,等 MySmartPointer 這個指標 class 消失,當中包含的 raw_ptr 就跟著被 delete
        delete raw_ptr; 
    }
};

智慧型指標的類別

智慧型指標其實都存在 <memory> 標頭檔中,目前共有 unique_ptrshared_ptrweak_ptr 三種。

在本段落介紹三種智慧型指標的範例中,將以下方 Cat 類別為基礎:

#include <iostream>
#include <memory>   // 要使用 Memory 標頭檔
#include <string>

using namespace std;

class Cat {
private:
    string name = "Kitty"; // 以 Cat 類別創建之物件,其 name 屬性皆為 "Kitty"

public:
    Cat() {   // 建構子:物件在堆積區創建時自動執行
        cout << "[" << name << "] saying hello" << endl;
    }

    ~Cat() {  // 解構子:物件在堆積區被釋放時自動執行 delete
        cout << "[" << name << "] is gone" << endl;
    }

    void meow() {  //  物件自帶的行為,外部程式可以透過 -> 符號來呼叫
        cout << "[" << name << "] says: Meow~" << endl;
    }
};

unique_ptr

每個存在堆積區的新物件,都只能有一個 unique_ptr 指向它,例如:

{
    unique_ptr<Cat> P1 = make_unique<Cat>(); // 建立一個名為 P1 的智慧型指標,指向在堆積區新建立的 Cat 物件
    myCat1->meow();  // 可直接呼叫新 Cat 物件中的 meow() 方法
}

如果建立一個新的名為 P2 的智慧型指標:

unique_ptr<Cat> P2;

再把 P1 指向的記憶體位址,透過賦值複製一份給 P2

P2 = P1;

這樣是不允許的,因為一個物件只能被一個 unique_ptr 指著;不過所有權可以被 std::move()「轉移」,例如:

P2 = move(P1);

這樣 P2 也可以呼叫當中的 meow() 方法,但 P1 就不行了:

P2->meow(); // P2 正指向 Cat 物件,因此可以顯示 [Kitty] says: Meow~
P1->meow(); // P1 已經不指向新建 Cat 物件,因此不行

整體 main() 函數範例如下:

int main() {
    {
        // 原本指向 Cat 物件的智慧型指標名為 P1
        unique_ptr<Cat> P1 = make_unique<Cat>();
        P1->meow(); // 執行 Cat 物件上的 meow() 方法

        // 製作一個新的、但不指向任何物件的智慧型指標 P2
        unique_ptr<Cat> P2;
        
        // P2 = P1;  錯誤寫法
        
        cout << "Now assign P1 to P2" << endl;
        P2 = move(P1); // 用 std::move() 將 Cat 物件所有權由 P1 移向 P2
        
        // 所有權轉移後,P1 變成 nullptr,沒指向任何東西
        if (P1 == nullptr) {
            cout << "P1 is now a nullptr" << endl;
        }
        
        // P2 現在可以呼叫 Cat 物件中的 meow() 方法
        P2->meow();
    }

    return 0;
}

執行結果如下:

[Kitty] saying hello
[Kitty] says: Meow~
Now assign P1 to P2
P1 is now a nullptr
[Kitty] says: Meow~
[Kitty] is gone

shared_ptr

剛剛 unique_ptr 只給一個智慧型指標指向目標物件,如果想要打破這項限制、讓兩個以上的智慧型指標指向相同物件,改用 shared_ptr 就可以了。

比較特別的是,因為可能有不只一個 shared_ptr 指向目標物件,其內建了 .use_count() 當計數器,用來表示當下包含自己共有多少指標指著相同的物件。

main() 函數中的範例程式碼如下:

int main() {
    {
        // 創建第一個智慧型指標 P1,指向新的 Cat 物件
        shared_ptr<Cat> P1 = make_shared<Cat>();
        cout << "Count after creating P1: " << P1.use_count() << endl; // 顯示 1

        // 創建第二個智慧型指標 P2,仍不指向任何物件
        shared_ptr<Cat> P2;
        
        // P1 為 1,表示目前有一個指標指向 P1 所指向的物件(就是 P1 自己)
        // P2 為 0,表示沒有指標指向 P2 指向的物件
        cout << "Count before assignment: P1=" << P1.use_count() << ", P2=" << P2.use_count() << endl; 

        cout << "Now assign P1 to P2" << endl;
        P2 = P1; // 共享所有權,P1 跟 P2 兩個智慧型指標都指向相同的 Cat 物件
        
        // 都變成 2,表示有兩個指標指向 P1 指向的物件、也有兩個指標指向 P2 指向的物件(P1 跟 P2 都指向同一個物件)
        cout << "Count after assignment: P1=" << P1.use_count() << ", P2=" << P2.use_count() << endl; 

    } // 大括號結束,計數器歸 0,Cat 物件所佔的堆積記憶體被釋放

    return 0;
}

執行結果如下:

[Kitty] saying hello
Count after creating P1: 1
Count before assignment: P1=1, P2=0
Now assign P1 to P2
Count after assignment: P1=2, P2=2
[Kitty] is gone

weak_ptr

shared_ptr 可以讓多個指標指向相同物件,而指向相同物件的指標數量將影響 .use_count() 的結果。

weak_ptr 一樣可以指向已經被 shared_ptr 指向的物件,但是 .use_count() 數量不會因此增加,一個物件也不能單純只給 weak_ptr 指著。

總之,weak_ptr 就像是個旁觀者,可以觀察 shared_ptr 指向之物件,但除非先套用 .lock() 方法檢查,否則無法直接呼叫物件當中的值與方法。

透過以下 main() 函數範例可感受 weak_ptr 之特性:

int main() {
    // 先創建 weak_ptr W1,不指向任何物件
    weak_ptr<Cat> W1;
    
    {
        // shared_ptr P1 指向新建的 Cat 物件,此時 count = 1(只有一個指標指向該 Cat 物件)
        shared_ptr<Cat> P1 = make_shared<Cat>();
        
        // W1 指向(其實是觀察)P1 指向的 Cat 物件
        W1 = P1; 
        
        // 依然顯示 1,因為只有 shared_ptr 指過來的才算、weak_ptr 指過來的不影響
        cout << "P1 count: " << P1.use_count() << endl; 

        // weak_ptr 不能直接用 ->
        // 要先用 .lock() 暫時將 weak_ptr 指標升為 shared_ptr 才可以取用當中的值與方法
        if (auto temp_shared = W1.lock()) { // 把 W1 透過 .lock() 升級為 shared_ptr,賦值於 temp_shared
            temp_shared->meow(); // temp_shared 才可以取用 meow() 方法,顯示 [Kitty] says: Meow~
        } else {
            cout << "The cat is not here anymore!" << endl;
        }

    } // 離開作用域,P1 指標連同其指向的 Cat 物件都被 delete

    cout << "\n--- Outside the inner scope ---" << endl;
    
    // 觀察此時的 weak_ptr 是否仍有指向任何物件
    if (auto temp_shared = W1.lock()) { // Cat 物件被 delete、W1 指向空物件,W1.lock() 只會回傳 nullptr
        temp_shared->meow();
    } else {
        // 因為 W1.lock() 回傳 nullptr,所以走這裡的 else 區塊
        cout << "The cat has been taken away by its owner!" << endl;
    }

    return 0;
}

至於為何要有 weak_ptr?主要是為了避免「循環引用(circular reference)」,也就是兩個物件互指,導致兩物件都在等對方先死、count 都沒辦法降成 0

三種智慧型指標整理

  • unique_ptr:獨佔物件所有權。
    • 不能跟其他智慧指標共享相同物件。
    • 只能用 std::move() 轉讓所有權。
  • shared_ptr:可以跟其他智慧指標共享相同物件所有權。
    • .use_count() 算有幾個智慧指標指向該物件。
  • weak_ptr:「觀察」而不「擁有」與 shared_ptr 相同之物件。
    • 不影響 .use_count() 數量。
    • 要用 .lock() 暫時升級成 shared_ptr 才可取用物件的值跟方法。

參考資料

  1. Smart Pointers in C++ (GeeksforGeeks)
  2. Smart Pointer in C++ (EJ Chow)

上一篇
Day 20 - 用 C++ 學 OOP
系列文
韌體工程師的不只 0x10 個問題21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言