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

wufei123 发布于 2025-09-11 阅读(2)
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。它们就像两个互相抱紧溺水的人,谁也无法放手,最终一同沉没,导致内存泄漏。

解决之道,就是引入

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
被销毁或重新赋值时,强引用计数减少。只有当强引用计数降到零时,被管理的对象才会被销毁。

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

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
被销毁。 PIA PIA

全面的AI聚合平台,一站式访问所有顶级AI模型

PIA226 查看详情 PIA
  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与循环依赖问题解决方法的详细内容,更多请关注知识资源分享宝库其它相关文章!

相关标签: node ai c++ ios 解决方法 作用域 标准库 red if 成员函数 析构函数 auto 局部变量 循环 Lambda 指针 接口 operator function 对象 作用域 事件 this 大家都在看: C++井字棋AI实现 简单决策算法编写 如何为C++搭建边缘AI训练环境 TensorFlow分布式训练配置 怎样用C++开发井字棋AI 简单决策算法实现方案 怎样为C++配置嵌入式AI开发环境 TensorFlow Lite Micro移植指南 C++井字棋游戏怎么开发 二维数组与简单AI逻辑实现

标签:  解决方法 依赖 循环 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。