延迟析构的核心是解耦“逻辑销毁”与“物理释放”,防止uaf:通过shared_ptr自定义deleter、atomic标记或weak_ptr边界检查实现,关键在确保释放时机安全可控。

为什么不能直接 delete 对象后还访问它
延迟析构不是“不让析构”,而是让析构时机可控——比如对象还在被其他线程读取,或正处在回调链中,此时强行 delete 会触发 UAF(Use-After-Free)。RAII 本身不提供延迟能力,std::unique_ptr 和 std::shared_ptr 的析构都是即时的,除非你主动把释放逻辑抽出来。
std::shared_ptr 配合自定义 deleter 实现延迟执行
这是最轻量、无需额外锁、且符合 RAII 直觉的做法:引用计数归零时并不立即释放资源,而是把释放动作交给一个可延后调用的函数(比如投递到线程池、压入队列、或等下一个事件循环 tick)。
常见错误是把 deleter 写成捕获局部变量的 lambda,导致 dangling reference:
auto ptr = std::shared_ptr<MyObj>(new MyObj, [](MyObj* p) {
// ❌ 错误:如果 this_queue 是栈变量,这里已析构
this_queue->post([p]{ delete p; });
});正确做法是确保 deleter 捕获的都是生命周期足够长的对象(如全局队列、静态线程池),或用 std::weak_ptr 避免强引用循环:
立即学习“C++免费学习笔记(深入)”;
- deleter 中只做「调度」,不直接操作被管理对象的成员
- 如果需访问对象状态,先用
std::shared_ptr重新构造(但要确认此时是否仍安全) - 避免在 deleter 里抛异常;C++ 标准规定若
delete抛异常,程序直接 terminate
手动管理 + std::atomic<bool></bool> 标记延迟释放条件
适合对性能敏感、且释放逻辑简单(比如仅需 delete)的场景。核心是把“析构权”和“对象存活权”解耦:对象内存仍存在,但业务逻辑认为它已“逻辑销毁”。
典型结构:
- 对象内含
std::atomic<bool> m_released{false}</bool> - 所有公开方法开头加
if (m_released.load(std::memory_order_acquire)) return; - 销毁入口(如
close())设为true,并触发异步清理任务 - 清理任务中检查
m_released并真正delete this(注意:只能用于 new 分配的、非栈对象)
容易踩的坑:
- 忘记在
delete this前调用基类析构逻辑(如有虚函数,必须显式调用~Base()) -
std::memory_order选错:release写要用memory_order_release,读端对应memory_order_acquire - 多线程下,对象可能被多个地方同时标记
released,delete this只能执行一次
用 std::weak_ptr 触发延迟析构的边界检查
当延迟析构需要“等待所有观察者离开”时,std::weak_ptr 是天然的信号源。比如一个对象被多个 worker 持有 std::weak_ptr,只有全部失效后才真正释放。
关键点:
- 持有方必须用
lock()转成std::shared_ptr才能安全访问;返回空说明对象已不可达 - 不能靠
use_count()判断——它不包含weak_ptr数量,且多线程下瞬时值无意义 - 想实现“最后一个 weak_ptr 析构时触发清理”,得配合
std::enable_shared_from_this+ 自定义控制块(较重,一般不推荐)
更实用的做法是:主线程维护一个 std::vector<:weak_ptr>></:weak_ptr>,定期遍历 lock(),清掉空项;当 vector 为空,再 delete 底层对象。
真正难的不是怎么写延迟逻辑,而是判断“什么时候才算安全释放”——这往往取决于你的并发模型和对象职责。比如网络连接对象,可能要等发送缓冲区清空、IOCP 完成例程返回、且所有 callback 已出栈,才能算真正可删。这些条件不会自动聚合,得靠你用状态机或引用计数显式建模。










