C++ 智能指针完全指南(三):weak_ptr 与循环引用

引言

上一篇我们学习了 shared_ptr------通过引用计数实现共享所有权。但引用计数有一个致命缺陷:循环引用

两个对象互相持有对方的 shared_ptr,引用计数永远无法归零------即使你已经无法访问它们,它们仍然互相"拽着"对方,谁也释放不了。这就是内存泄漏

weak_ptr 就是为解决这个问题而生的。它指向 shared_ptr 管理的对象,但不增加引用计数 。当最后一个 shared_ptr 销毁时,对象照常释放,weak_ptr 会自动变为空。

第一部分:循环引用的灾难

一、一个内存泄漏的实例

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

class B;  // 前向声明

class A {
public:
    shared_ptr<B> ptrB;
    ~A() { cout << "A 析构" << endl; }
};

class B {
public:
    shared_ptr<A> ptrA;  // ← 如果是 shared_ptr,循环引用!
    ~B() { cout << "B 析构" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();
    
    a->ptrB = b;   // a 持有 b
    b->ptrA = a;   // b 持有 a → 循环!
    
    // a 和 b 离开作用域
    // 但它们的引用计数各为 1(对方持有的)
    // → 永远不会析构 → 内存泄漏!
}

运行结果 :什么也不输出。A 和 B 的析构函数永远不会被调用

二、循环引用的形成过程

第二部分:weak_ptr 的原理

一、weak_ptr 是什么

weak_ptr 是一种不控制对象生命周期 的智能指针。它指向 shared_ptr 管理的对象,但:

  • 不增加引用计数

  • 不影响对象的释放时机

  • 当对象被释放后,weak_ptr 自动变为空(不会悬空)

二、基本操作

cpp 复制代码
auto sp = make_shared<int>(42);
weak_ptr<int> wp = sp;  // 从 shared_ptr 创建

// 检查对象是否还存在
if (!wp.expired()) {
    cout << "对象还活着" << endl;
}

// 获取 shared_ptr(锁定)
if (auto locked = wp.lock()) {
    // locked 是一个 shared_ptr,引用计数 +1
    cout << *locked << endl;
}  // locked 离开作用域,引用计数 -1

sp.reset();  // 释放对象

if (wp.expired()) {
    cout << "对象已被释放" << endl;
}

// 对象已释放,lock() 返回空 shared_ptr
auto locked = wp.lock();
if (!locked) {
    cout << "lock() 返回空" << endl;
}
操作 含义
wp.expired() 检查对象是否已被释放
wp.lock() 返回一个 shared_ptr(如果对象还活着)
wp.use_count() 返回 shared_ptr 的引用计数
wp.reset() 清空 weak_ptr

三、为什么 lock() 是原子操作

cpp 复制代码
// ❌ 不是线程安全的
if (!wp.expired()) {            // 步骤1:检查
    auto sp = wp.lock();        // 步骤2:锁定
    // 步骤1 和 步骤2 之间,对象可能被另一个线程释放!
}

// ✅ lock() 是原子操作
if (auto sp = wp.lock()) {      // 一步完成
    // 安全使用 sp
}

第三部分:解决循环引用

cpp 复制代码
class B;

class A {
public:
    shared_ptr<B> ptrB;        // A 持有 B(强引用)
    ~A() { cout << "A 析构" << endl; }
};

class B {
public:
    weak_ptr<A> ptrA;          // B 持有 A(弱引用!不增加计数)
    ~B() { cout << "B 析构" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();
    
    a->ptrB = b;   // a 持有 b → B 引用计数 = 2
    b->ptrA = a;   // b 持有 a → A 引用计数 = 1(weak_ptr 不增加!)
    
    // main 结束,a 和 b 销毁:
    // a 销毁 → A 引用计数 = 0 → A 析构
    //    → A 析构后,A::ptrB 销毁 → B 引用计数 -1
    // b 销毁 → B 引用计数 = 0 → B 析构 ✅
}

运行结果

A 析构

B 析构

为什么能解决?

第四部分:weak_ptr 的使用场景

一、观察者模式

cpp 复制代码
class Subject {
private:
    vector<weak_ptr<Observer>> observers;  // 弱引用观察者
public:
    void addObserver(shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    
    void notify() {
        for (auto& weak : observers) {
            if (auto obs = weak.lock()) {  // 观察者还活着
                obs->update();
            } else {
                // 观察者已销毁,可以清理 weak_ptr
            }
        }
    }
};
// 观察者释放后,Subject 不会阻止其销毁

二、缓存系统

cpp 复制代码
class Cache {
private:
    map<int, weak_ptr<Data>> cache;
public:
    shared_ptr<Data> get(int key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            if (auto data = it->second.lock()) {
                return data;  // 缓存命中
            }
            // 数据已被外部释放,清理缓存
            cache.erase(it);
        }
        
        auto data = make_shared<Data>(key);
        cache[key] = data;
        return data;
    }
};
// 缓存持有 weak_ptr,外部用完了数据自然释放

三、打破 shared_ptr 循环引用

第五部分:三种智能指针总结

对比 unique_ptr shared_ptr weak_ptr
所有权 独占 共享 不拥有(弱引用)
拷贝 ✅(计数+1) ✅(不增加计数)
移动
大小 8 字节 16 字节 16 字节
释放 自动 计数归零自动 不影响释放
循环引用 不存在此问题 会内存泄漏 解决循环引用
创建 make_unique make_shared 从 shared_ptr 创建

使用原则

总结

一、核心要点

要点 内容
本质 弱引用,不增加引用计数,不控制对象生命周期
核心操作 lock() 获取 shared_ptr(原子操作)、expired() 检查对象是否存活
主要用途 打破 shared_ptr 循环引用、实现观察者模式、缓存系统
创建 shared_ptr 创建:weak_ptr<T> wp = sp;

二、一句话记忆

weak_ptrshared_ptr 的弱引用搭档,指向对象但不增加引用计数。用 lock() 原子地获取一个 shared_ptr 来安全访问对象。主要用来打破循环引用------父子关系中父用 shared_ptr、子用 weak_ptr

相关推荐
fox_lht1 小时前
第十五章 函数式语言:迭代器和闭包
开发语言·后端·学习·算法·rust
Web极客码1 小时前
如何通过 Python + LLM 用最少的 Token 完成精准推荐任务
开发语言·人工智能·python·ai
BestOrNothing_20151 小时前
ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程
c++·ros2·subscriber·publisher·msg·topic通信·自定义接口
TPBoreas1 小时前
AQS 是啥
java·开发语言
小短腿的代码世界1 小时前
Qt绘图引擎QPainter渲染管线:从光栅化到GPU加速的完整架构——为什么你的2D绘制慢了10倍?
开发语言·qt·架构
-森屿安年-1 小时前
91. 解码方法
c++·动态规划
一晌小贪欢1 小时前
第26节:自动化办公——利用 Python 自动生成动态分析报告 (PPT/PDF)
开发语言·python·数据分析·自动化·powerpoint·pandas·数据可视化
有点。1 小时前
C++(二分答案)
c++
程序喵大人2 小时前
【C++并发系列】第一章:多线程读写同一个变量为什么会出错
开发语言·c++·多线程·并发