C++ 面試題:循環(huán)引用兩個(gè)節(jié)點(diǎn)相互引用,如何判斷哪個(gè)用 shared_ptr?哪個(gè)用 weak_ptr?
一、回顧循環(huán)引用問題
當(dāng)兩個(gè)對象通過 shared_ptr 相互引用時(shí),會(huì)產(chǎn)生循環(huán)引用問題,導(dǎo)致內(nèi)存泄漏。因?yàn)檫@兩個(gè)對象的引用計(jì)數(shù)永遠(yuǎn)不會(huì)變?yōu)?0,即使它們在程序的其他部分已經(jīng)不被使用了。
典型循環(huán)引用:
#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結(jié)束時(shí),a和b的引用計(jì)數(shù)均為1,對象未銷毀
解決方案是打破這個(gè)循環(huán),通常是讓其中一個(gè)對象使用 weak_ptr 指向另一個(gè)對象,而另一個(gè)對象使用 shared_ptr。這里決定使用 shared_ptr 還是 weak_ptr 的關(guān)鍵在于所有權(quán)關(guān)系的分析。
二、所有權(quán)模型與指針選擇
1. 核心原則:所有權(quán)決定指針類型
所有權(quán)關(guān)系指一個(gè)對象對另一個(gè)對象生命周期的控制權(quán)。所有權(quán)持有者使用shared_ptr,非所有者使用weak_ptr。
示例:父子節(jié)點(diǎn)模型
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結(jié)束時(shí),parent引用計(jì)數(shù)歸零,觸發(fā)Child析構(gòu)
輸出結(jié)果:
Parent destroyed
Child destroyed
關(guān)鍵點(diǎn):Parent 擁有 Child,Child 僅引用 Parent。
2. 雙向鏈表的設(shè)計(jì)取舍
雙向鏈表中,節(jié)點(diǎn)間常需雙向引用。為避免循環(huán)引用,需明確主從關(guān)系。
示例:鏈表節(jié)點(diǎn)設(shè)計(jì)
class Node {
public:
shared_ptr<Node> next; // 擁有下一個(gè)節(jié)點(diǎn)
weak_ptr<Node> prev; // 非擁有前驅(qū)節(jié)點(diǎn)
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結(jié)束時(shí),node1和node2的引用計(jì)數(shù)歸零
輸出結(jié)果:
Node 1 destroyed
Node 2 destroyed
設(shè)計(jì)邏輯:鏈表的構(gòu)建通常從前向后遍歷,故next持有所有權(quán),prev僅作反向引用。
三、復(fù)雜場景的決策策略
1. 多所有者場景
若多個(gè)對象共享某資源,需由頂層管理者持有shared_ptr,其余使用weak_ptr。
示例:緩存系統(tǒng)設(shè)計(jì)
#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(); // 關(guān)鍵行
}
};
說明:CacheManager擁有所有Resource,Resource通過weak_ptr反向引用管理器。
2. 無明確所有權(quán)場景
若對象間無明確從屬關(guān)系,需重新審視設(shè)計(jì)或使用雙向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) 可以加入多個(gè)聊天室,聊天室(ChatRoom) 包含多個(gè)用戶,但是他們互相并沒有所有權(quán)關(guān)系,所以使用雙向weak_ptr,用戶和聊天室的生命周期由外部系統(tǒng)管理!
四、實(shí)踐中的注意事項(xiàng)
1. weak_ptr 的安全訪問
使用weak_ptr時(shí)需通過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. 循環(huán)引用的檢測工具
Valgrind、AddressSanitizer 等工具可輔助檢測內(nèi)存泄漏。
靜態(tài)代碼分析器(如 Clang-Tidy)可識(shí)別潛在循環(huán)引用。
五、總結(jié)
場景特征 | 推薦策略 | 示例 |
明確單向所有權(quán)(如父子節(jié)點(diǎn)) | 所有者用 | Parent-Child 模型 |
雙向依賴但需單向控制 | 主方向用 | 雙向鏈表 |
多對象共享資源 | 頂層管理用 | 緩存系統(tǒng) |
無明確所有權(quán) | 重新設(shè)計(jì)或雙向 | 聊天室和用戶 |
核心準(zhǔn)則:通過分析對象生命周期控制權(quán),確定shared_ptr和weak_ptr的使用。始終確保至少有一條所有權(quán)路徑不形成閉環(huán)。
- 誰管理生命周期,誰用 shared_ptr。
- 誰僅需引用對方,誰用 weak_ptr。