跳至主要内容

[C++] 智慧指標 (Smart Pointer)

本文會講解 C++ 中智慧指標 (smart pointer) 的基本概念。

RAII

在進入智慧指標之前,需要先了解錯誤的指標使用方式,舉例如下 :

#include <iostream>

int main() {
int *p = new int(5);

int a = 5;
if (a == 5) {
throw 0;
}

// do something
delete p;
return 0;
}

在上面的範例中,我們可以發現當 a == 5 時,會拋出例外,但是在拋出例外之前,我們沒有釋放 p 所指向的記憶體而造成記憶體洩漏 (memory leak)。

因此在 C++ 中,最好的做法並非是手動在例外情況中釋放記憶體,而是在 constructor 中分配記憶體,在 destructor 中釋放記憶體,也就是所謂的 RAII (Resource Acquisition Is Initialization)。

C++11 引入了四種智慧指標來解決這種問題,分別是 :

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr
  • std::auto_ptr(在 C++17 中移除)

std::unique_ptr

std::unique_ptr 是一個獨占所有權的智慧指標,禁止其他智慧指標指向同一個記憶體位置。

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};

int main() {
std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
std::unique_ptr<Resource> res2{}; // Start as nullptr

std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

// res2 = res1; // Won't compile: copy assignment is disabled
res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

std::cout << "Ownership transferred\n";

std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

return 0;
} // Resource destroyed here when res2 goes out of scope

而在 C++14 中,我們可以使用 std::make_unique 來建立 std::unique_ptr

std::unique_ptr<int> t = std::make_unique<int>(5);

也可以使用 t.get() 來取得指向的記憶體位置。

std::shared_ptr

std::shared_ptr 是一個共享所有權的智慧指標,它可以記錄有多少個 std::shared_ptr 指向同一個記憶體位置,從而避免顯性的調用 delete

shared_ptr 內部會維護兩個指標,一個指向記憶體位置,一個指向 control block,control block 會記錄有多少個 shared_ptr 指向同一個記憶體位置。 當使用 shared_ptr 時,兩塊記憶體會單獨分配,而使用 make_shared 時,兩塊記憶體會一起分配並獲得更好的效能。

在使用時,可以使用 new 來建立 std::shared_ptr,但非必要的話請使用 std::make_shared 來建立 (由於使用 new 時會獨立分配 control block,所以可能導致過早刪除)。

#include <iostream>
#include <memory> // for std::shared_ptr

class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};

int main() {
// allocate a Resource object and have it owned by std::shared_ptr
Resource* res { new Resource };
std::shared_ptr<Resource> ptr1{ res };
{
std::shared_ptr<Resource> ptr2 { ptr1 }; // make another std::shared_ptr pointing to the same thing

std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, but nothing happens

std::cout << "Killing another shared pointer\n";

return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed
std::shared_ptr<int> t = std::make_shared<int>(5);

std::shared_ptr 中,我們可以使用 get() 來取得原始的指標,使用 use_count() 來取得有多少個 std::shared_ptr 指向同一個記憶體位置,使用 reset() 來將計數減一。

std::shared_ptr<int> t = std::make_shared<int>(5);
std::shared_ptr<int> t2 = t;
std::cout << t.use_count() << std::endl; // 2
t2.reset();
std::cout << t.use_count() << std::endl; // 1

std::weak_ptr

在前面我們已經提到過關於 std::shared_ptr 的使用方式,但如果我們仔細觀察,就會發現這種方式依然有問題,舉例如下 :

struct A;
struct B;

struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A destroyed" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B destroyed" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}

上面程式碼的結果就是 a 跟 b 都不會被釋放,因為 a 跟 b 互相指向對方,造成循環引用,這時候就可以使用 std::weak_ptr 來解決這個問題。

使用 std::weak_ptr 並不會讓計數增加,而是扮演一個 觀察者 的角色,在上面的問題中,可以將其中一個 std::shared_ptr 改為 std::weak_ptr 就可以避免循環引用。

struct A;
struct B;

struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A destroyed" << std::endl;
}
};
struct B {
std::weak_ptr<A> pointer;
~B() {
std::cout << "B destroyed" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}

weak_ptr 還有一個缺點是它沒有 *-> 運算子,因此沒辦法直接對資源進行操作,必須使用 lock() 來取得 std::shared_ptr

std::shared_ptr<int> t = std::make_shared<int>(5);
std::weak_ptr<int> w = t;
std::shared_ptr<int> t2 = w.lock();

也可以使用 expired() 來判斷 std::weak_ptr 是否已經失效。

std::shared_ptr<int> t = std::make_shared<int>(5);
std::weak_ptr<int> w = t;
std::cout << w.expired() << std::endl; // 0
t.reset();
std::cout << w.expired() << std::endl; // 1

參考資料