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

相关推荐
apocelipes1 天前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
郝学胜_神的一滴3 天前
CMake 034:生成器表达式:解耦构建时序、精简分支逻辑的终极利器
c++·cmake
见过夏天3 天前
C++ 基础入门完全指南
c++
用户805533698035 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
BadBadBad__AK5 天前
线段树维护区间 k 次方和
c++·数学·算法·stl
卷无止境6 天前
Eigen 库如何借助 OpenMP 加速计算
c++·后端
卷无止境6 天前
OpenMPI、MPICH 与 OpenMP:关系、核心概念与架构全解
c++·后端
郝学胜_神的一滴6 天前
CMake 30:循环语法全解|foreach_while双循环精讲、迭代技巧与实战避坑指南
c++·cmake
卷无止境8 天前
C++ 的Eigen 库全解析
c++
卷无止境8 天前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端