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_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_ptr是shared_ptr的弱引用搭档,指向对象但不增加引用计数。用lock()原子地获取一个shared_ptr来安全访问对象。主要用来打破循环引用——父子关系中父用shared_ptr、子用weak_ptr。
