跳转至

std::unique_ptr

标准 难度 状态 分级 证据 审阅 最后校验
C++11 2/5 final A 3 条(manual 3) 单审(zyuhao7) 2026-10-30

编译器支持:GCC 4.4+ · Clang 3.0+ · MSVC 18.0+。版本查询:cppreference 编译器支持。

主张与证据

主张 支持证据
用 unique_ptr 成员表达输入段与重定位段的独占所有权 ev-mold-unique-ptr-sections-001
用 unique_ptr 参数转移格式化器的所有权 ev-spdlog-unique-ptr-formatter-002
用 unique_ptr 成员独占时区实现并显式禁用复制 ev-abseil-unique-ptr-zone-003

本站证据页:ev-abseil-unique-ptr-zone-003 · ev-mold-unique-ptr-sections-001 · ev-spdlog-unique-ptr-formatter-002

解决什么问题

裸指针加 new/delete 把释放责任留给调用方:提前返回、异常抛出或路径分支都会漏掉 delete;复制指针又会让两个所有者指向同一对象,最终重复释放。std::auto_ptr 曾尝试表达所有权,但它在复制时偷偷转移所有权,无法放入标准容器,已被废弃。

std::unique_ptr 用类型表达“唯一所有者”:对象析构时自动释放,复制被禁用,所有权只能通过移动转移。它没有额外开销——默认删除器是空基类,指针大小的对象即可完成管理。标准提案为 N1856。

基础用法

用 std::unique_ptr<T> 持有对象,用 std::move 转移所有权。C++11 没有 std::make_unique,通常先 new 再交给构造函数。

#include <iostream>
#include <memory>
#include <utility>

struct Connection {
  explicit Connection(int id) : id(id) {}
  ~Connection() { std::cout << "close " << id << '\n'; }
  int id;
};

std::unique_ptr<Connection> connect(int id) {
  return std::unique_ptr<Connection>(new Connection(id));
}

int main() {
  std::unique_ptr<Connection> primary = connect(1);
  std::unique_ptr<Connection> backup = connect(2);
  primary = std::move(backup);
  std::cout << (backup ? "held" : "moved") << '\n';
}

输出 close 1、moved,退出时再打印 close 2:赋值先把 primary 原来的连接释放,再接管 backup 的对象;backup 变为空,同一个对象只释放一次。完整版本见 examples/cpp11/unique_ptr.cpp。

进阶用法

删除器是 unique_ptr 的第二个模板参数。声明 std::unique_ptr<FILE, decltype(&fclose)> 时删除器是函数指针,对象大小从 8 字节涨到 16 字节;无捕获的 lambda 或空结构体删除器能享受空基类优化,恢复零开销。删除器参与类型,因此 unique_ptr<T, D1> 与 unique_ptr<T, D2> 不能互相赋值。

所有权可以沿继承层次向上转换:std::unique_ptr<Derived> 能隐式移动构造 std::unique_ptr<Base>,前提是 Base 的析构函数为虚函数;反向转换不提供。release() 交出裸指针并清空自身,reset() 先释放旧对象再接管新指针,get() 只观察不移交。

工厂函数按值返回 unique_ptr 依赖移动语义,编译器会省略多余的移动。C++14 的 std::make_unique 把分配封装成一次调用,避免 new T(f(), g()) 中参数求值顺序导致的泄漏;C++11 代码可以自己封装等价函数。

Pimpl 惯用法把实现细节放进 std::unique_ptr<Impl> 成员,头文件不再依赖实现类型的完整定义。代价是析构函数必须定义在实现文件里,否则编译器在实例化默认析构时看不到完整类型。

真实项目证据

项目 位置 用法 为什么典型
mold src/mold.h#L533-L535 用 unique_ptr 容器成员与单个成员持有段对象 所有权随父对象析构,禁止复制避免重复释放
spdlog include/spdlog/spdlog-inl.h#L23-L30 set_formatter 接收 unique_ptr 并移入注册表 API 用参数类型明确表达所有权转移
abseil absl/time/internal/cctz/src/time_zone_impl.h#L80-L90 unique_ptr<TimeZoneIf> 成员持有实现,外层复制被删除 实现对象只被一处拥有,随父对象析构释放

三条证据均为 manual 级:作者复核,尚未引入第二名审核者。三处都在生产路径上,覆盖容器成员、API 参数与实现指针三种所有权形态。

陷阱与边界

同一裸指针不能构造两个 unique_ptr。 std::unique_ptr<T> a(p); std::unique_ptr<T> b(p); 会让两者都尝试释放 p。指针已经归某个 unique_ptr 所有后,只能通过 get() 观察,不能再交给另一个所有者。

传参方式表达所有权。 函数要接管对象就按值接收 std::unique_ptr<T> 并 std::move 传入;只借用则接收 T* 或 T&,不要让调用方为了传参而无谓转移。

删除器会改变类型。 std::unique_ptr<T, D> 的 D 参与类型,不同删除器的 unique_ptr 不能互相赋值;函数指针删除器还会让对象不再是零开销。

数组要用 unique_ptr<T[]>。 普通 unique_ptr<T> 对数组调用 delete 而不是 delete[];C++11 提供数组特化,但更推荐 std::vector 或 std::array。

std::make_unique 是 C++14。 C++11 代码里只能写 std::unique_ptr<T>(new T(...));自己封装时要注意异常安全,避免 new T(f(), g()) 中参数求值顺序带来的泄漏。

不要用 get() 接管生命周期。 release() 交出裸指针后必须由调用方负责释放;混用 get() 与手工 delete 会产生悬空指针和二次释放。

基类析构函数必须是虚函数。 通过 unique_ptr<Base> 释放派生对象时,非虚析构只调用基类部分,派生部分泄漏;新增虚函数前先确认这一约定。

库联动

  • <memory>:std::default_delete 是默认删除器;C++14 的 std::make_unique 提供异常安全的构造入口。
  • 标准容器:std::vector<std::unique_ptr<T>> 是异构多态容器的常见写法,元素只能移动;std::map 的 value 为 unique_ptr 时同样只支持移动插入。
  • <cstdio> / 系统资源:std::unique_ptr<FILE, decltype(&fclose)> 把 C 接口纳入 RAII,socket、句柄的封装方式相同。
  • mold / spdlog:链接器用 unique_ptr 容器管理输入段与重定位段,日志库用参数类型声明格式化器的所有权转移。
  • std::shared_ptr:需要多个持有者时改用共享所有权,但要接受原子计数与循环引用问题。

横向对比

方案 所有权表达 复制 运行期开销 主要限制
unique_ptr 独占 禁止 默认零额外开销 不能共享,转移需显式移动
shared_ptr 共享 允许 原子引用计数 有循环引用与控制块成本
裸指针 + 手工管理 无 允许 无 泄漏、重复释放、异常不安全
值语义 独占 值复制 取决于类型 大对象复制昂贵,不适合多态

跨标准演进:C++11 通过 N1856 引入 unique_ptr 并废弃 auto_ptr;C++14 增加 std::make_unique;C++17 起 auto_ptr 从标准中移除。