3.3 智能指针所有权模型


3.3 智能指针所有权模型

本节摘要:智能指针是把 3.2 节五法则实例化的现成工具:unique_ptr 表达"独占所有权",零开销、不可拷贝可移动;shared_ptr 用引用计数表达"共享所有权",控制块另住堆上;weak_ptr 是不拥有对象的观察者,专治 shared_ptr 的循环引用。选型的实质不是选类型,而是回答"这块内存归谁"。

你将学会

阅读完本节,你应当能够:

  1. 按所有权语义为场景选定 unique/shared/weak;
  2. 解释 shared_ptr 控制块的内存结构与两次堆分配问题;
  3. 识别并拆解循环引用;
  4. 说出 make_unique/make_shared 相对裸 new 的三点优势。

一、unique_ptr:独占者

#include <memory> #include <iostream> struct Task { ~Task() { std::cout << "Task 析构\n"; } }; void run() { auto t = std::make_unique<Task>(); // 独占所有权 // auto t2 = t; // 编译错误:独占不可拷贝 auto t2 = std::move(t); // 只能过户 此后 t 为空 } // t2 离开作用域 Task 析构自动发生

unique_ptr 是 3.2 节 Buffer 的标准化版本,而且更聪明:不可拷贝、只能移动,所有权在类型系统里就是排他的。它的开销与裸指针完全相同(对象本体就一个指针,delete 内联进析构),多态删除自动正确(内部存的是删除器感知的 delete)。默认选择就它——"我想要堆上这个对象,且同一时刻只有一个主人"覆盖了绝大多数场景。

两个使用细节:数组特化 std::unique_ptr<int[]> 会正确调用 delete[](但更该用 vector);.get() 取裸指针仅供"只读借用"的旧接口使用,绝不 delete 从 get() 拿到的指针,也不许用它再造一个智能指针

二、shared_ptr:共享者与其账本

对象真有多主人时(缓存、观察者列表、图结构的节点),shared_ptr 用引用计数摊派责任:

auto a = std::make_shared<Task>(); // 计数 1 auto b = a; // 拷贝 计数 2 a.reset(); // a 放手 计数 1 b.reset(); // 计数归 0 Task 析构

计数的账本不在对象里,在控制块——一块独立的堆内存,记录强计数、弱计数与删除器。拷贝 shared_ptr 计数原子加一,析构原子减一,归零时释放对象,弱计数也归零时释放控制块。

图:shared_ptr 的两块内存与计数流转

图:shared_ptr 的两块内存与计数流转

成本清单要记牢:一次 make_shared 其实是一次堆分配同时安置对象与控制块(比裸 new 加构造 shared_ptr 的两次分配省一半);每次拷贝析构各是一次原子操作(多线程下有争用);对象直到最后一个 shared_ptr 消亡才释放,生命周期最长可达"到处都是"——这是共享所有权最大的隐性代价:你很难说清对象到底什么时候死。所以纪律是:默认 unique_ptr,确有多主人才 shared_ptr,函数参数要裸引用或裸指针(只借用不拥有)。

三、weak_ptr:观察者与拆弹专家

shared_ptr 有一处天生的软肋——循环引用

struct Node { std::shared_ptr<Node> next; ~Node() { std::cout << "Node 析构\n"; } }; void leak_cycle() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; // a 指 b:b 计数 2 b->next = a; // b 指 a:a 计数 2 } // 离开作用域:a b 各自计数仍为 1 永不归零 // 两个 Node 都不析构 静默泄漏 输出无析构字样

引用计数只认"指向我的柄有几个",环里互指的两点在局外柄消失后仍互相当拐杖,内存永不释放。拆弹方式是把环中一处改为不拥有weak_ptr

struct Node2 { std::shared_ptr<Node2> next; // 前向方向:拥有 std::weak_ptr<Node2> back; // 回指方向:只观察 }; void no_leak() { auto a = std::make_shared<Node2>(); auto b = std::make_shared<Node2>(); a->next = b; b->back = a; // weak 不增加强计数 } // 顺序析构 正常输出两次析构

weak_ptr 的用法是两步走:lock() 尝试升级为 shared_ptr,成功说明对象还活着(这本身就是线程安全的检查),失败得空:

if (auto live = a->back.lock()) { // 对象仍在 use(*live); } else { // 对象已消亡 // 缓存失效 观察者已退场 等场景 }

选型经验:父子关系,父拥有子(unique 或 shared 的 vector),子回望父用 weak;缓存持有 weak(命中就 lock 续命,未命中重建);观察者模式里主体存 weak 列表,避免"注册即永生"。一句话总结三兄弟:unique 是所有权,shared 是合伙制,weak 是知情权。

四、make 系列与遗留陷阱

std::make_uniquestd::make_shared 应当取代裸 new 的三个理由:合并堆分配次数(shared 情形);异常安全(C++17 前的 f(shared_ptr<T>(new T), may_throw()) 求值顺序可能泄漏);写起来更短且类型名只出现一次。少数不能用 make 的场合(自定义删除器、私有构造函数、内存紧张到不能容忍控制块与对象同生共死)才退回 new 加构造。

⚠️ 常见坑四连:同一裸指针构造两个 shared_ptr(两本账,双重释放);成员函数里按值拷贝 shared_ptr<self> 当参数(白白两次原子操作,传 const 引用即可);shared_from_this 必须在已有 shared 管理的对象上调用(公开继承 enable_shared_from_this);多线程下"计数安全"被误当"对象安全"——保护对象数据仍需互斥或原子(第 4.3 节展开)。

本节要点回顾

  • 默认 unique_ptr:零开销独占,只能移动;九成场景的正确答案。
  • shared_ptr 是合伙制:控制块记账、原子计数有真实成本、生命周期难推理;确有多主人才用。
  • weak_ptr 拆环:回指、缓存、观察者三类位置改观察者,lock 检活两步走。
  • make 优于 new:少分配、异常安全、少打字;自定义删除器等例外才裸 new。
  • 智能指针管的是"柄",不管"对象内部":多线程写对象本体仍需另行同步。

下一节解释为什么 unique_ptr "只能移动"反而是它快的秘密:移动语义,C++11 送给对象世界的搬家技术。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U