0

0

C++shared_ptr与循环依赖问题解决方法

P粉602998670

P粉602998670

发布时间:2025-09-11 13:12:01

|

282人浏览过

|

来源于php中文网

原创

shared_ptr循环依赖因相互强引用导致引用计数无法归零,造成内存泄漏;解决方法是使用weak_ptr打破循环,weak_ptr不增加引用计数,通过lock()安全访问对象,确保在无强引用时对象可被释放。

c++shared_ptr与循环依赖问题解决方法

C++中

shared_ptr
导致的循环依赖,本质上是对象间相互持有强引用,导致引用计数永远无法归零,从而造成内存泄漏。解决这个问题的核心方案是引入
weak_ptr
,它提供了一种非拥有性的引用,能够打破循环。

当我们谈论C++的智能指针,尤其是

shared_ptr
时,它无疑是管理动态内存的一把利器。它通过引用计数机制,确保对象在不再被任何
shared_ptr
引用时自动释放。然而,这套机制并非万无一失,它有一个著名的陷阱——循环依赖(或称循环引用)。说实话,我个人第一次遇到这个问题时,着实困惑了一阵子,代码逻辑看起来都没错,但内存就是不释放。

解决方案

shared_ptr
循环依赖的发生,通常是因为两个或多个对象通过
shared_ptr
相互持有对方的引用。想象一下A对象有一个
shared_ptr
指向B,同时B对象也有一个
shared_ptr
指向A。当外部对A和B的
shared_ptr
都失效后,A的引用计数因为B的存在而不会降到0,B的引用计数也因为A的存在而不会降到0。它们就像两个互相抱紧溺水的人,谁也无法放手,最终一同沉没,导致内存泄漏。

立即学习C++免费学习笔记(深入)”;

解决之道,就是引入

weak_ptr
weak_ptr
是一种“弱引用”智能指针,它不增加对象的引用计数。你可以把它理解为一种观察者,它能“看”到对象,但不会“拥有”对象。当所有
shared_ptr
都释放后,即便还有
weak_ptr
指向该对象,对象也会被正确销毁。
weak_ptr
的强大之处在于,它提供了一个
lock()
方法,可以尝试获取一个
shared_ptr
。如果对象仍然存在(即至少有一个
shared_ptr
还在引用它),
lock()
会返回一个有效的
shared_ptr
;否则,它会返回一个空的
shared_ptr

以下是一个经典的循环依赖示例及其

weak_ptr
解决方案:

#include 
#include 
#include 

class B; // 前向声明

class A {
public:
    std::shared_ptr b_ptr;
    std::string name;

    A(const std::string& n) : name(n) {
        std::cout << "A " << name << " constructed." << std::endl;
    }
    ~A() {
        std::cout << "A " << name << " destructed." << std::endl;
    }
    void set_b(std::shared_ptr b) {
        b_ptr = b;
    }
};

class B {
public:
    // 循环依赖问题:这里如果也是 shared_ptr a_ptr; 就会形成循环
    // 解决方案:使用 weak_ptr
    std::weak_ptr a_ptr; 
    std::string name;

    B(const std::string& n) : name(n) {
        std::cout << "B " << name << " constructed." << std::endl;
    }
    ~B() {
        std::cout << "B " << name << " destructed." << std::endl;
    }
    void set_a(std::shared_ptr a) {
        a_ptr = a;
    }
    void use_a() {
        if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr
            std::cout << "B " << name << " is using A " << sharedA->name << std::endl;
        } else {
            std::cout << "B " << name << ": A is no longer available." << std::endl;
        }
    }
};

void create_circular_dependency() {
    std::shared_ptr a = std::make_shared("Alpha");
    std::shared_ptr b = std::make_shared("Beta");

    // 建立相互引用
    a->set_b(b);
    b->set_a(a); // 这里B持有A的weak_ptr

    std::cout << "A's ref count: " << a.use_count() << std::endl; // 此时为1 (因为b_ptr持有)
    std::cout << "B's ref count: " << b.use_count() << std::endl; // 此时为1 (因为a_ptr持有)

    b->use_a(); // B可以安全地使用A
} // a 和 b 在这里离开作用域,shared_ptr 被销毁

int main() {
    create_circular_dependency();
    std::cout << "End of main function." << std::endl;
    // 如果没有使用 weak_ptr,A和B的析构函数将不会被调用,造成内存泄漏。
    // 使用 weak_ptr 后,A和B会正确析构。
    return 0;
}

运行上述代码,你会看到A和B的析构函数被正确调用,表明内存得到了释放。关键在于,当

create_circular_dependency
函数结束,
a
b
这两个
shared_ptr
离开
作用域时,它们所持有的对象的引用计数会减一。对于
a
对象,它的引用计数降为0(因为
b_ptr
持有的是
shared_ptr
,而
b
持有的是
weak_ptr
weak_ptr
不增加引用计数),
a
被销毁。
a
销毁后,其内部的
b_ptr
也会被销毁,导致
b
的引用计数降为0,
b
也被销毁。这样,循环就被完美打破了。

C++
shared_ptr
循环引用究竟是如何发生的?

要真正理解

weak_ptr
的巧妙,我们得先深挖一下
shared_ptr
循环引用的根源。这并不是
shared_ptr
设计上的缺陷,而是它“共享所有权”语义的自然结果。每个
shared_ptr
内部都维护着一个控制块(control block),这个控制块存储着两个计数器:一个是强引用计数(use_count),记录有多少个
shared_ptr
指向该对象;另一个是弱引用计数(weak_count),记录有多少个
weak_ptr
指向该对象。

当一个

shared_ptr
被创建或复制时,强引用计数增加。当
shared_ptr
被销毁或重新赋值时,强引用计数减少。只有当强引用计数降到零时,被管理的对象才会被销毁。

Jukedeck
Jukedeck

一个由人工智能驱动的音乐创作工具,允许用户为各种项目生成免版税的音乐。

下载

循环引用就发生在两个或多个对象彼此“强拥有”对方的时候。 举个例子:

class Parent;
class Child;

class Parent {
public:
    std::shared_ptr child;
    Parent() { std::cout << "Parent constructed." << std::endl; }
    ~Parent() { std::cout << "Parent destructed." << std::endl; }
};

class Child {
public:
    std::shared_ptr parent; // 问题所在:这里是 shared_ptr
    Child() { std::cout << "Child constructed." << std::endl; }
    ~Child() { std::cout << "Child destructed." << std::endl; }
};

void create_problem() {
    std::shared_ptr p = std::make_shared();
    std::shared_ptr c = std::make_shared();

    p->child = c; // Parent持有Child,Child的强引用计数变为2 (p->child 和 c)
    c->parent = p; // Child持有Parent,Parent的强引用计数变为2 (c->parent 和 p)

    std::cout << "Parent ref count: " << p.use_count() << std::endl; // 输出 2
    std::cout << "Child ref count: " << c.use_count() << std::endl;  // 输出 2
} // p 和 c 离开作用域

create_problem
函数执行完毕,局部变量
p
c
被销毁。

  1. p
    被销毁,
    Parent
    对象的强引用计数从2降到1(因为
    c->parent
    还在持有)。
  2. c
    被销毁,
    Child
    对象的强引用计数从2降到1(因为
    p->child
    还在持有)。 此时,
    Parent
    Child
    对象的强引用计数都为1,谁都无法降到0。这意味着它们所指向的内存永远不会被释放,即使它们已经无法从程序中被访问到,形成了内存泄漏。这就是
    shared_ptr
    循环引用的发生机制。它不是错误,而是
    shared_ptr
    强所有权语义在特定场景下的一个副作用。

weak_ptr
是如何解决循环依赖的,以及它有哪些使用上的注意事项?

weak_ptr
解决循环依赖的核心机制,在于它不参与对象的强引用计数。它仅仅是“观察”对象是否存在,而不会影响对象的生命周期。当一个
weak_ptr
被创建时,它只会增加对象的弱引用计数(weak_count),这个计数只用于判断控制块是否可以被销毁,而不是对象本身。只要对象的强引用计数不为零,它就不会被销毁。

使用

weak_ptr
时,最关键的一点是,你不能直接通过
weak_ptr
访问它所指向的对象。你需要先通过
weak_ptr::lock()
方法,尝试获取一个
shared_ptr

  • 如果对象仍然存在(即至少有一个
    shared_ptr
    在引用它),
    lock()
    会返回一个有效的
    shared_ptr
    。你可以像使用普通
    shared_ptr
    一样安全地访问对象。
  • 如果对象已经被销毁(所有
    shared_ptr
    都已释放),
    lock()
    会返回一个空的
    shared_ptr
    (即
    nullptr
    )。这时,你必须检查返回的
    shared_ptr
    是否为空,以避免访问已销毁的内存,这是一种非常重要的安全机制。

使用注意事项:

  1. 务必检查
    lock()
    的返回值:
    这是
    weak_ptr
    使用的黄金法则。
    weak_ptr
    所指向的对象随时可能被销毁,因此在使用前必须通过
    if (auto shared_obj = weak_ptr_instance.lock()) { ... }
    这样的结构来确保对象仍然有效。
  2. 选择正确的“弱”边: 在设计对象关系时,需要仔细考虑哪一方应该持有弱引用。通常,拥有者持有
    shared_ptr
    ,被拥有者或者观察者持有
    weak_ptr
    • 父子关系: 如果父对象拥有子对象,子对象需要访问父对象但不能影响父对象的生命周期,那么子对象应该持有父对象的
      weak_ptr
      。例如,一个
      Node
      持有其
      children
      shared_ptr
      ,而
      children
      则持有
      Parent
      weak_ptr
    • 观察者模式: 在观察者模式中,被观察者通常持有观察者的
      weak_ptr
      。这样,当观察者自身生命周期结束时,它就可以被安全销毁,而不会因为被观察者持有强引用而造成泄漏。
    • 缓存: 缓存系统有时会使用
      weak_ptr
      来引用缓存项。如果一个缓存项没有其他强引用,它就可以被垃圾回收,即使缓存本身还“记得”它。
  3. weak_ptr
    的开销:
    weak_ptr
    的创建、复制和销毁都会操作控制块,
    lock()
    方法也需要一定的开销。但这些开销通常很小,在大多数应用中可以忽略不计。过度担心性能而避免使用
    weak_ptr
    ,可能导致更严重的内存泄漏问题。
  4. weak_ptr
    不能直接解引用:
    记住,
    weak_ptr
    本身不提供
    operator*
    operator->
    。它只是一个句柄,必须先提升为
    shared_ptr
    才能使用。

除了
weak_ptr
,还有其他避免
shared_ptr
循环引用的策略吗?

虽然

weak_ptr
是解决
shared_ptr
循环依赖最标准、最推荐的方案,但在某些情况下,我们也可以从设计层面去规避这个问题。这往往需要我们重新审视对象间的关系和所有权语义。

  1. 重新设计所有权关系: 这是最根本的策略。很多时候,循环依赖的出现,可能暗示着对象模型本身存在一些不清晰或不合理之处。

    • 单向所有权: 问问自己,两个对象真的都需要“拥有”对方吗?是否可以将其中的一个关系改为单向引用?例如,A拥有B,B知道A的存在但并不拥有A(即B内部持有A的裸指针或
      weak_ptr
      )。
    • 明确的层次结构: 在树形或图状结构中,尽量建立明确的父子关系,让父节点拥有子节点,子节点通过
      weak_ptr
      或裸指针(在生命周期明确受控的情况下)引用父节点。
    • 引入中间管理者: 有时,可以将相互引用的两个对象A和B的共同管理职责抽离到一个第三者C。由C持有A和B的
      shared_ptr
      ,而A和B之间则只通过裸指针或
      weak_ptr
      进行通信。这样,C负责它们的生命周期,A和B则避免了直接的强引用循环。
  2. 使用裸指针(极度谨慎): 在某些非常特殊且生命周期严格受控的场景下,可以考虑使用裸指针来打破循环。但这种做法风险极高,因为它完全放弃了智能指针提供的安全性。你必须100%确定:

    • 被裸指针指向的对象在其生命周期内不会被提前销毁。
    • 裸指针绝不会被用于删除对象。
    • 裸指针的使用范围和时间都非常有限。 这种方法通常只适用于内部实现细节,且有明确的注释和文档说明。对于初学者或大多数应用场景,强烈不建议使用。
  3. 事件/回调机制: 当对象之间需要相互通信但又不想建立直接的强引用时,可以考虑事件或回调机制。

    • 例如,A需要知道B的状态变化,而不是直接持有B的
      shared_ptr
      。B可以提供一个注册回调的接口,A通过这个接口注册一个lambda函数或成员函数。当B状态变化时,它调用这些回调。这里的关键是,B在存储这些回调时,如果回调涉及到A的成员函数,B应该存储一个
      std::function
      ,并且这个
      std::function
      内部捕获的
      this
      指针应该是
      weak_ptr
      lock()
      结果,或者干脆只存储一个不捕获A的
      this
      的普通函数指针。

总的来说,

weak_ptr
是C++
标准库为解决
shared_ptr
循环引用提供的优雅且安全的方案。而其他策略更多的是从设计思想上进行规避,它们在某些特定场景下可能更合适,但通常也伴随着更高的设计复杂性或潜在的风险。在实际开发中,优先考虑
weak_ptr
,如果发现
weak_ptr
导致代码结构复杂或不自然,再回头审视对象间的关系是否可以简化。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

783

2023.08.22

lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

208

2023.09.15

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

191

2025.11.08

Python lambda详解
Python lambda详解

本专题整合了Python lambda函数相关教程,阅读下面的文章了解更多详细内容。

55

2026.01.05

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1179

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

215

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2105

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

24

2026.01.19

2026赚钱平台入口大全
2026赚钱平台入口大全

2026年最新赚钱平台入口汇总,涵盖任务众包、内容创作、电商运营、技能变现等多类正规渠道,助你轻松开启副业增收之路。阅读专题下面的文章了解更多详细内容。

54

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 6.8万人学习

前端基础到实战(HTML5+CSS3+ES6+NPM)
前端基础到实战(HTML5+CSS3+ES6+NPM)

共162课时 | 19.1万人学习

第二十二期_前端开发
第二十二期_前端开发

共119课时 | 12.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号