外观
Stage07|Modern C++ Foundation
Lesson086|unique_ptr、shared_ptr、weak_ptr 与所有权
Tags: #C++17 #SmartPointer #Ownership #LifetimeDifficulty: ⭐⭐⭐⭐⭐
一、问题场景:用了 shared_ptr 为什么仍然泄漏
cpp
struct Node {
std::shared_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
};父子互相强持有形成引用环,计数永远不归零。智能指针不是“自动正确”,它首先是一种所有权建模工具。
二、本课目标
- 区分唯一所有、共享所有和非拥有观察。
- 正确使用
make_unique、make_shared。 - 理解控制块、引用计数和引用环。
- 识别异步任务中的生命周期风险。
- 根据领域关系选择类型,而不是默认 shared_ptr。
三、回到 TaskSystem:队列应该拥有什么
TaskSystem 最简单的所有权图是:
text
TaskSystem
└── queue<Task> 或 queue<unique_ptr<Task>>
└── worker 临时借用正在执行的任务如果任务只在一个队列和一个 worker 之间转移,unique_ptr 或值语义更清楚;只有多个独立缓存、回调和执行者共同决定生命周期时,才需要 shared_ptr。先画 owner,再选择指针,而不是看到异步就全部 shared_ptr。
四、先画所有权图
text
World
└── unique_ptr<Entity>
└── unique_ptr<Component>
RenderQueue
└── Component* / reference:本帧观察,不拥有如果一个对象有明确唯一 owner,优先 unique_ptr。它体积小、语义强、不能复制但可移动。
cpp
auto entity = std::make_unique<Entity>();
world.add(std::move(entity));五、何时需要 shared_ptr
只有当多个独立参与者确实共同决定对象生命周期时,才使用 shared_ptr:
cpp
auto asset = std::make_shared<TextureAsset>();
cache.store(asset);
renderer.submit(asset);shared_ptr 通常包含对象指针和控制块关系;复制会修改原子或非原子计数,热路径中也有成本。它解决共享生命周期,不解决线程安全:多个线程可以安全管理不同 shared_ptr 实例,但被指向对象仍需自身同步。
六、weak_ptr 打断环并观察
cpp
struct Node : std::enable_shared_from_this<Node> {
std::weak_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
};使用前必须锁定:
cpp
if (auto parent = node.parent.lock()) {
parent->refresh();
}expired() 后再单独取对象存在检查和使用之间的竞态,lock() 才是原子地尝试获得临时强引用。
七、enable_shared_from_this 陷阱
不要对同一个裸指针创建两个独立控制块:
cpp
// 错误:可能双重释放
std::shared_ptr<Node> a(raw);
std::shared_ptr<Node> b(raw);对象必须从一开始由 shared_ptr 管理,才能安全调用 shared_from_this();构造函数内调用也可能因控制块尚未建立而失败。
八、可编译验证:观察析构
cpp
#include <iostream>
#include <memory>
struct Child;
struct Parent {
~Parent() { std::cout << "~Parent\n"; }
std::shared_ptr<Child> child;
};
struct Child {
~Child() { std::cout << "~Child\n"; }
std::weak_ptr<Parent> parent;
};
int main() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent;
}先用 shared_ptr<Parent> 作为反向引用复现不析构,再改成 weak_ptr,验证两个析构日志出现。
九、TaskSystem 的异步捕获
cpp
std::weak_ptr<ViewModel> weak = model;
queue.post([weak] {
if (auto model = weak.lock()) model->applyResult();
});这表达“任务不应延长页面生命,但执行时必须临时保证对象存在”。若任务必须完成并拥有状态,则捕获 shared_ptr 可能是正确选择,关键是明确语义。
在任务系统中,捕获 this 只是借用指针,不会延长 owner 生命周期;捕获 shared_ptr 会让任务延长对象生命,可能导致页面或服务无法退出;捕获 weak_ptr 则允许结果因 owner 已离开而丢弃。三种写法分别表达三种产品语义,不能只用“防崩溃”解释。
十、诊断矩阵
| 症状 | 检查 |
|---|---|
| 对象不析构 | 引用环、队列闭包、缓存持有、全局 shared_ptr |
| 双重释放 | 多控制块、裸指针和智能指针混管 |
| 偶发 use-after-free | observer 生命周期、异步捕获、错误的裸 this |
| 线程竞态 | 被指向对象数据,而非只看引用计数 |
十一、练习与答案
1. unique_ptr 能否放进 vector?
可以。它不可复制但可移动,使用 push_back(std::move(ptr)) 或 emplace_back 转移所有权。
2. weak_ptr 是否保证对象存在?
不保证。只有成功 lock() 得到的临时 shared_ptr 在其作用域内保证对象存在。
3. 为什么不把所有指针都改成 shared_ptr?
它会模糊 owner、增加控制块和计数成本,还可能制造环;观察关系也不应无意延长生命周期。
十二、本课总结
智能指针选择的根问题是“谁决定对象何时死亡”。先画所有权图,再选择唯一所有、共享所有或观察关系。
