跳到主要内容

智能指针选型: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 被移除,且"拷贝即转移"的语义极易踩坑。迁移方法:

  1. 找出所有 auto_ptr 出现点,确认是否需要共享;
  2. 独占语义换成 std::unique_ptr,配合 std::move;
  3. 共享语义换成 std::shared_ptr;
  4. 函数签名的 auto_ptr 参数同步改成移动语义;
  5. 编译选项中打开 -Wdeprecated-declarations 兜底。

智能指针写出的是"谁负责释放"的契约。契约清楚,代码自然短。