一、核心概念:什么是 std::unique_ptr
?
std::unique_ptr
是 C++11 引入的一种独占所有权的智能指针。它的设计哲学是:
- 独占性:同一时间内,有且只能有一个
unique_ptr
指向一个特定的对象。它无法被复制(copy
)。 - 移动性:所有权可以通过
std::move
转移给另一个unique_ptr
。转移后,原来的指针变为nullptr
。 - 自动释放:当
unique_ptr
离开其作用域(被销毁)时,它会自动调用delete
或指定的删除器来释放其所管理的对象内存。
基本用法:
1#include <memory>
2
3class MyClass {
4public:
5 MyClass(int value) : data(value) {}
6 void doSomething() {}
7 int data;
8};
9
10// 1. 创建一个 unique_ptr (C++11 方式 - 不推荐)
11std::unique_ptr<MyClass> ptr1(new MyClass(10));
12
13// 2. 使用 make_unique (C++14 方式 - 推荐)
14auto ptr2 = std::make_unique<MyClass>(20);
15
16// 使用 -> 操作符访问成员
17ptr2->doSomething();
18// 使用 * 操作符解引用
19(*ptr2).data = 30;
20
21// 所有权转移
22std::unique_ptr<MyClass> ptr3 = std::move(ptr2); // ptr2 现在为 nullptr
23// std::unique_ptr<MyClass> ptr4 = ptr3; // 错误!无法复制
二、std::make_unique
的出现
std::make_unique
是 C++14 才加入标准库的(但很容易在 C++11 中自己实现一个)。它是一个工厂函数模板,用于创建并返回一个管理着新对象的 std::unique_ptr
。
它的存在主要是为了解决直接使用 new
构造 unique_ptr
时存在的缺陷。
三、核心区别与为什么首选 make_unique
1. 异常安全性 (最关键的区别!)
危险场景:
1void process(std::unique_ptr<MyClass> p1, std::unique_ptr<MyClass> p2);
2
3// 使用 new 的调用方式
4process(std::unique_ptr<MyClass>(new MyClass(100)),
5 std::unique_ptr<MyClass>(new MyClass(200))); // 潜在内存泄漏风险!
编译器可能按以下顺序执行:
new MyClass(100)
new MyClass(200)
- 构造第一个
unique_ptr
- 构造第二个
unique_ptr
如果在步骤2之后、步骤3之前抛出异常,那么第一个 MyClass
对象已经分配但没有任何智能指针管理它,导致内存泄漏。
解决方案:使用 make_unique
1process(std::make_unique<MyClass>(100),
2 std::make_unique<MyClass>(200)); // 异常安全!
make_unique
将 new
和 unique_ptr
的构造封装在一个函数调用内,消除了中间状态,保证了异常安全。
2. 代码简洁性与可读性 (DRY原则)
- 使用
new
:违反 DRY (Don’t Repeat Yourself) 原则。std::unique_ptr<MyClass> p(new MyClass(10)); // 'MyClass' 写了两次
- 使用
make_unique
:只需指定一次类型,通常与auto
搭配,非常干净。auto p = std::make_unique<MyClass>(10); // 清晰简洁
3. 潜在性能优势
- 使用
new
:可能进行两次内存分配(一次对象,一次控制块)。 - 使用
make_unique
:编译器有机会将对象和控制块的内存分配合并为一次操作,减少内存开销和碎片。
四、使用技巧与注意事项
与
auto
关键字搭配使用:这是最地道的写法。1auto widget = std::make_unique<MyWidget>("name", 123);
传递参数:
make_unique
的参数就是传递给构造函数参数。1class Person { 2 Person(std::string name, int age); 3}; 4auto person = std::make_unique<Person>("Alice", 30); // 等价于 new Person("Alice", 30)
管理数组:
unique_ptr
支持数组,但语法略有不同。1// 管理一个 int 数组 2auto arr = std::make_unique<int[]>(10); // 创建一个大小为10的int数组 3arr[0] = 42; 4 5// 也可以使用传统方式(需要指定删除器) 6std::unique_ptr<int[], std::default_delete<int[]>> arr2(new int[10]);
自定义删除器:
make_unique
不支持传递自定义删除器。这是你需要直接使用new
构造unique_ptr
的主要场景。1// 使用 make_unique,无法指定删除器 2auto p1 = std::make_unique<MyClass>(); 3 4// 需要自定义删除器时,必须直接构造 unique_ptr 5auto FileDeleter = [](FILE* fp) { if(fp) fclose(fp); }; 6std::unique_ptr<FILE, decltype(FileDeleter)> file_ptr(fopen("data.txt", "r"), FileDeleter);
获取原始指针:使用
.get()
方法。注意:不要手动 delete 这个指针!1auto ptr = std::make_unique<MyClass>(); 2MyClass* raw_ptr = ptr.get(); // 用于调用一些需要原始指针的API 3some_legacy_api(raw_ptr);
重置与释放:
1auto ptr = std::make_unique<MyClass>(); 2ptr.reset(); // 释放当前管理的对象,ptr变为nullptr 3ptr.reset(new MyClass); // 释放旧对象,管理新对象 4 5MyClass* raw_ptr = ptr.release(); // 释放所有权,返回原始指针,ptr变为nullptr。 6 // 现在你必须手动管理 raw_ptr 的生命周期!
五、取舍总结:何时用 make_unique
,何时用 new
?
场景 | 推荐方式 | 理由 |
---|---|---|
绝大多数情况 | std::make_unique<T>(...) | 异常安全、代码简洁、性能更优。是现代C++的惯用法。 |
需要自定义删除器 | std::unique_ptr<T, D>(new T(...), deleter) | make_unique 无法传递自定义删除器。 |
需要处理遗留代码或特殊内存 | 直接构造 unique_ptr | 例如,对象是由特殊的 malloc 或第三方库函数分配的,需要配套的自定义删除器。 |
黄金法则:优先使用 std::make_unique
。只有在它无法满足需求(主要是自定义删除器)时,才退回到直接使用 new
来构造 std::unique_ptr
。