更新时间: 2026-08-27
42 篇讲了 RAII 思想:资源绑定对象生命周期。现在看第一个落地工具 std::unique_ptr。它是智能指针里最常用、也最"省心"的一个——但省心的前提是理解它的脾气:独占所有权,只移动,不拷贝。
本文要回答:unique_ptr 为什么不能拷贝?"移动"是什么?它和裸指针怎么安全地来回切换?以及那些 new 裸指针该不该换成 unique_ptr?
一、从裸指针到 unique_ptr
先用一个对比看它解决什么:
#include <memory>
// 裸指针的经典烦恼
void demo_raw() {
Widget* w = new Widget();
use(w);
if (error_occurred()) {
return; // 忘记 delete,泄漏
}
delete w;
}
// unique_ptr 版本
void demo_unique() {
std::unique_ptr<Widget> w = std::make_unique<Widget>();
use(w.get()); // 需要裸指针时,用 get()
if (error_occurred()) {
return; // 自动释放,无泄漏
}
} // 离开作用域自动 deleteunique_ptr 拥有它指向的对象:它是唯一的持有者。对象析构时,unique_ptr 自动 delete 所指对象。名字里的 "unique" 就是"独占"的意思。
二、为什么不能拷贝
这是新手最容易踩的坑:
std::unique_ptr<int> p = std::make_unique<int>(42);
std::unique_ptr<int> q = p; // 编译错误!拷贝被删除如果允许拷贝,两个 unique_ptr 指向同一个对象,各自析构时都 delete 一次——双重释放(double free),程序直接崩。C++ 的解法是:把拷贝删掉,只允许移动。
"移动"的意思是:把所有权转交给别人,自己变成空。移动之后原指针不再持有任何东西:
std::unique_ptr<int> p = std::make_unique<int>(42);
std::unique_ptr<int> q = std::move(p); // 所有权转移到 q
if (!p) { std::cout << "p 已为空\n"; } // p 现在是 nullptr
std::cout << *q; // q 持有 42std::move(p) 把 p 变成"可以移动"的状态,unique_ptr 的移动构造把内部指针交给 q,并把 p 置空。规则很简单:unique_ptr 可以搬走,不能复制。
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "拷贝(被禁止)" as copy #FFD9D9 {
node "p ──▶ 对象" as p1
node "q ──▶ 对象" as q1 #FFD9D9
node "双重释放 ✗" as dbl #FFD9D9
}
rectangle "移动(允许)" as move #D9FFE2 {
node "p ──▶ 对象" as p2
node "q ──▶ 对象 (p 变空)" as q2 #D9FFE2
node "单一持有者 ✓" as single #D9FFE2
}
copy -[hidden]down- move
@enduml这个"删拷贝、留移动"的模式,正是现代 C++ 表达"独占"的方式——类型系统替你保证同一时刻只有一个持有者,错误在编译期就被拒了,而不是运行时崩。
三、移动语义的直觉
std::move 这个名字容易让人误解,好像"把东西搬走"是它干的。其实 std::move 只是把左值转换成右值引用,本身不搬任何东西。真正搬东西的是接收方的移动构造函数/移动赋值。移动构造和拷贝构造的区别:拷贝是"再做一个",移动是"掏空对方的,装进自己的"。
std::unique_ptr<int> p = std::make_unique<int>(1);
std::unique_ptr<int> q = std::move(p); // 调用 q 的移动构造:取走 p 的指针,p 置空关于右值引用、移动语义的完整机制,本站有专门的 move-semantics.md 高手篇,这篇只用到"移动 = 所有权转移"这一层直觉就够了。
四、unique_ptr 的常用操作
#include <memory>
// 创建(强烈推荐 make_unique,理由见 46 篇)
std::unique_ptr<int> p = std::make_unique<int>(42);
// 判空
if (p) { /* 非空 */ }
if (!p) { /* 空 */ }
// 解引用
*p = 10;
int v = *p;
// 访问成员(指向类时)
auto obj = std::make_unique<Widget>();
obj->method(); // 和裸指针一样的 -> 语法
// 拿裸指针(只读借用,不转移所有权)
Widget* raw = obj.get();
if (raw) { /* ... */ }
// 释放所有权,交还裸指针
Widget* taken = obj.release(); // obj 变空,所有权在你手里,记得自己 delete
// 显式重置 / 释放
obj.reset(); // delete 旧对象,置空
obj.reset(new Widget()); // 换成新对象重点区分三个"放行"操作:
| 操作 | 作用 | 所有权 |
|---|---|---|
get() | 返回裸指针 | 不转移,用完别 delete |
release() | 交还裸指针 | 转移,必须自己 delete |
reset() | 释放旧对象 | 完全释放 |
get() 是最常用的——把 unique_ptr 传给只认裸指针的老接口。注意别把 get() 的结果存起来长期用:unique_ptr 一析构或 reset,那个裸指针就悬垂了。release() 用得少,它相当于"主动放弃 RAII",通常是你要把指针交给不认智能指针的 C 接口时。
五、C 对照:malloc/free 与 unique_ptr
C 里动态内存的完整生命周期是:malloc → 使用 → free。漏了 free 就泄漏。unique_ptr 对应关系:
| 环节 | C(裸指针) | C++(unique_ptr) |
|---|---|---|
| 分配 | malloc | make_unique / new |
| 使用 | p->field / *p | 相同语法,无差别 |
| 释放 | free(靠自觉) | 析构自动(编译器保证) |
| 提前 return | 可能泄漏 | 不会泄漏 |
| 赋值/复制 | 指针复制(别名) | 拷贝禁止,移动转移 |
| 判空 | if (p == NULL) | if (!p) |
C 的指针复制天然产生"多个指针指向同一块内存",谁 free 谁负责——这是 C 内存错误的大本营(double free、use-after-free)。C++ 用 unique_ptr 把"独占"写进类型,从根上消灭这类问题。
六、unique_ptr 作为函数参数和返回值
这是工程里最容易写错的地方,单独拎出来讲。
参数:函数只读使用,传裸指针或 const Widget&:
void inspect(const Widget* w); // 或 const Widget&
inspect(up.get()); // 借用返回值:直接返回 unique_ptr 就行,这是最省心的模式:
std::unique_ptr<Widget> make_widget() {
auto w = std::make_unique<Widget>();
return w; // 自动移动(甚至不算移动,是 NRVO 优化)
}
auto w = make_widget(); // 调用方拿到独占所有权函数返回 unique_ptr 代表"我把这个对象的所有权交给你"。这是 C++ 里表达"工厂产出、调用方负责销毁"的标准方式。注意别在函数参数里传 unique_ptr 按值——那是"你要把所有权交进来",多数时候不是本意。
七、数组与自定义删除器
unique_ptr 也支持数组和自定义删除方式:
// 数组版本(C++11 起有数组特化)
std::unique_ptr<int[]> arr = std::make_unique<int[]>(100);
arr[0] = 1; // 支持下标访问
// 自定义删除器(比如关文件、释放句柄)
auto closer = [](FILE* f) { if (f) std::fclose(f); };
std::unique_ptr<FILE, decltype(closer)> fp(std::fopen("a.txt", "r"), closer);数组特化会在析构时 delete[] 而不是 delete,不用自己操心。自定义删除器让它能管"非 new 来的资源"——只要是"借了要还"的东西,都能用 unique_ptr 包装。但日常开发里,make_unique + 默认删除器覆盖了绝大多数场景,自定义删除器属于"遇到了才用"。
八、与本站主线衔接
- 20 篇(拷贝与 Rule of Three):unique_ptr 是"禁拷贝"的教科书案例
- 42 篇(RAII):unique_ptr 是 RAII 在内存上的标准实现
- 44 篇(shared_ptr):需要共享时升级为 shared_ptr
- 31 篇(对象切片):多态容器存
unique_ptr<Base>是标准做法 move-semantics.md(高手篇):移动语义的完整机制
九、一句话总结
unique_ptr 用"拷贝删除 + 移动转移"表达独占所有权,让动态对象的释放由栈生命周期自动保证——创建用 make_unique,借用用 get(),转移用 std::move,从根上消灭 double free 和泄漏。