11. unique_ptr:独占所有权、移动语义、make_unique
好,咱们今天聊聊 unique_ptr。这是 C++11 引入的智能指针里,我个人用得最多的一个。说白了,它就是「独占所有权」的代名词。
你想想看,在传统 C++ 里,我们 new 出一个对象,然后到处传递原始指针。谁该负责 delete?什么时候 delete?传出去之后,接收方会不会偷偷 delete 掉?这些问题,几乎每个项目都会踩坑。我早期做嵌入式开发时,就曾因为一个裸指针在多个模块间流转,最后 double free 导致程序崩溃,查了整整两天。
unique_ptr 就是来解决这个问题的。它的核心理念很简单:一个对象,只能被一个 unique_ptr 拥有。不能拷贝,只能移动。所有权转移了,原来的指针就自动置空。
独占所有权的含义
什么叫「独占」?就是这把钥匙只有一个人拿着。你拿着,别人就拿不到。你想把钥匙给别人,必须亲手递过去,而且你手里就不再有了。
代码里就是这样:
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1); // 可以移动,所有权转移给 p2
// 此时 p1 为空,p2 持有资源
嗯,这里要注意:移动之后,p1 变成了空指针。如果你继续用 p1 访问资源,程序会崩溃。所以移动后最好不要再碰原指针,除非你重新给它赋值。
移动语义:所有权如何流转
移动语义是 unique_ptr 的灵魂。它让资源的所有权可以安全地转移,而不产生额外的拷贝开销。
我举个例子。假设你有一个工厂函数:
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>();
}
void useResource() {
auto res = createResource(); // 所有权从函数内部转移到外部
// 使用 res...
// 函数结束,res 析构,自动释放资源
}
这里 createResource 内部创建了资源,然后通过返回值把所有权转移给调用方。整个过程没有拷贝,只有一次移动(甚至被编译器优化掉)。
你可能会问:为什么返回局部 unique_ptr 是合法的?因为 C++ 标准规定,函数返回局部对象时,优先使用移动语义。如果移动不可用,才会尝试拷贝。而 unique_ptr 支持移动,所以没问题。
我在项目中经常用这种模式来封装资源获取逻辑。比如打开文件、连接数据库、分配 GPU 内存等。调用方拿到 unique_ptr 后,用完自动释放,省心省力。
make_unique:更安全的创建方式
C++14 引入了 std::make_unique。我个人强烈建议你用它,而不是直接 new。为什么?
- 异常安全:如果你写
foo(std::unique_ptr<T>(new T), bar()),编译器对参数的求值顺序是不确定的。如果bar()抛异常,new 出来的内存就泄漏了。而make_unique把创建和包装合为一步,不会出现这个问题。 - 代码更简洁:不用写两次类型名。比如
auto p = std::make_unique<MyClass>(arg1, arg2);,比std::unique_ptr<MyClass> p(new MyClass(arg1, arg2));清爽多了。 - 性能更好:
make_unique可以避免一次额外的内存分配(虽然现代编译器通常能优化掉,但习惯用 make_unique 总没错)。
不过有一个例外:如果你需要自定义删除器,就不能用 make_unique 了。这时候你得手动 new:
auto deleter = [](FILE* f) { fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> p(fopen("test.txt", "r"), deleter);
嗯,这种情况虽然少见,但遇到了要知道怎么处理。
自定义删除器
unique_ptr 的第二个模板参数就是删除器。默认是 std::default_delete<T>,调用 delete。但你可以换成任何可调用对象,比如函数指针、lambda、仿函数等。
我曾经在一个项目中需要管理第三方 C 库分配的内存,那个库要求用 my_free() 来释放。这时候自定义删除器就派上用场了:
auto my_deleter = [](int* p) {
std::cout << "自定义释放: " << *p << std::endl;
my_free(p);
};
std::unique_ptr<int, decltype(my_deleter)> p(
static_cast<int*>(my_alloc()), my_deleter
);
注意:自定义删除器会改变 unique_ptr 的类型。两个 unique_ptr 即使模板参数 T 相同,如果删除器类型不同,它们也是不同的类型,不能互相赋值或比较。
unique_ptr 与数组
unique_ptr 也支持数组。写法是 std::unique_ptr<int[]>。这时候默认删除器会调用 delete[] 而不是 delete。
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
arr[0] = 42; // 支持下标访问
// 自动调用 delete[]
不过说实话,我很少用 unique_ptr 管理数组。大多数情况下,std::vector 是更好的选择。只有在需要与 C 接口交互,或者需要精确控制内存布局时,我才会考虑 unique_ptr 数组。
知识结构图
下面这张图总结了 unique_ptr 的核心知识点和它们之间的关系:
unique_ptr 与裸指针的对比
| 特性 | 裸指针 | unique_ptr |
|---|---|---|
| 所有权语义 | 不明确,全靠文档和约定 | 独占所有权,编译器强制 |
| 内存释放 | 手动 delete,容易忘或重复 | 析构时自动释放 |
| 异常安全 | 异常路径容易泄漏 | 栈展开时自动释放 |
| 拷贝行为 | 浅拷贝,多个指针指向同一内存 | 禁止拷贝,只能移动 |
| 性能开销 | 无额外开销 | 几乎为零(与裸指针大小相同) |
| 适用场景 | 非拥有指针、观察者模式 | 资源所有者、工厂函数返回值 |
核心原则:默认使用 unique_ptr 管理动态分配的对象。只在需要「观察」但不「拥有」资源时,才使用裸指针或引用。
个人建议:写新代码时,尽量用 std::make_unique 而不是 new。这不仅是风格问题,更是安全习惯。我团队里已经把它写进了代码规范,违反的代码 review 直接打回。
避坑指南:我曾经在重构一个老项目时,看到有人把 unique_ptr 的 get() 返回的裸指针存起来,然后在别的地方 delete 掉。结果程序崩溃得一塌糊涂。记住:unique_ptr 的 get() 只用于临时访问,不要长期持有,更不要手动释放。
好了,unique_ptr 的核心内容就这些。它简单、高效、安全,是现代 C++ 资源管理的基石。你只要记住「独占、移动、make_unique」这三个关键词,就能用好它。