导读:本期聚焦于小伙伴创作的《C++中weak_ptr是用来解决什么问题的?如何用它消除shared_ptr循环引用》,敬请观看详情。当一个对象通过shared_ptr被两个互为引用的实例各自持有时,引用计数永远无法归零,内存便泄漏了。weak_ptr专门打破这种强引用环:它指向被管理对象却不增加控制块中的共享计数,只提供临时提升为shared_ptr的观测能力。实际编码里,父子节点、观察者列表、缓存回指等结构最容易踩坑。正确使用weak_ptr的lock方法,可以在对象存活时安全访问,销毁后自动失效,既保留关联又释放生命周期捆绑。理解控制块与引用计数差异,是写好无泄漏C++智能指针代码的关键。

在C++现代内存管理中,shared_ptr通过引用计数自动释放堆对象,但两个shared_ptr互相指向会形成闭环,导致计数永远不小于一,析构时内存无法回收。weak_ptr正是为标准库设计的弱引用智能指针,它绑定到shared_ptr管理的对象却不参与所有权,从而专门化解这类循环引用问题。

C++中weak_ptr是用来解决什么问题的?如何用它消除shared_ptr循环引用

一、shared_ptr循环引用是怎么发生的

假设我们有一个双向链表节点,或者一个包含父指针和子指针的对象树。若父节点用shared_ptr保存子节点,子节点又用shared_ptr保存父节点,便构成了典型的循环引用。下面代码演示了这种错误结构:

#include <memory>
#include <iostream>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyedn"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyedn"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b; // A持有B
    b->a_ptr = a; // B持有A
    // 离开作用域时,a和b的引用计数均为2,互相等待对方释放
    return 0;
}

运行上面程序,终端不会打印任何析构信息。这是因为栈上的a和b离开main时,各自计数从二减到一,堆里的A和B仍被彼此的shared_ptr引用,控制块无法归零,操作系统最终回收进程空间才顺便清掉,属于明确的内存泄漏。

这种问题在观察者模式里也很常见:主题用shared_ptr保存观察者,观察者为了回调用shared_ptr保存主题,结果双方都死不掉。单纯增加代码复杂度去手动打破关系容易出错,所以标准库给出了weak_ptr作为语言级方案。

二、weak_ptr的基本能力与底层原理

weak_ptr必须从一个shared_ptr或另一个weak_ptr构造,它本身不控制对象生命周期,只观测。标准库把引用计数拆成两部分:共享计数(strong count)和弱计数(weak count)。shared_ptr拷贝影响强计数,weak_ptr拷贝只影响弱计数。当强计数归零,对象被销毁;只有当强弱计数都为零,控制块才释放。

#include <memory>
#include <iostream>

int main() {
    auto sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp; // 不增加强计数
    std::cout << "use_count=" << sp.use_count() << "n"; // 输出1
    if (auto locked = wp.lock()) { // 尝试提升
        std::cout << "value=" << *locked << "n";
    }
    sp.reset(); // 强计数归零,对象销毁
    if (wp.expired()) {
        std::cout << "object gonen";
    }
    return 0;
}

上例中wp.lock()在对象存活时返回非空shared_ptr,用完即释放,不影响原生命周期;sp.reset()后wp.expired()立刻为真。这样我们既保留了“知道对方在哪”的能力,又避免把对方拴死。

从实现看,weak_ptr内部存的是控制块指针和原始指针的观测副本。它的大小通常和shared_ptr一样为两个机器字,不会带来额外堆分配。使用weak_ptr不会阻止对象析构,非常适合缓存、回边指针、调试追踪等非拥有关系。

三、用weak_ptr改写循环引用代码

回到第一节的A/B例子,只需把其中一侧改为weak_ptr即可打破环。通常规则是:从属方持有拥有方的弱引用,拥有方持有从属方的强引用。下面把B中的a_ptr改成weak_ptr:

#include <memory>
#include <iostream>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyedn"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为弱引用
    ~B() { std::cout << "B destroyedn"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
    // 离开作用域:a强计数1,b强计数1
    // a先析构,b强计数变0,B销毁;B析构后a强计数变0,A销毁
    return 0;
}

程序现在会正常打印A destroyed与B destroyed。因为B不直接拥有A,A析构时b里的weak_ptr自动失效,不会阻止A释放;A释放后b失去外部引用也释放,整个环自然解开。

实际工程中,如果双向关系频繁互相调用,建议在访问弱引用前都用lock()取临时shared_ptr,防止正在用的时候对方被别的线程释放。相比原始指针,weak_ptr提供了安全的存活判定,是C++多线程共享对象设计的常用手法。

四、常见误用与注意点

有人以为weak_ptr可以完全替代原始指针做所有非拥有引用,其实不然。weak_ptr每次访问需lock,有微小开销;且它只能从shared_ptr体系来,不能指向栈对象。若对象本就用unique_ptr管理,就没必要套weak_ptr。

#include <memory>

struct Node {
    std::weak_ptr<Node> parent; // 正确:配合shared_ptr树使用
    // std::weak_ptr<Node> sibling; // 若树用unique_ptr拥有子节点则不应如此写
};

void bad_example() {
    int x = 10;
    // std::weak_ptr<int> wp(&x); // 编译错误:不能从裸指针构造
}

另一个坑是长期持有lock()出来的shared_ptr。若函数在拿到locked后把它存进全局容器,就等于偷偷恢复了强引用,可能重新引入泄漏。weak_ptr的价值在于“临时、按需、可失效”,把它当永久句柄用就失去了设计初衷。

在调试阶段,可以打印use_count和weak_ptr.expired()来确认环是否真的断开。现代Sanitizer工具也能捕获shared_ptr泄漏,但理解原理才能写出根本不漏的代码。

五、总结

weak_ptr解决的是shared_ptr所有权模型里的固有缺陷:强引用环导致计数死锁。它用弱计数观测对象,不延长生命周期,通过lock方法提供受控的临时访问。在父子结构、观察者、缓存回指等场景,把其中一侧改为weak_ptr即可消除循环引用。掌握控制块中强弱计数的区别,才能在现代C++里既享受自动内存管理,又避开隐藏的资源泄漏。

weak_ptrshared_ptr循环引用修改时间:2026-08-07 19:42:34

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。