智能指针选型:unique_ptr、shared_ptr、weak_ptr
裸指针的问题不是"危险",而是"所有权不明"。智能指针的价值在于把所有权写进类型。选型只有一句话原则:默认 unique_ptr,共享所有权才用 shared_ptr,打破循环才用 weak_ptr。
一、unique_ptr:独占所有权
#include <memory>
// 创建
std::unique_ptr<Foo> p = std::make_unique<Foo>(1, 2); // C++14
// 转移所有权(不能拷贝)
std::unique_ptr<Foo> q = std::move(p);
// p 现在为 nullptr
// 释放
p.reset();
q = nullptr; // 自动 delete
要点:
make_unique是 C++14 才有,C++11 里写std::unique_ptr<Foo>(new Foo(...));- 删除器可定制,适合管理非
new分配的资源:
auto deleter = [](FILE* f) { if (f) std::fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(std::fopen("a.txt", "r"), deleter);
- 作为函数参数时,
const Foo&表示"观察",std::unique_ptr<Foo>按值表示"接管所有权",语义一目了然。
二、shared_ptr:共享所有权
auto sp1 = std::make_shared<Foo>();
auto sp2 = sp1; // 引用计数 = 2
std::weak_ptr<Foo> wp = sp1; // 不影响计数
控制块里保存强引用计数与弱引用计数:强引用归零时析构对象,弱引用也归零时才释放控制块。
优先用 std::make_shared:一次分配同时容纳对象与控制块,比 shared_ptr<Foo>(new Foo) 更快、异常更安全。代价是无法指定自定义删除器,并且只要还有 weak_ptr,对象内存就不会归还(因为同一块内存)。
三、代价不可忽略
shared_ptr 的拷贝是原子计数操作,在热点路径上明显比 unique_ptr 慢;对象大小也是两个指针。因此:
- 传递时用
const std::shared_ptr<T>&(不改变所有权)或T&/T*(纯观察); - 不要为了"方便"把成员全写成
shared_ptr,会带来意外延长生命周期的风险; - 循环引用必须用
weak_ptr或干脆改成值语义。
四、循环引用排查
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // ❌ 双向链表:互相持有,永不为 0
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // a、b 计数都为 2,泄漏
修复:把"反向"关系改成 weak_ptr:
struct Node2 {
std::shared_ptr<Node2> next;
std::weak_ptr<Node2> prev; // ✅ 不增加计数
};
判断哪一边用 weak_ptr 的原则是:谁的生命周期由谁决定。父节点持有子节点用 shared_ptr,子节点回指父节点用 weak_ptr。
五、weak_ptr 的正确用法
weak_ptr 不能直接解引用,必须先 lock() 提升:
void use(std::weak_ptr<Foo> wp) {
if (auto sp = wp.lock()) { // 提升成功说明对象还活着
sp->run(); // 在此期间对象不会被销毁
} else {
// 对象已销毁,安全跳过
}
}
不要写 if (!wp.expired()) wp.lock()->run();——expired() 与 lock() 之间存在竞态窗口,多线程下可能崩溃。
六、常见误用清单
| 误用 | 问题 | 修复 |
|---|---|---|
std::shared_ptr<Foo>(new Foo) 两次包装同一裸指针 | 双重释放 | 从源头就用 make_shared,或先建立唯一所有者 |
this 被外部 shared_ptr 接管 | 对象可能已销毁 | 继承 enable_shared_from_this,用 shared_from_this() |
unique_ptr 未标 noexcept 场景下频繁移动 | 影响容器性能 | 移动操作天然 noexcept,别额外抛异常 |
数组用 unique_ptr<T> | 删除不匹配 | 用 unique_ptr<T[]> 或 std::vector |
| 智能指针当函数参数不加区分 | 语义模糊 | 观察用 T&,转移用 unique_ptr<T> |
七、enable_shared_from_this
class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
// ❌ auto self = std::shared_ptr<Session>(this);
auto self = shared_from_this(); // ✅ 共享已有控制块
async_op([self] { self->handle(); });
}
};
前提:对象必须已经由 shared_ptr 管理,否则 shared_from_this() 抛 bad_weak_ptr。
八、选型决策表
| 场景 | 选择 |
|---|---|
| 工厂返回单个对象 | std::unique_ptr<T> |
| 类成员持有从属对象 | std::unique_ptr<T> |
| 多处共享、生命周期难以确定 | std::shared_ptr<T> |
| 观察但不影响生命周期 | T* 或 T&,必要时 weak_ptr<T> |
| 缓存(可随时失效) | weak_ptr<T> |
| 数组 | std::vector<T> 优先 |
九、与老代码共存
C++98 项目里常见 std::auto_ptr,它在 C++11 已废弃、C++17 被移除,且"拷贝即转移"的语义极易踩坑。迁移方法:
- 找出所有
auto_ptr出现点,确认是否需要共享; - 独占语义换成
std::unique_ptr,配合std::move; - 共享语义换成
std::shared_ptr; - 函数签名的
auto_ptr参数同步改成移动语义; - 编译选项中打开
-Wdeprecated-declarations兜底。
智能指针写出的是"谁负责释放"的契约。契约清楚,代码自然短。