C++ 記憶體共分為三區:
1. 靜態(static):儲存「全域變數」與「靜態變數」,會存活至整段程式結束為止。
2. 堆疊(stack):儲存「區域變數」與「區塊變數」,離開如函數的作用域後就消失,如 int* ptr = new int; 的 ptr。
3. 堆積(heap):儲存人為手動以 new、delete 配置記憶體空間的變數,如 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_ptr、shared_ptr、weak_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_ptrshared_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 才可取用物件的值跟方法。