Skip to content

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;
};

父子互相强持有形成引用环,计数永远不归零。智能指针不是“自动正确”,它首先是一种所有权建模工具。

二、本课目标 ​

  1. 区分唯一所有、共享所有和非拥有观察。
  2. 正确使用 make_unique、make_shared。
  3. 理解控制块、引用计数和引用环。
  4. 识别异步任务中的生命周期风险。
  5. 根据领域关系选择类型,而不是默认 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-freeobserver 生命周期、异步捕获、错误的裸 this
线程竞态被指向对象数据,而非只看引用计数

十一、练习与答案 ​

1. unique_ptr 能否放进 vector? ​

可以。它不可复制但可移动,使用 push_back(std::move(ptr)) 或 emplace_back 转移所有权。

2. weak_ptr 是否保证对象存在? ​

不保证。只有成功 lock() 得到的临时 shared_ptr 在其作用域内保证对象存在。

3. 为什么不把所有指针都改成 shared_ptr? ​

它会模糊 owner、增加控制块和计数成本,还可能制造环;观察关系也不应无意延长生命周期。

十二、本课总结 ​

智能指针选择的根问题是“谁决定对象何时死亡”。先画所有权图,再选择唯一所有、共享所有或观察关系。