C++ 中 weak ptr 详解:打破循环引用的观察者指针

发布时间:2026/7/25 17:00:05
C++ 中 weak ptr 详解:打破循环引用的观察者指针 C 中 weak_ptr 详解打破循环引用的观察者指针一、引言不参与所有权的智能指针std::weak_ptr是 C11 引入的智能指针它是std::shared_ptr的配套工具。与shared_ptr不同weak_ptr不参与对象的共享所有权——它持有一个对象的“弱引用”不会增加对象的引用计数不会阻止对象被销毁。weak_ptr的核心使命是打破shared_ptr之间的循环引用以及提供安全的临时访问——在需要时尝试提升为shared_ptr如果对象还存在就安全访问如果已经被销毁就安全失败。它就像一个可以“过期”的观察者。二、核心概念速览| 维度 | 说明 || --- | --- || 所有权 | 无所有权不参与引用计数 || 引用计数影响 | 不影响强引用计数只影响弱引用计数 || 能否直接访问对象 | 不能没有 operator* 和 operator- || 访问方式 | 先 lock() 提升为 shared_ptr检查是否为空 || 主要用途 | 打破循环引用、观察者模式、缓存系统 || 过期检测 | 当所有 shared_ptr 销毁后weak_ptr 自动“过期” || 创建方式 | 从 shared_ptr 或另一个 weak_ptr 构造 || 线程安全 | 与 shared_ptr 相同引用计数操作原子 |三、weak_ptr 的内部原理3.1 weak_ptr 与控制块的关系weak_ptr和shared_ptr共享同一个控制块(Control Block)。控制块中维护两个引用计数shared_count强引用计数shared_ptr的数量决定对象何时被销毁weak_count弱引用计数weak_ptr的数量加上shared_ptr的数量决定控制块何时被销毁图表代码下载全屏3.2 对象与控制块的销毁时机图表代码下载全屏关键规则对象在shared_count 0时销毁控制块在shared_count 0 weak_count 0时销毁weak_ptr不阻止对象销毁但可能延长控制块的生命周期3.3 weak_ptr 的简化实现cpp复制下载templatetypename T class weak_ptr { private: T* ptr_; // 指向被管理的对象可能过期 control_block* cb_; // 指向控制块 public: // 从 shared_ptr 构造 weak_ptr(const shared_ptrT sp) : ptr_(sp.ptr_), cb_(sp.cb_) { cb_-weak_count; // 增加弱引用计数 } // 拷贝构造 weak_ptr(const weak_ptr other) : ptr_(other.ptr_), cb_(other.cb_) { cb_-weak_count; } // 析构函数 ~weak_ptr() { --cb_-weak_count; // 如果 shared_count 和 weak_count 都归零释放控制块 if (cb_-shared_count 0 cb_-weak_count 0) { delete cb_; } } // 尝试提升为 shared_ptr shared_ptrT lock() const { // 原子地检查并增加 shared_count if (cb_-shared_count 0) { cb_-shared_count; // 原子操作 return shared_ptrT(ptr_, cb_); } return shared_ptrT(); // 对象已销毁返回空 shared_ptr } // 检查对象是否已被销毁 bool expired() const { return cb_-shared_count 0; } // 返回引用计数调试用 size_t use_count() const { return cb_-shared_count; } };四、weak_ptr 的核心操作4.1 基本使用cpp复制下载#include memory #include iostream int main() { // 创建 shared_ptr auto sp std::make_sharedint(42); std::cout use_count: sp.use_count() std::endl; // 1 // 从 shared_ptr 创建 weak_ptr std::weak_ptrint wp sp; std::cout use_count: sp.use_count() std::endl; // 1不变 std::cout expired: wp.expired() std::endl; // false // lock()尝试提升为 shared_ptr if (auto locked wp.lock()) { std::cout Value: *locked std::endl; // 42 std::cout use_count: sp.use_count() std::endl; // 2 } // locked 析构use_count 回到 1 // 重置 shared_ptr对象被销毁 sp.reset(); std::cout expired: wp.expired() std::endl; // true // lock() 返回空指针 if (auto locked wp.lock()) { // 不会执行到这里 } else { std::cout Object has been destroyed std::endl; } }4.2 weak_ptr 的典型使用模式cpp复制下载// 模式一安全访问检查是否过期 void safeAccess(std::weak_ptrint wp) { if (auto sp wp.lock()) { // 对象仍然存在安全使用 *sp 100; } else { // 对象已被销毁执行降级逻辑 std::cout Object no longer available std::endl; } } // 模式二过期检测 void checkExpired(std::weak_ptrint wp) { if (wp.expired()) { std::cout Expired std::endl; } else { // 注意这里对象可能即将被销毁 // 应该立即 lock() 而不是先检查再使用 } }五、weak_ptr 的主要应用场景5.1 场景一打破循环引用最常用这是weak_ptr存在的首要原因。当两个对象通过shared_ptr互相引用时会形成循环引用导致内存泄漏。cpp复制下载#include memory #include iostream #include string class Person { public: std::string name_; std::shared_ptrPerson bestFriend_; // 强引用 // 如果两个 Person 互相持有 shared_ptr → 循环引用 → 内存泄漏 Person(const std::string name) : name_(name) { std::cout name_ created std::endl; } ~Person() { std::cout name_ destroyed std::endl; } }; // 正确的设计一方使用 weak_ptr class PersonFixed { public: std::string name_; std::weak_ptrPersonFixed bestFriend_; // 弱引用不增加引用计数 PersonFixed(const std::string name) : name_(name) { std::cout name_ created std::endl; } ~PersonFixed() { std::cout name_ destroyed std::endl; } void greetFriend() { if (auto friendPtr bestFriend_.lock()) { std::cout name_ : Hello, friendPtr-name_ ! std::endl; } else { std::cout name_ : My friend is gone... std::endl; } } }; int main() { { auto alice std::make_sharedPersonFixed(Alice); auto bob std::make_sharedPersonFixed(Bob); alice-bestFriend_ bob; // Alice 弱引用 Bob bob-bestFriend_ alice; // Bob 弱引用 Alice alice-greetFriend(); } // alice 和 bob 正确销毁 // 输出: Alice destroyed, Bob destroyed }图表代码下载全屏5.2 场景二观察者模式cpp复制下载#include memory #include vector #include iostream #include algorithm // 主题Subject class Subject { public: void notifyObservers() { // 清理过期的观察者并通知有效的观察者 observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [](const std::weak_ptrObserver wp) { return wp.expired(); }), observers_.end() ); for (auto weak : observers_) { if (auto observer weak.lock()) { observer-onNotify(); } } } void addObserver(std::shared_ptrObserver observer) { observers_.push_back(observer); // 存储为 weak_ptr } private: std::vectorstd::weak_ptrObserver observers_; }; // 观察者Observer class Observer { public: virtual void onNotify() 0; virtual ~Observer() default; }; class ConcreteObserver : public Observer { std::string name_; public: ConcreteObserver(const std::string name) : name_(name) { } void onNotify() override { std::cout name_ received notification std::endl; } ~ConcreteObserver() { std::cout name_ destroyed std::endl; } }; int main() { Subject subject; { auto obs1 std::make_sharedConcreteObserver(Observer1); auto obs2 std::make_sharedConcreteObserver(Observer2); subject.addObserver(obs1); subject.addObserver(obs2); subject.notifyObservers(); // 两个观察者都收到通知 } // obs2 离开作用域被销毁 subject.notifyObservers(); // 只有 Observer1 收到通知 // Observer2 的 weak_ptr 已过期被自动清理 }5.3 场景三缓存系统cpp复制下载#include memory #include unordered_map #include string #include iostream #include algorithm class Image { public: std::string filename_; std::vectorchar data_; // 大量的图像数据 Image(const std::string filename) : filename_(filename) { std::cout Loading image: filename std::endl; data_.resize(10 * 1024 * 1024); // 模拟 10MB 图像 } ~Image() { std::cout Unloading image: filename_ std::endl; } }; class ImageCache { std::unordered_mapstd::string, std::weak_ptrImage cache_; public: // 获取图像如果缓存中有返回否则加载并缓存 std::shared_ptrImage getImage(const std::string filename) { auto it cache_.find(filename); // 检查缓存是否命中且未过期 if (it ! cache_.end()) { if (auto img it-second.lock()) { std::cout Cache hit: filename std::endl; return img; } // 已过期移除条目 cache_.erase(it); } // 缓存未命中加载新图像 auto img std::make_sharedImage(filename); cache_[filename] img; // 存储为 weak_ptr return img; } // 清理过期的缓存条目 void cleanExpired() { for (auto it cache_.begin(); it ! cache_.end(); ) { if (it-second.expired()) { it cache_.erase(it); } else { it; } } } size_t cacheSize() const { size_t count 0; for (const auto entry : cache_) { if (!entry.second.expired()) { count; } } return count; } }; int main() { ImageCache cache; { auto img1 cache.getImage(photo1.jpg); // 加载并缓存 auto img2 cache.getImage(photo2.jpg); // 加载并缓存 std::cout Active images in cache: cache.cacheSize() std::endl; // 2 auto img1Again cache.getImage(photo1.jpg); // 缓存命中 } // img1 和 img2 离开作用域引用计数归零 std::cout Active images in cache: cache.cacheSize() std::endl; // 0 // 缓存的 weak_ptr 已过期不会阻止 Image 对象被释放 // 应用程序可以主动释放不用的图像缓存自动感知过期 }5.4 场景四防止 this 循环引用cpp复制下载#include memory #include functional #include iostream class AsyncTask : public std::enable_shared_from_thisAsyncTask { public: void startAsync() { // 捕获 weak_ptr 而不是 shared_ptr避免循环引用 std::weak_ptrAsyncTask weakSelf shared_from_this(); // 模拟异步操作 scheduleCallback([weakSelf]() { if (auto self weakSelf.lock()) { self-onCompleted(); } else { std::cout Task was cancelled std::endl; } }); } void onCompleted() { std::cout Task completed std::endl; } private: void scheduleCallback(std::functionvoid() callback) { // 模拟异步调度 callback(); } }; int main() { auto task std::make_sharedAsyncTask(); task-startAsync(); // 如果 task 被提前释放回调能安全检测到 }六、weak_ptr 的注意事项6.1 lock() 和 expired() 的竞态条件cpp复制下载// ❌ 不安全的做法 void unsafeAccess(std::weak_ptrint wp) { if (!wp.expired()) { // 检查时未过期 auto sp wp.lock(); // 但这里可能已经过期了 // 多线程环境下expired() 和 lock() 之间对象可能被销毁 } } // ✓ 安全的做法直接 lock() void safeAccess(std::weak_ptrint wp) { if (auto sp wp.lock()) { // 原子地尝试提升 // sp 持有强引用保证对象在作用域内不被销毁 *sp 100; } }6.2 make_shared 与 weak_ptr 的内存释放cpp复制下载// make_shared 分配单块内存对象 控制块 auto sp std::make_sharedint(42); std::weak_ptrint wp sp; sp.reset(); // shared_count 0对象析构 // 但内存块没有被释放因为 weak_count 0wp 仍存在 // wp 存在期间整个内存块包括已析构对象占用的空间不会释放 // 直到 wp 也销毁weak_count 0内存块才被释放 // 如果对象很大且 weak_ptr 长期存在考虑用直接构造 std::shared_ptrint sp2(new int(42)); // 分开分配 std::weak_ptrint wp2 sp2; sp2.reset(); // 对象内存立即释放只有控制块保留七、总结std::weak_ptr是 C 智能指针体系中专门解决共享所有权边界问题的工具本质weak_ptr是shared_ptr的“弱观察者”——它引用控制块但不增加强引用计数不阻止对象销毁只延长控制块的生命周期。当shared_count 0时对象销毁当shared_count 0 weak_count 0时控制块销毁。访问方式weak_ptr不能直接解引用没有operator*和operator-必须通过lock()提升为shared_ptr才能访问对象。lock()返回空指针表示对象已被销毁。主要场景打破循环引用父子结构、双向关联中一方使用weak_ptr观察者模式Subject 持有 Observer 的weak_ptrObserver 可独立销毁缓存系统缓存持有weak_ptr应用层可主动释放不用的对象异步回调在回调中捕获weak_ptr安全检测对象是否仍存在注意事项不要先调用expired()再调用lock()直接lock()后判空make_shared的单块内存分配在weak_ptr存在时不会释放weak_ptr不是线程安全的银弹——它只保证引用计数操作的原子性weak_ptr的设计体现了 C 资源管理的精细控制——它不拥有资源但能安全地观察资源的生命周期。正确使用它可以优雅地解决共享所有权模型中的循环引用和对象生命周期不确定性这两大难题。