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

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

引言

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

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

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

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

一、一个内存泄漏的实例

#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自动变为空(不会悬空)

二、基本操作

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() 是原子操作

// ❌ 不是线程安全的 if (!wp.expired()) { // 步骤1:检查 auto sp = wp.lock(); // 步骤2:锁定 // 步骤1 和 步骤2 之间,对象可能被另一个线程释放! } // ✅ lock() 是原子操作 if (auto sp = wp.lock()) { // 一步完成 // 安全使用 sp }

第三部分:解决循环引用

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 的使用场景

一、观察者模式

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 不会阻止其销毁

二、缓存系统

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_ptrshared_ptrweak_ptr
所有权独占共享不拥有(弱引用)
拷贝✅(计数+1)✅(不增加计数)
移动
大小8 字节16 字节16 字节
释放自动计数归零自动不影响释放
循环引用不存在此问题会内存泄漏解决循环引用
创建make_uniquemake_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