欢迎光临
我们一直在努力

C++ weak_ptr 详解:从原理到实战,彻底解决shared_ptr循环引用

        在C++智能指针体系中,shared_ptr 的出现让开发者摆脱了手动管理内存的繁琐,大幅降低了内存泄漏的风险,但它并非完美——循环引用问题会让其引用计数机制失效,进而导致内存泄漏。而 weak_ptr 作为C++11引入的“辅助型”智能指针,正是为解决这一痛点而生,同时承担着“安全观察”共享对象的角色。

        本文将从 weak_ptr 的核心概念、底层原理、常用接口、实战场景(重点解决循环引用)、避坑指南五个维度,结合完整代码示例,帮你彻底掌握 weak_ptr 的使用,适合C++入门到进阶开发者阅读,请和之前的shared_ptr搭配使用。

一、为什么需要weak_ptr?先看shared_ptr的“致命缺陷”

        在讲解 weak_ptr 之前,我们先搞清楚一个核心问题:为什么需要它?答案很简单——解决 shared_ptr 的循环引用问题。

        我们先看一个典型的循环引用场景(双向链表节点、父子对象等场景中极易出现):

#include <iostream>
#include <memory>
using namespace std;

// 双向链表节点结构
struct Node {
int data;
shared_ptr<Node> next; // 指向后一个节点的强引用
~Node() {
cout << "Node 析构,data: " << data << endl;
}
};

int main() {
auto node1 = make_shared<Node>();
auto node2 = make_shared<Node>();

node1->data = 1;
node2->data = 2;

// 循环引用:node1指向node2,node2指向node1
node1->next = node2;
node2->next = node1;

// 查看引用计数
cout << "node1引用计数: " << node1.use_count() << endl; // 输出2
cout << "node2引用计数: " << node2.use_count() << endl; // 输出2

return 0;
}

        运行结果发现:程序结束时,Node 的析构函数并未执行!

        原因分析:node1 和 node2 互相持有对方的 shared_ptr,导致两者的引用计数均为2。当main函数结束,局部变量 node1 和 node2 析构时,各自的引用计数减1(变为1),但始终无法减至0,因此节点对象无法被销毁,造成内存泄漏。

        除了双向链表,父子对象也是循环引用的高频场景

struct Child;
struct Parent
{
std::shared_ptr<Child>child;
Parent() { cout << "create Parent" << endl; }
~Parent() { cout << "destroy Parent" << endl; }
void hi()const { cout << "hello Parent" << endl; }
};
struct Child
{
std::shared_ptr<Parent>parent;
Child() { cout << "create Child" << endl; }
~Child() { cout << "destroy Child" << endl; }
};
int main()
{
std::shared_ptr<Parent>pa(new Parent());
std::shared_ptr<Child>pc(new Child());
pa->child = pc;
pc->parent = pa;
return 0;
}

        运行结果发现:程序结束时,Parent 和 Child 的析构函数均未执行!

        原因分析:父对象 pa 持有子对象 pc 的强引用,子对象 pc 同时持有父对象 pa 的强引用,形成循环引用。此时 pa 和 pc 的引用计数均为2,main函数结束后,局部变量析构使引用计数减至1,无法减为0,导致两个对象均无法销毁,同样造成内存泄漏。

        图解:

std::shared_ptr<Parent>pa(new Parent());
std::shared_ptr<Child>pc(new Child());

pa->child = pc;
pc->parent = pa;

        当主函数结束时,先析构pc,再析构pa

        这就是 shared_ptr 的致命缺陷——循环引用会打破其引用计数的回收逻辑。而 weak_ptr 的核心作用,就是打破这种循环,同时不影响共享对象的生命周期。

二、weak_ptr 核心概念:只“观察”,不“拥有”

        weak_ptr是一种“弱引用”智能指针,它的核心特性可以总结为一句话:不拥有对象的所有权,仅作为观察者,不影响对象的生命周期。

        具体来说,它有以下3个关键特点:

  • 不增加强引用计数:weak_ptr 指向由 shared_ptr 管理的对象,但它的构造、析构不会改变对象的强引用计数(shared_count),只会影响控制块中的弱引用计数(weak_count)。

  • 无法直接访问对象:weak_ptr 没有重载 operator* 和 operator->,无法直接解引用访问对象,必须先通过 lock() 方法“升级”为 shared_ptr,才能访问对象。

  • 能检测对象存活状态:通过 expired() 方法可以判断其指向的对象是否已被销毁(即强引用计数是否为0),避免悬空指针问题。

  •         简单类比:shared_ptr 是对象的“所有者”,多个所有者共同决定对象的生命周期;而weak_ptr 是对象的“旁观者”,只负责观察对象是否存活,不参与决定对象的生死。

    三、weak_ptr 底层原理:与shared_ptr共享控制块

            要真正理解 weak_ptr,必须搞懂它与 shared_ptr 共享的“控制块”(Control Block)结构——这是两者协同工作的核心。

    3.1 控制块的结构

            当我们用 make_shared 创建一个 shared_ptr 时,会同时分配两块内存:

  • 对象内存:存储实际的对象数据(如Node的data、next)。

  • 控制块内存:存储引用计数、删除器、分配器等元数据,其中包含两个关键计数:

    (1)强引用计数(shared_count):记录当前持有对象所有权的 shared_ptr 数量,决定对象是否被销毁(shared_count为0时,对象销毁)。

  •          (2)弱引用计数(weak_count):记录当前观察对象的 weak_ptr 数量,决定控制块是否被销毁(weak_count为0时,控制块销毁)。

            控制块的简化结构如下(参考MSVC源码逻辑):

    struct ControlBlock {
    std::atomic<size_t> shared_count; // 强引用计数
    std::atomic<size_t> weak_count; // 弱引用计数
    // 其他元数据:删除器、分配器等
    };

    3.2 weak_ptr 与 shared_ptr 的协同流程

            我们通过一段代码,拆解两者的协同过程,清晰看到引用计数的变化:

    #include <iostream>
    #include <memory>
    using namespace std;

    int main() {
    // 1. 创建shared_ptr,控制块初始化:shared_count=1,weak_count=1(控制块自身引用)
    auto sp = make_shared<int>(100);
    cout << "sp创建后:shared_count=" << sp.use_count() << endl; // 输出1

    // 2. 创建weak_ptr,指向sp管理的对象:weak_count+1(变为2),shared_count不变
    weak_ptr<int> wp = sp;
    cout << "wp创建后:shared_count=" << sp.use_count()
    << ", weak_count隐含值(可通过wp.use_count()间接查看)=" << wp.use_count() << endl; // shared_count=1,wp.use_count()返回1(即sp的shared_count)

    // 3. 销毁sp:shared_count-1(变为0),对象被销毁,但控制块保留(weak_count=1)
    sp.reset();
    cout << "sp重置后:wp是否过期?" << (wp.expired() ? "是" : "否") << endl; // 输出“是”

    // 4. 销毁wp:weak_count-1(变为0),控制块被释放
    wp.reset();

    return 0;
    }

    核心流程总结:

  • 对象销毁条件:强引用计数(shared_count)归0,无论弱引用计数是否为0。

  • 控制块销毁条件:强引用计数和弱引用计数均归0。

  • weak_ptr 的作用:通过观察控制块中的 shared_count,判断对象是否存活,自身不影响对象生命周期。

  •         补充:weak_ptr 的 use_count() 方法,返回的是其指向对象的强引用计数(即 shared_count),而非弱引用计数,该方法仅适合调试,不推荐用于生产环境的逻辑判断(效率较低)。

    四、weak_ptr 常用接口(实战必备)

            weak_ptr 的接口不多,但每一个都很实用,以下是最常用的5个接口,结合代码示例说明其用法(重点掌握 lock() 和 expired())。

    4.1 构造与赋值

            weak_ptr 无法直接构造(无默认构造以外的构造方式),必须通过 shared_ptr 或其他 weak_ptr 构造/赋值,示例:

    // 1. 空构造
    weak_ptr<int> wp1;

    // 2. 通过shared_ptr构造
    shared_ptr<int> sp = make_shared<int>(10);
    weak_ptr<int> wp2(sp);

    // 3. 通过其他weak_ptr构造
    weak_ptr<int> wp3(wp2);

    // 4. 赋值操作
    weak_ptr<int> wp4;
    wp4 = sp;
    wp4 = wp2;

    4.2 lock():核心接口,弱引用升级为强引用

            作用:尝试将 weak_ptr 升级为 shared_ptr。如果对象存活(shared_count > 0),返回一个有效的 shared_ptr(此时 shared_count 会加1);如果对象已销毁(expired() 为true),返回一个空的 shared_ptr。

            关键特性:lock() 是原子操作,线程安全,可避免“检查对象存活后,对象被其他线程销毁”的竞态条件,是访问对象的唯一安全方式。

    #include <iostream>
    #include <memory>
    using namespace std;

    int main() {
    shared_ptr<int> sp = make_shared<int>(100);
    weak_ptr<int> wp(sp);

    // 升级弱引用:对象存活,返回有效shared_ptr
    if (auto sp2 = wp.lock()) {
    cout << "对象值:" << *sp2 << endl; // 输出100
    cout << "升级后shared_count:" << sp2.use_count() << endl; // 输出2
    } else {
    cout << "对象已销毁" << endl;
    }

    // 销毁对象
    sp.reset();

    // 再次升级:对象已销毁,返回空shared_ptr
    if (auto sp3 = wp.lock()) {
    cout << "对象值:" << *sp3 << endl;
    } else {
    cout << "对象已销毁" << endl; // 输出此句
    }

    return 0;
    }

    4.3 expired():判断对象是否存活

            作用:判断 weak_ptr 指向的对象是否已被销毁,本质是判断 shared_count 是否为0,返回 bool 值(true 表示对象已销毁,false 表示对象存活)。

            注意:expired() 虽然比 use_count() 高效,但不建议单独使用(存在竞态条件),优先使用 lock() 进行判断+升级。

    // 不推荐写法(有竞态风险)
    if (!wp.expired()) {
    // 此时对象可能被其他线程销毁,wp.lock()返回空
    auto sp = wp.lock();
    }

    // 推荐写法(原子操作,无竞态)
    if (auto sp = wp.lock()) {
    // 安全访问对象
    }

    4.4 reset():清空弱引用

            作用:清空 weak_ptr,使其不再指向任何对象,同时将控制块的 weak_count 减1(如果weak_count减至0,且shared_count为0,控制块会被释放)。

    weak_ptr<int> wp(sp);
    wp.reset(); // 清空wp,不再指向任何对象
    cout << "wp是否过期?" << (wp.expired() ? "是" : "否") << endl; // 输出“是”

    4.5 swap():交换两个weak_ptr的指向

            作用:交换两个 weak_ptr 所指向的对象(实际是交换它们的控制块指针和对象指针),无异常抛出。

    shared_ptr<int> sp1 = make_shared<int>(10);
    shared_ptr<int> sp2 = make_shared<int>(20);

    weak_ptr<int> wp1(sp1);
    weak_ptr<int> wp2(sp2);

    wp1.swap(wp2); // 交换wp1和wp2的指向

    // 交换后,wp1指向sp2的对象,wp2指向sp1的对象
    if (auto sp = wp1.lock()) cout << *sp << endl; // 输出20
    if (auto sp = wp2.lock()) cout << *sp << endl; // 输出10

    五、实战场景:用weak_ptr解决循环引用

            回到本文开头的循环引用问题,我们用 weak_ptr 进行修改,看看如何打破循环,避免内存泄漏。

    5.1 修复双向链表节点的循环引用

    #include <iostream>
    #include <memory>
    using namespace std;

    struct Node {
    int data;
    weak_ptr<Node> next; // 关键:将shared_ptr改为weak_ptr
    ~Node() {
    cout << "Node 析构,data: " << data << endl;
    }
    };

    int main() {
    auto node1 = make_shared<Node>();
    auto node2 = make_shared<Node>();

    node1->data = 1;
    node2->data = 2;

    // 此时node1的next是weak_ptr,不会增加node2的shared_count
    node1->next = node2;
    // node2的next是weak_ptr,不会增加node1的shared_count
    node2->next = node1;

    // 查看引用计数:均为1(无循环引用)
    cout << "node1引用计数: " << node1.use_count() << endl; // 输出1
    cout << "node2引用计数: " << node2.use_count() << endl; // 输出1

    // 访问weak_ptr指向的对象(需lock()升级)
    if (auto next_node = node1->next.lock()) {
    cout << "node1的下一个节点data: " << next_node->data << endl; // 输出2
    }

    return 0;
    }

            运行结果:程序结束时,两个 Node 的析构函数均会执行,内存泄漏问题被完美解决!

            原因分析:将其中一方(或双方)的 shared_ptr 改为 weak_ptr 后,两者不再互相持有“强引用”,引用计数均为1。main函数结束时,局部变量 node1 和 node2 析构,引用计数减至0,节点对象被正常销毁。

    5.2 修复父子对象的循环引用

            针对前文提到的父子对象循环引用问题,我们同样可以通过 weak_ptr 打破循环,以下是完整的修复代码、运行结果及详细分析:

    struct Child;
    struct Parent
    {
    //std::shared_ptr<Child>child;
    std::weak_ptr<Child>child;
    Parent() { cout << "create Parent" << endl; }
    ~Parent() { cout << "destroy Parent" << endl; }
    void hi()const { cout << "hello Parent" << endl; }
    };
    struct Child
    {
    //std::shared_ptr<Parent>parent;
    std::weak_ptr<Parent>parent;
    Child() { cout << "create Child" << endl; }
    ~Child() { cout << "destroy Child" << endl; }
    };
    int main()
    {
    std::shared_ptr<Parent>pa(new Parent());
    std::shared_ptr<Child>pc(new Child());
    pa->child = pc;
    pc->parent = pa;
    pc->parent.lock()->hi();
    return 0;
    }

    运行结果

    create Parent

    create Child

    hello Parent

    destroy Child

    destroy Parent

    原因分析

            1. 修复核心:将 Parent 类中的 shared_ptr<Child> child 和 Child 类中的 shared_ptr<Parent> parent,均改为 weak_ptr,彻底打破了两者之间的强引用循环。

            2. 引用计数变化:

    • 创建 pa(shared_ptr<Parent>)时,Parent对象的强引用计数为1;创建pc(shared_ptr<Child>)时,Child对象的强引用计数为1。

    • 执行 pa->child = pc 和 pc->parent = pa 时,由于赋值的是 weak_ptr,不会增加Parent和Child对象的强引用计数,两者的强引用计数仍保持为1。

            3. 弱引用升级与对象访问:pc->parent.lock()->hi() 中,lock() 方法将 weak_ptr<Parent> 升级为 shared_ptr<Parent>(此时Parent对象的强引用计数临时变为2),升级成功后正常调用 hi() 方法,输出“hello Parent”;语句执行结束后,升级得到的临时shared_ptr析构,Parent对象的强引用计数恢复为1。

            4. 对象正常销毁:main函数结束时,局部变量pa 和 pc 析构,Parent和Child对象的强引用计数均减至0,因此两个对象会被正常销毁,依次输出“destroy Child”和“destroy Parent”,彻底解决了内存泄漏问题。

            图解分析:

            _Weaks++:

            析构时,_Uses–:

    5.3 其他常见实战场景

  • 父子对象场景:父对象持有子对象的 shared_ptr,子对象持有父对象的 weak_ptr,避免循环引用。

  • 缓存场景:用 weak_ptr 存储缓存对象,当缓存对象被其他地方释放时,缓存自动失效,无需手动清理,避免缓存悬空。

  • 观察者模式:观察者持有被观察者的 weak_ptr,当被观察者销毁时,观察者可通过 expired() 检测到,避免访问已销毁对象。

  • 六、weak_ptr 避坑指南(新手必看)

            虽然 weak_ptr 用法简单,但新手很容易踩坑,以下是4个高频坑点,务必注意:

    坑点1:直接解引用weak_ptr

            weak_ptr 没​​​​​​​有重载 operator* 和 operator->,直接解引用会编译报错,必须通过 lock() 升级为 shared_ptr 后再访问。

    weak_ptr<int> wp(sp);
    // cout << *wp << endl; // 错误:无法直接解引用
    // cout << wp->data << endl; // 错误:无法直接访问成员

    // 正确写法
    if (auto sp2 = wp.lock()) {
    cout << *sp2 << endl;
    }

    坑点2:用expired()判断后直接访问对象

            如前所述,expired() 和 lock() 之间存在竞态条件(多线程环境下,可能刚判断完expired()为false,对象就被其他线程销毁),优先使用 lock() 进行判断+升级。

    坑点3:误以为weak_ptr会影响对象生命周期

            记住:weak_ptr 不拥有对象所有权,无论有多少个 weak_ptr 指向对象,只要最后一个 shared_ptr 被销毁,对象就会被释放,weak_ptr 仅负责观察。

    坑点4:用weak_ptr作为函数参数传递

            如果函数需要访问对象,优先传递 shared_ptr(保证对象在函数执行期间存活)或裸指针;weak_ptr 适合作为类成员存储弱引用,不适合作为函数参数(频繁升级会增加开销)。

    七、总结:weak_ptr 的核心价值

    1. 核心定位:weak_ptr 不是独立的智能指针,而是 shared_ptr 的“辅助工具”,用于解决循环引用和安全观察对象。

    2. 核心特性:不拥有对象、不增加强引用计数、需升级为 shared_ptr 才能访问对象、线程安全的 lock() 接口。

    3. 核心场景:解决 shared_ptr 循环引用、缓存、观察者模式。

            掌握 weak_ptr,能让你在使用 shared_ptr 时更安全、更灵活,彻底摆脱循环引用导致的内存泄漏问题,写出更健壮的C++代码。

            最后,留一个小练习:尝试用 weak_ptr 实现一个简单的父子对象模型,避免循环引用,欢迎在评论区留下你的代码~

    创作不易,点赞+收藏,关注我,持续分享C++进阶干货!

    赞(0)
    未经允许不得转载:171主机测评 » C++ weak_ptr 详解:从原理到实战,彻底解决shared_ptr循环引用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址