首页 > 后端开发 > C++ > 正文

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

P粉602998670
发布: 2025-09-11 13:12:01
原创
269人浏览过
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 <iostream>
#include <memory>
#include <string>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> 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) {
        b_ptr = b;
    }
};

class B {
public:
    // 循环依赖问题:这里如果也是 shared_ptr<A> a_ptr; 就会形成循环
    // 解决方案:使用 weak_ptr<A>
    std::weak_ptr<A> 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) {
        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> a = std::make_shared<A>("Alpha");
    std::shared_ptr<B> b = std::make_shared<B>("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>
登录后复制
,而
b
登录后复制
持有的是
weak_ptr<A>
登录后复制
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
登录后复制
被销毁或重新赋值时,强引用计数减少。只有当强引用计数降到零时,被管理的对象才会被销毁。

AI建筑知识问答
AI建筑知识问答

用人工智能ChatGPT帮你解答所有建筑问题

AI建筑知识问答 22
查看详情 AI建筑知识问答

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

class Parent;
class Child;

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

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

void create_problem() {
    std::shared_ptr<Parent> p = std::make_shared<Parent>();
    std::shared_ptr<Child> c = std::make_shared<Child>();

    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<A>
      登录后复制
      lock()
      登录后复制
      结果,或者干脆只存储一个不捕获A的
      this
      登录后复制
      的普通函数指针。

总的来说,

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

以上就是C++shared_ptr与循环依赖问题解决方法的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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