一、回顾循环引用问题
当两个对象通过 shared_ptr 相互引用时,会产生循环引用问题,导致内存泄漏。因为这两个对象的引用计数永远不会变为 0,即使它们在程序的其他部分已经不被使用了。
典型循环引用:
#include <memory>
#include <iostream>
using namespace std;
class B; // 前置声明
class A {
public:
shared_ptr<B> b_ptr;
~A() { cout << "A destroyed" << endl; }
};
class B {
public:
shared_ptr<A> a_ptr;
~B() { cout << "B destroyed" << endl; }
};
void test() {
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 的关键在于所有权关系的分析。
二、所有权模型与指针选择
2.1 核心原则:所有权决定指针类型
所有权关系指一个对象对另一个对象生命周期的控制权。所有权持有者使用 shared_ptr,非所有者使用 weak_ptr。
示例:父子节点模型
class Child;
class Parent {
public:
vector<shared_ptr<Child>> children;
void addChild(shared_ptr<Child> child) {
children.push_back(child);
}
~Parent() { cout << "Parent destroyed" << endl; }
};
class Child {
public:
weak_ptr<Parent> parent; // 非拥有性引用
explicit Child(shared_ptr<Parent> p) : parent(p) {}
~Child() { cout << "Child destroyed" << endl; }
};
void demo() {
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.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; }
};
void buildList() {
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 仅作反向引用。
三、复杂场景的决策策略
3.1 多所有者场景
若多个对象共享某资源,需由顶层管理者持有 shared_ptr,其余使用 weak_ptr。
示例:缓存系统设计
#include <unordered_map>
class CacheManager;
class Resource {
public:
weak_ptr<CacheManager> manager; // 弱引用管理器
};
class CacheManager : public enable_shared_from_this<CacheManager> {
public:
void addResource(int id) {
resources[id] = make_shared<Resource>();
resources[id]->manager = shared_from_this(); // 关键行
}
};
说明:CacheManager 拥有所有 Resource,Resource 通过 weak_ptr 反向引用管理器。
3.2 无明确所有权场景
若对象间无明确从属关系,需重新审视设计或使用双向 weak_ptr。
示例:聊天室和用户
#include <memory>
#include <vector>
#include <iostream>
using namespace std;
class ChatRoom;
class User {
public:
string name;
vector<weak_ptr<ChatRoom>> rooms; // 弱引用聊天室
};
class ChatRoom {
public:
string name;
vector<weak_ptr<User>> users; // 弱引用用户
};
int main() {
auto alice = make_shared<User>("Alice");
auto general = make_shared<ChatRoom>("General");
return 0;
}
说明:用户(User)可以加入多个聊天室,聊天室(ChatRoom)包含多个用户,但是他们互相并没有所有权关系,所以使用双向 weak_ptr,用户和聊天室的生命周期由外部系统管理!
四、实践中的注意事项
4.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;
}
}
4.2 循环引用的检测工具
- Valgrind、AddressSanitizer 等工具可辅助检测内存泄漏。
- 静态代码分析器(如 Clang-Tidy)可识别潜在循环引用。
五、总结
| 场景特征 | 推荐策略 | 示例 |
|---|---|---|
| 明确单向所有权(如父子节点) | 所有者用 shared_ptr | Parent-Child 模型 |
| 双向依赖但需单向控制 | 主方向用 shared_ptr | 双向链表 |
| 多对象共享资源 | 顶层管理者用 shared_ptr,其余用 weak_ptr | 缓存系统 |
| 无明确所有权 | 重新设计或双向 weak_ptr | 聊天室和用户 |
核心准则:通过分析对象生命周期控制权,确定 shared_ptr 和 weak_ptr 的使用。始终确保至少有一条所有权路径不形成闭环。谁管理生命周期,谁用 shared_ptr。谁仅需引用对方,谁用 weak_ptr。