C++ std::shared_ptr 用法與範例

本篇 ShengYu 將介紹 C++ 的 std::shared_ptr 用法,std::shared_ptr 是可以讓多個 std::shared_ptr 共享一份記憶體,並且在最後一個 std::shared_ptr 生命週期結束時時自動釋放記憶體,本篇一開始會先介紹原始指標與智慧型指標寫法上的差異,再來介紹如何開始使用智慧型指標,並提供一些範例參考。

需要引入的標頭檔<memory>,編譯需要支援 C++11

範例1. 原始指標宣告與智慧型指標宣告的比較

我們先來看看原始指標是怎麼寫跟怎麼用,如 UseRawPointer() 內容所示,
另外有的對應的智慧型指標的版本,如 UseSmartPointer() 內容所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void UseRawPointer() {
// 使用原始指標
Song* pSong = new Song("Just The Way You Are", "Bruno Mars");

// Use pSong...
pSong->DoSomething();

// 別忘了要 delete!
delete pSong;
}

void UseSmartPointer() {
// 使用智慧型指標
shared_ptr<Song> song2(new Song("Just The Way You Are", "Bruno Mars"));

// Use song2...
song2->DoSomething();

} // song2 在這裡自動地被 deleted

範例2. 開始使用智慧型指標

以下為 C++ shared_ptr 幾種初始化寫法,盡可能地使用 make_shared 而不是用 new,範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 盡可能地使用 make_shared
auto sp1 = make_shared<Song>("The Beatles", "Hey Jude");
// 或者不使用 auto 像這樣寫
shared_ptr<Song> sp1 = make_shared<Song>("The Beatles", "Hey Jude");

// 下面這樣的寫法也可以,但會有一些副作用,記憶體配置上不連續,以及分開記憶體配置效能相對低弱
// Note: Using new expression as constructor argument
// creates no named variable for other code to access.
shared_ptr<Song> sp2(new Song("Lady Gaga", "Poker Face"));

// When initialization must be separate from declaration, e.g. class members,
// initialize with nullptr to make your programming intent explicit.
shared_ptr<Song> sp5(nullptr);
//等於: shared_ptr<Song> sp5;
sp5 = make_shared<Song>("Avril Lavigne", "What The Hell");

在 Scott Meyers 大神的《Effective Modern C++》書裡的條款 21 也提到:「盡量用 std::make_shared 取代直接使用 new」

範例3. 手動釋放記憶體

std::shared_ptr 如果需要手動釋放記憶體的話,可以使用智慧型指標裡的 reset() 函式,這邊要注意的是使用”“來存取 std::shared_ptr.reset(),範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void SmartPointerDemo2() {
// Create the object and pass it to a smart pointer
std::shared_ptr<LargeObject> pLarge(new LargeObject());

// Call a method on the object
pLarge->DoSomething();

// 在離開函式前手動釋放記憶體
pLarge.reset();

// ...

}

重複釋放記憶體問題

不建議使用下列這種寫法,用原始指標變數(下例中的p)去建立 shared_ptr 這種寫法容易造成重複釋放記憶體問題,因為 new Object 完後有個原始指標變數 p 指向這個物件,容易造成其它地方再把 p 建立 shared_ptr,
之後 sp 釋放完該物件後(delete),sp2 又再次釋放完該物件,造成重複釋放記憶體,

1
2
3
4
5
6
{
Object *p = new Object();
std::shared_ptr<Object> sp(p);
// ...
std::shared_ptr<Object> sp2(p);
} // sp 釋放,表示 delete 該物件,再換 sp2 釋放,造成釋放重複記憶體

應該改成下列這種寫法,直接在 shared_ptr 建構子裡 new Object,而避免用原始指標變數去建立 shared_ptr,

1
2
3
{
std::shared_ptr<Object> sp(new Object());
} // sp 釋放

範例4. 存取 std::shared_ptr 的原始指標

透過std::shared_ptr.get()可以取得原始指標,大概有兩種情況會使用到,一種是需要呼叫傳統的api,會需要傳遞原始指標,另一種是直接用原始指標,範例如下:

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
// g++ std-shared-ptr.cpp -o a.out -std=c++11
#include <iostream>
#include <string>
#include <memory>

class LargeObject {
public:
void DoSomething() {
printf("DoSomething\n");
}
};

void LegacyLargeObjectFunction(LargeObject *lo) {
printf("LegacyLargeObjectFunction\n");
lo->DoSomething();
}

int main() {
printf("===1===\n");
// Create the object and pass it to a smart pointer
std::shared_ptr<LargeObject> pLarge(new LargeObject());

printf("===2===\n");
// Call a method on the object
pLarge->DoSomething();

printf("===3===\n");
// 傳遞原始指標給 legacy API
LegacyLargeObjectFunction(pLarge.get());

printf("===4===\n");
// 用原始指標去接
LargeObject *p = pLarge.get();
LegacyLargeObjectFunction(p);

printf("===5===\n");
return 0;
}

輸出如下

1
2
3
4
5
6
7
8
9
10
===1===
===2===
DoSomething
===3===
LegacyLargeObjectFunction
DoSomething
===4===
LegacyLargeObjectFunction
DoSomething
===5===

ShengYu 探索實驗1. 生命週期

看看生命週期以及印出這兩種的記憶體位置

std-shared-ptr-exp1.cpp
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
// g++ std-shared-ptr-exp1.cpp -o a.out -std=c++11
#include <iostream>
#include <string>
#include <memory>

class LargeObject {
public:
LargeObject() {
printf("LargeObject::LargeObject()\n");
}

~LargeObject() {
printf("LargeObject::~LargeObject()\n");
}

void DoSomething() {
printf("DoSomething, x=%d\n", x);
}

int x = 10;
};

int main() {
printf("===1===\n");
LargeObject *p = new LargeObject();
p->DoSomething();
printf("%p\n", p);

printf("===2===\n");
std::shared_ptr<LargeObject> pLarge(p);
pLarge->DoSomething();
printf("%p\n", pLarge.get());
printf("%p\n", p);

printf("===3===\n");

//delete p; // double free or corruption

printf("===4===\n");
return 0;
}

輸出如下

1
2
3
4
5
6
7
8
9
10
11
===1===
LargeObject::LargeObject()
DoSomething, x=10
0x1524030
===2===
DoSomething, x=10
0x1524030
0x1524030
===3===
===4===
LargeObject::~LargeObject()

範例5. 判斷兩個 shared_ptr 是不是都是指到同一個物件

1
2
3
4
5
6
7
auto song1 = new Song("Village People", "YMCA");
auto song2 = new Song("Village People", "YMCA");
shared_ptr<Song> p1(song1);
shared_ptr<Song> p2(song2);
shared_ptr<Song> p3(p2);
cout << "p1 == p2 = " << std::boolalpha << (p1 == p2) << endl;
cout << "p3 == p2 = " << std::boolalpha << (p3 == p2) << endl;

unique_ptr 轉 shared_ptr

shared_ptr 轉換成 unique_ptr 是不允許的,但是可以反過來 unique_ptr 轉成 shared_ptr,
unique_ptr 轉 shared_ptr 有兩種方式,一種是

1
shared_ptr<Point> p = make_unique<Point>();

另一種是使用 move,

1
2
unique_ptr<Point> p1 = make_unique<Point>();
shared_ptr<Point> p2 = move(p1);

下一篇將會介紹 std::unique_ptr 用法

參考
[1] std::shared_ptr - cppreference.com
https://en.cppreference.com/w/cpp/memory/shared_ptr
[2] How to: Create and Use shared_ptr instances
https://docs.microsoft.com/zh-tw/cpp/cpp/how-to-create-and-use-shared-ptr-instances?view=vs-2019
圖表一與圖表二把 “參考計數器 ref count” 解釋地很好
[3] 智慧型指標 (新式 C++)
https://docs.microsoft.com/zh-tw/cpp/cpp/smart-pointers-modern-cpp?view=vs-2019

其它相關文章推薦
C/C++ 新手入門教學懶人包
std::unique_ptr 用法與範例
std::thread 用法與範例
std::deque 用法與範例
std::find 用法與範例
std::mutex 用法與範例
std::unordered_map 用法與範例
std::sort 用法與範例
std::random_shuffle 產生不重複的隨機亂數
std::async 用法與範例