循环引用:用 shared_ptr 还是 weak_ptr?

一、回顾循环引用问题

当两个对象通过 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。


   转载规则


《循环引用:用 shared_ptr 还是 weak_ptr?》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
一道经典 fork 面试题 一道经典 fork 面试题
这是一道关于 Unix fork() 系统调用的经典面试题:每一轮 fork 产生的新进程数量等于当前正在运行的进程数量。题目问——下面这个程序一共输出多少个 -? #include <stdio.h> #include <sy
2020-06-09
下一篇 
fork 的开销与争议 fork 的开销与争议
fork 是一个拥有 50 年历史的系统调用,也是一个传奇。一个程序员可以永远不用 read/write,也可以不懂 mmap,但必须懂 fork。fork 没有参数、如此简单,是 UNIX 哲学的布道者们的首选,被写进了几乎每一本操作系统
2020-05-28
  目录