C++ 面試題:循環引用兩個節點相互引用,如何判斷哪個用 shared_ptr?哪個用 weak_ptr?
一、回顧循環引用問題
當兩個對象通過 shared_ptr 相互引用時,會產生循環引用問題,導致內存泄漏。因為這兩個對象的引用計數永遠不會變為 0,即使它們在程序的其他部分已經不被使用了。
典型循環引用:
#include <memory>
#include <iostream>
usingnamespace std;
classB; // 前置聲明
classA {
public:
shared_ptr<B> b_ptr;
~A() { cout << "A destroyed" << endl; }
};
classB {
public:
shared_ptr<A> a_ptr;
~B() { cout << "B destroyed" << endl; }
};
voidtest(){
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
}
// test結束時,a和b的引用計數均為1,對象未銷毀
解決方案是打破這個循環,通常是讓其中一個對象使用 weak_ptr 指向另一個對象,而另一個對象使用 shared_ptr。這里決定使用 shared_ptr 還是 weak_ptr 的關鍵在于所有權關系的分析。
二、所有權模型與指針選擇
1. 核心原則:所有權決定指針類型
所有權關系指一個對象對另一個對象生命周期的控制權。所有權持有者使用shared_ptr,非所有者使用weak_ptr。
示例:父子節點模型
class Child;
classParent {
public:
vector<shared_ptr<Child>> children;
voidaddChild(shared_ptr<Child> child){
children.push_back(child);
}
~Parent() { cout << "Parent destroyed" << endl; }
};
classChild {
public:
weak_ptr<Parent> parent; // 非擁有性引用
explicitChild(shared_ptr<Parent> p) : parent(p) {}
~Child() { cout << "Child destroyed" << endl; }
};
voiddemo(){
auto parent = make_shared<Parent>();
auto child = make_shared<Child>(parent);
parent->addChild(child);
}
// demo結束時,parent引用計數歸零,觸發Child析構
輸出結果:
Parent destroyed
Child destroyed
關鍵點:Parent 擁有 Child,Child 僅引用 Parent。
2. 雙向鏈表的設計取舍
雙向鏈表中,節點間常需雙向引用。為避免循環引用,需明確主從關系。
示例:鏈表節點設計
class Node {
public:
shared_ptr<Node> next; // 擁有下一個節點
weak_ptr<Node> prev; // 非擁有前驅節點
int data;
Node(int val) : data(val) {}
~Node() { cout << "Node " << data << " destroyed" << endl; }
};
voidbuildList(){
auto node1 = make_shared<Node>(1);
auto node2 = make_shared<Node>(2);
node1->next = node2;
node2->prev = node1;
}
// buildList結束時,node1和node2的引用計數歸零
輸出結果:
Node 1 destroyed
Node 2 destroyed
設計邏輯:鏈表的構建通常從前向后遍歷,故next持有所有權,prev僅作反向引用。
三、復雜場景的決策策略
1. 多所有者場景
若多個對象共享某資源,需由頂層管理者持有shared_ptr,其余使用weak_ptr。
示例:緩存系統設計
#include <unordered_map>
classCacheManager;
classResource {
public:
weak_ptr<CacheManager> manager; // 弱引用管理器
};
classCacheManager : public enable_shared_from_this<CacheManager> {
public:
voidaddResource(int id){
resources[id] = make_shared<Resource>();
resources[id]->manager = shared_from_this(); // 關鍵行
}
};
說明:CacheManager擁有所有Resource,Resource通過weak_ptr反向引用管理器。
2. 無明確所有權場景
若對象間無明確從屬關系,需重新審視設計或使用雙向weak_ptr。
示例:聊天室和用戶
#include <memory>
#include <vector>
#include <iostream>
usingnamespace std;
classChatRoom;
classUser {
public:
string name;
vector<weak_ptr<ChatRoom>> rooms; // 弱引用聊天室
};
classChatRoom {
public:
string name;
vector<weak_ptr<User>> users; // 弱引用用戶
};
intmain(){
auto alice = make_shared<User>("Alice");
auto general = make_shared<ChatRoom>("General");
return0;
}
說明:用戶(User) 可以加入多個聊天室,聊天室(ChatRoom) 包含多個用戶,但是他們互相并沒有所有權關系,所以使用雙向weak_ptr,用戶和聊天室的生命周期由外部系統管理!
四、實踐中的注意事項
1. weak_ptr 的安全訪問
使用weak_ptr時需通過lock()獲取shared_ptr,并檢查有效性。
void accessParent(shared_ptr<Child> child) {
if (auto parent = child->parent.lock()) {
cout << "Parent is alive: " << parent << endl;
} else {
cout << "Parent has been destroyed" << endl;
}
}
2. 循環引用的檢測工具
Valgrind、AddressSanitizer 等工具可輔助檢測內存泄漏。
靜態代碼分析器(如 Clang-Tidy)可識別潛在循環引用。
五、總結
場景特征 | 推薦策略 | 示例 |
明確單向所有權(如父子節點) | 所有者用 | Parent-Child 模型 |
雙向依賴但需單向控制 | 主方向用 | 雙向鏈表 |
多對象共享資源 | 頂層管理用 | 緩存系統 |
無明確所有權 | 重新設計或雙向 | 聊天室和用戶 |
核心準則:通過分析對象生命周期控制權,確定shared_ptr和weak_ptr的使用。始終確保至少有一條所有權路徑不形成閉環。
- 誰管理生命周期,誰用 shared_ptr。
- 誰僅需引用對方,誰用 weak_ptr。