在 C++ 中,智慧指標是現代記憶體管理的重要工具,尤其是在複雜的多執行緒環境中,能顯著減少記憶體洩漏和懸空指標等問題。std::shared_ptr
是 C++11 引入的一種共享智慧指標,透過引用計數機制管理物件的生命週期。本文將詳細介紹 std::shared_ptr
的基本用法、迴圈引用問題、執行緒安全性及其侷限性。
1. 什麼是 std::shared_ptr
std::shared_ptr
是 C++ 標準庫中的一種智慧指標,允許多個指標共享管理同一個物件的生命週期。它透過引用計數(reference count)來記錄有多少個指標指向同一個物件,當引用計數為零時,std::shared_ptr
會自動釋放物件,避免手動管理記憶體帶來的風險。
#include <iostream>
#include <memory>
void example() {
std::shared_ptr<int> p1 = std::make_shared<int>(10); // p1 引用計數為 1
std::shared_ptr<int> p2 = p1; // p1 和 p2 都指向同一個 int,引用計數為 2
std::cout << *p1 << std::endl; // 輸出 10
p2.reset(); // p2 被重置,引用計數減少為 1
} // 作用域結束,p1 被銷燬,引用計數為 0,物件被釋放
在上面的例子中,std::shared_ptr
可以安全地管理記憶體的分配和釋放,保證了在作用域結束時物件被自動釋放。
2. std::shared_ptr
的優勢
使用 std::shared_ptr
帶來了以下主要優勢:
- 自動釋放:當最後一個
std::shared_ptr
離開作用域時,引用計數變為零,自動呼叫物件的解構函式,防止記憶體洩漏。 - 物件共享:多個
std::shared_ptr
可以指向同一物件,簡化了資源共享的實現。 - 異常安全:
std::shared_ptr
的引用計數會自動管理,不會因為函式異常退出而洩漏記憶體。
這些優勢使 std::shared_ptr
特別適合用於物件共享和複雜的生命週期管理。
3. 迴圈引用問題
儘管 std::shared_ptr
帶來了諸多便利,但它的引用計數機制也可能帶來迴圈引用問題。迴圈引用發生在兩個或多個物件相互引用對方的 std::shared_ptr
,導致引用計數永遠無法歸零,進而造成記憶體洩漏。
示例:迴圈引用問題
#include <iostream>
#include <memory>
class B; // 前向宣告
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
void example() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 離開作用域時,A 和 B 的解構函式不會被呼叫,造成記憶體洩漏
}
在上述程式碼中,A
和 B
互相持有 std::shared_ptr
,因此即使 example
結束,a
和 b
的引用計數也不會歸零,導致解構函式未被呼叫。為了解決迴圈引用問題,C++ 提供了 std::weak_ptr
。
4. 使用 std::weak_ptr
打破迴圈引用
std::weak_ptr
是一種弱引用,它不會影響 std::shared_ptr
的引用計數,因此可以避免迴圈引用問題。std::weak_ptr
的主要作用是打破迴圈引用,同時提供一種安全的方式來訪問 std::shared_ptr
所管理的物件。
示例:使用 std::weak_ptr
解決迴圈引用
#include <iostream>
#include <memory>
class B;
class A {
public:
std::weak_ptr<B> b_ptr; // 使用 weak_ptr 避免迴圈引用
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免迴圈引用
~B() { std::cout << "B destroyed\n"; }
void useA() {
if (auto shared_a = a_ptr.lock()) { // 使用 lock() 獲取 shared_ptr
std::cout << "Using A\n";
} else {
std::cout << "A 已被釋放,無法使用\n";
}
}
};
void example() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
b->useA(); // 輸出 "Using A"
}
在這個例子中,A
和 B
使用 std::weak_ptr
互相引用,這樣就不會增加引用計數,從而避免了迴圈引用的問題。std::weak_ptr
的 lock()
方法會嘗試返回一個有效的 std::shared_ptr
,如果物件已經被釋放,則返回空的 std::shared_ptr
,這樣可以安全地檢查物件是否有效。
5. std::shared_ptr
的執行緒安全性
std::shared_ptr
提供了基本的執行緒安全性,保證了引用計數的執行緒安全更新。這意味著多個執行緒可以安全地同時持有和複製同一個 std::shared_ptr
,引用計數的遞增和遞減操作會被正確地同步。
執行緒安全性帶來的好處:
- 引用計數執行緒安全:在多執行緒環境中,
std::shared_ptr
的引用計數更新是原子操作,無需額外的加鎖操作。 - 自動釋放的執行緒安全性:在最後一個
std::shared_ptr
離開作用域時,std::shared_ptr
會自動釋放物件,而這一過程在多執行緒中是安全的。
示例:多執行緒使用 std::shared_ptr
#include <iostream>
#include <memory>
#include <thread>
void thread_func(std::shared_ptr<int> ptr) {
std::cout << "Thread: " << *ptr << std::endl;
}
void example() {
auto shared_int = std::make_shared<int>(42);
std::thread t1(thread_func, shared_int);
std::thread t2(thread_func, shared_int);
t1.join();
t2.join();
} // 作用域結束,shared_int 被自動釋放
在這個例子中,shared_int
在兩個執行緒之間共享,std::shared_ptr
自動管理引用計數,並確保在多執行緒環境下引用計數的更新是安全的,避免了計數錯誤和資源釋放問題。
注意事項:雖然 std::shared_ptr
確保了引用計數的執行緒安全,但對物件本身的訪問並非執行緒安全。如果多個執行緒要修改 std::shared_ptr
指向的物件,仍然需要額外的同步措施(如使用 std::mutex
)來保證執行緒安全。
6. 多執行緒修改 std::shared_ptr
指向的物件
如果多個執行緒需要同時訪問並修改 std::shared_ptr
指向的物件,使用 std::mutex
可以保證執行緒安全。這裡提供一個示例展示如何使用 std::mutex
來保護對共享物件的訪問和修改。
示例:多執行緒修改 std::shared_ptr
指向的物件
在這個例子中,我們建立一個共享的計數器物件,多個執行緒將同時訪問並修改該計數器。在沒有 std::mutex
保護的情況下,計數器的值可能會因資料競爭而出現錯誤。透過在訪問和修改計數器的程式碼塊中新增互斥鎖,我們可以確保每個執行緒按順序訪問該資源,避免資料競爭。
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
#include <vector>
class Counter {
public:
int value;
Counter() : value(0) {}
void increment() {
++value;
}
int getValue() const {
return value;
}
};
void thread_func(std::shared_ptr<Counter> counter, std::mutex& mtx) {
for (int i = 0; i < 100; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 加鎖保護對 counter 的訪問
counter->increment();
}
}
int main() {
auto counter = std::make_shared<Counter>();
std::mutex mtx;
std::vector<std::thread> threads;
// 啟動10個執行緒,每個執行緒對 counter 執行 100 次 increment 操作
for (int i = 0; i < 10; ++i) {
threads.emplace_back(thread_func, counter, std::ref(mtx));
}
// 等待所有執行緒完成
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter->getValue() << std::endl; // 期望輸出 1000
return 0;
}
在這個例子中,Counter
類的物件由 std::shared_ptr
管理,並在多個執行緒中共享,在 thread_func
函式中,每次呼叫 counter->increment()
前,都用 std::lock_guard<std::mutex>
鎖定 mtx
,保證每次訪問 increment()
是原子操作,std::lock_guard
是 RAII
風格的鎖管理器,它會在程式碼塊結束時自動釋放鎖。啟動 10 個執行緒,每個執行緒對共享計數器執行 100 次增量操作。透過 std::mutex
,我們保證了計數器的修改是執行緒安全的。
程式輸出:Final counter value: 1000
在沒有互斥鎖的情況下,counter->increment()
在多個執行緒中可能會發生競爭,導致最終計數值低於預期的 1000。使用 std::mutex
來保護對共享資源的訪問,保證了執行緒安全,確保最終計數器值為 1000。