0

0

智能指针在观察者模式中的使用 weak_ptr解决监听器悬挂问题

P粉602998670

P粉602998670

发布时间:2025-07-25 08:20:02

|

292人浏览过

|

来源于php中文网

原创

1.原始指针会导致悬挂问题,shared_ptr会引发循环引用;2.weak_ptr通过lock()方法安全访问观察者并避免内存泄漏。在观察者模式中,使用原始指针会使主体持有悬挂指针,导致未定义行为;使用shared_ptr相互引用会造成循环依赖,无法释放内存。std::weak_ptr以非拥有方式引用观察者,在通知时尝试提升为shared_ptr,若失败则移除该观察者,从而安全处理生命周期问题并避免循环引用。

智能指针在观察者模式中的使用 weak_ptr解决监听器悬挂问题

在C++的观察者模式中,std::weak_ptr是解决监听器(Observer)悬挂问题和避免循环引用的关键。它允许主体(Subject)“观察”其监听器,而不会强行持有它们,确保当监听器自身生命周期结束时,不会导致主体持有一个无效的指针,也不会与监听器形成相互持有的循环引用,从而避免内存泄漏和程序崩溃。

智能指针在观察者模式中的使用 weak_ptr解决监听器悬挂问题

解决方案

观察者模式的核心在于主体维护一个其感兴趣的观察者列表,并在状态改变时通知它们。传统上,如果主体持有观察者的原始指针,一旦观察者被销毁,主体持有的指针就变成了悬挂指针(dangling pointer),后续的通知操作将导致未定义行为甚至崩溃。而如果主体和观察者都使用std::shared_ptr相互持有,则会形成循环引用,导致两者都无法被正确释放,造成内存泄漏。

std::weak_ptr提供了一种非拥有(non-owning)的引用方式。主体可以将观察者的std::shared_ptr转换为std::weak_ptr并存储起来。当需要通知观察者时,主体会尝试将std::weak_ptr提升(lock)为一个std::shared_ptr。如果提升成功,说明观察者仍然存活,可以安全地进行通知;如果提升失败(返回一个空的std::shared_ptr),则说明观察者已经被销毁,主体可以将其从列表中移除。这种机制巧妙地解决了生命周期管理的问题,既保证了安全性,又避免了循环引用。

智能指针在观察者模式中的使用 weak_ptr解决监听器悬挂问题

为什么传统的指针或shared_ptr在观察者模式中会带来问题?

在我看来,选择合适的指针类型,尤其是在像观察者模式这样涉及多对象协作的场景,是C++内存管理中的一个核心考量。我刚开始接触C++智能指针时,也曾纠结于何时用哪种。shared_ptr的自动管理魅力十足,但在循环引用这里,它就像一个美丽的陷阱。

首先是原始指针(raw pointer)。它最直接,也最危险。当主体存储观察者的原始指针时,一旦观察者对象被析构,主体持有的那个地址就成了“悬挂的幽灵”。任何试图通过这个指针进行的操作都可能导致程序崩溃,或者更糟糕的是,静默地破坏数据,这在调试时简直是噩梦。想象一下,一个UI组件作为观察者,在用户关闭它后被销毁了,但后台的数据模型还在尝试向它发送更新通知,那场景简直无法想象。

智能指针在观察者模式中的使用 weak_ptr解决监听器悬挂问题

然后是std::shared_ptr。它解决了所有权问题,多个shared_ptr可以共享同一个对象的生命周期。这听起来很棒,但如果观察者和主体之间形成了相互的shared_ptr引用,比如主体持有shared_ptr,而观察者又持有shared_ptr(常见于观察者需要调用主体方法进行反注册或查询),那么它们就会形成一个循环。在这种情况下,即使外部已经没有其他shared_ptr指向它们,它们的引用计数也永远不会降到零,导致它们永远不会被析构,最终造成内存泄漏。这就像两个固执的人,谁也不肯先放手,结果一起被困住了。

weak_ptr在观察者模式中的具体实现机制是怎样的?

weak_ptr在观察者模式中,扮演的是一个“不干涉”的观察者角色。它本身不拥有对象,也不会增加对象的引用计数。它存在的意义,就是为了能够安全地“窥探”一个shared_ptr所管理的对象是否还活着。

其核心机制在于,当一个shared_ptr被创建时,除了管理实际的对象内存,还会有一个共享的控制块(control block)。这个控制块里维护着两个计数器:一个是强引用计数(strong count),由shared_ptr维护;另一个是弱引用计数(weak count),由weak_ptr维护。

当主体将一个shared_ptr转换为weak_ptr并存储时,它只增加了弱引用计数。这意味着,即使所有的shared_ptr都释放了对观察者的引用,导致强引用计数归零,观察者对象被销毁,这个weak_ptr仍然可以存在。

关键在于weak_ptr::lock()方法。当主体需要通知观察者时,它会遍历存储的weak_ptr列表,对每一个weak_ptr调用lock()

CodeSquire
CodeSquire

AI代码编写助手,把你的想法变成代码

下载
  • 如果lock()成功返回一个有效的shared_ptr(即强引用计数大于0),那么说明被观察的观察者对象仍然存活,主体就可以安全地通过这个shared_ptr调用观察者的更新方法。
  • 如果lock()返回一个空的shared_ptr,则表明被观察的观察者对象已经不存在了(它已经被销毁,因为所有shared_ptr都已释放),此时主体就知道这个weak_ptr已经失效,可以将其从观察者列表中移除。

这个lock()方法简直是神来之笔,它优雅地解决了“对象还在不在”这个世纪难题。

概念性代码示例如下:

#include 
#include 
#include 
#include  // For std::remove_if

// 观察者接口
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void update() = 0;
};

// 具体观察者
class ConcreteObserver : public IObserver, public std::enable_shared_from_this {
private:
    int id_;
public:
    ConcreteObserver(int id) : id_(id) {
        std::cout << "Observer " << id_ << " created." << std::endl;
    }
    ~ConcreteObserver() {
        std::cout << "Observer " << id_ << " destroyed." << std::endl;
    }
    void update() override {
        std::cout << "Observer " << id_ << " received update." << std::endl;
    }
};

// 主体
class Subject {
private:
    std::vector> observers_; // 存储weak_ptr
public:
    void addObserver(std::shared_ptr observer) {
        observers_.push_back(observer);
        std::cout << "Subject added an observer." << std::endl;
    }

    void notify() {
        std::cout << "Subject notifying observers..." << std::endl;
        // 使用临时vector来避免在迭代时修改原vector导致迭代器失效
        std::vector> active_observers;
        for (auto& wptr : observers_) {
            if (auto sptr = wptr.lock()) { // 尝试提升为shared_ptr
                sptr->update(); // 如果成功,说明观察者还在,进行通知
                active_observers.push_back(wptr); // 保留活跃的观察者
            }
            // else: wptr.lock()返回空,说明观察者已销毁,自动从列表中移除
        }
        observers_ = active_observers; // 更新观察者列表
        std::cout << "Notification complete. Active observers count: " << observers_.size() << std::endl;
    }
};

// 实际使用
// int main() {
//     Subject s;
//     {
//         auto obs1 = std::make_shared(1);
//         s.addObserver(obs1);
//         auto obs2 = std::make_shared(2);
//         s.addObserver(obs2);
//         
//         s.notify(); // obs1, obs2 都会收到通知
//     } // obs1, obs2 在这里超出作用域,被销毁
//     
//     std::cout << "--- After observers destroyed ---" << std::endl;
//     s.notify(); // 此时,s会发现obs1和obs2的weak_ptr已失效,并清理列表
//     return 0;
// }

使用weak_ptr解决监听器悬挂问题的最佳实践和注意事项有哪些?

我觉得,真正的工程实践,往往不是找到一个完美的银弹,而是权衡利弊,选择最适合当前场景的方案。weak_ptr在这里,就是那个“几乎”完美的答案,但它也有自己的使用之道。

  • 何时使用weak_ptr 只要你发现存在一个对象需要“知道”另一个对象,但又“不拥有”它,并且这两个对象的生命周期可能独立,或者存在潜在的循环引用风险时,weak_ptr就应该被考虑。在观察者模式中,主体对观察者就是这种关系。观察者通常由其他模块管理其生命周期,主体不应该因为持有观察者的引用而阻止其销毁。

  • 观察者列表的清理: 虽然weak_ptrlock()方法能判断对象是否存活,但失效的weak_ptr本身并不会自动从容器中移除。因此,在每次通知或周期性地,主体都需要遍历其观察者列表,移除那些已失效的weak_ptr。我上面给出的代码示例,就是在notify方法内部顺便做了清理,这是一个很常见的做法,效率也高。

  • 线程安全性: 如果你的观察者模式涉及多线程,比如一个线程添加观察者,另一个线程通知观察者,或者多个线程同时通知,那么访问observers_列表就需要同步机制(例如std::mutex)。weak_ptr::lock()本身是原子操作,但对std::vector的增删改查则不是。

  • 性能考量: 相比于原始指针的直接解引用,weak_ptr::lock()操作会引入轻微的开销,因为它需要访问控制块并原子地增加强引用计数。但在绝大多数应用场景中,这种开销是微不足道的,安全性带来的收益远超这一点点性能损耗。不要为了极小的性能提升而牺牲稳定性。

  • 显式反注册: 尽管weak_ptr能自动处理已销毁的观察者,但提供一个显式的removeObserver方法仍然是好的实践。这允许观察者在不再需要接收通知时主动解除注册,而不是等到被销毁后才被动地从列表中清理。比如,一个对话框关闭前,它可能希望立即停止接收所有通知,而不是等到下次通知时才被发现已失效。

  • enable_shared_from_this 如果你的观察者需要在其自己的成员函数中获取自身的shared_ptr(例如,为了将自己添加到主体中,或者在回调中传递自己的shared_ptr),那么这个观察者类必须继承自std::enable_shared_from_this。这允许对象通过shared_from_this()方法安全地获取自身的shared_ptr,避免了从this指针直接构造shared_ptr的风险。

总而言之,weak_ptr是C++智能指针家族中一个非常实用的成员,尤其是在处理复杂的对象关系和生命周期管理时。它在观察者模式中的应用,完美地体现了其“非拥有但可观察”的特性,让我们的代码更加健壮和安全。

相关专题

更多
counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

193

2023.11.20

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

472

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

131

2025.12.24

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

65

2025.12.31

php网站源码教程大全
php网站源码教程大全

本专题整合了php网站源码相关教程,阅读专题下面的文章了解更多详细内容。

45

2025.12.31

视频文件格式
视频文件格式

本专题整合了视频文件格式相关内容,阅读专题下面的文章了解更多详细内容。

40

2025.12.31

不受国内限制的浏览器大全
不受国内限制的浏览器大全

想找真正自由、无限制的上网体验?本合集精选2025年最开放、隐私强、访问无阻的浏览器App,涵盖Tor、Brave、Via、X浏览器、Mullvad等高自由度工具。支持自定义搜索引擎、广告拦截、隐身模式及全球网站无障碍访问,部分更具备防追踪、去谷歌化、双内核切换等高级功能。无论日常浏览、隐私保护还是突破地域限制,总有一款适合你!

41

2025.12.31

出现404解决方法大全
出现404解决方法大全

本专题整合了404错误解决方法大全,阅读专题下面的文章了解更多详细内容。

232

2025.12.31

html5怎么播放视频
html5怎么播放视频

想让网页流畅播放视频?本合集详解HTML5视频播放核心方法!涵盖<video>标签基础用法、多格式兼容(MP4/WebM/OGV)、自定义播放控件、响应式适配及常见浏览器兼容问题解决方案。无需插件,纯前端实现高清视频嵌入,助你快速打造现代化网页视频体验。

9

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号