Featured image of post C++智能指针unique_ptr​与make_unique

C++智能指针unique_ptr​与make_unique

一、核心概念:什么是 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))); // 潜在内存泄漏风险!

编译器可能按以下顺序执行:

  1. new MyClass(100)
  2. new MyClass(200)
  3. 构造第一个 unique_ptr
  4. 构造第二个 unique_ptr

如果在步骤2之后、步骤3之前抛出异常,那么第一个 MyClass 对象已经分配但没有任何智能指针管理它,导致内存泄漏

解决方案:使用 make_unique

1process(std::make_unique<MyClass>(100),
2        std::make_unique<MyClass>(200)); // 异常安全!

make_uniquenewunique_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:编译器有机会将对象和控制块的内存分配合并为一次操作,减少内存开销和碎片。

四、使用技巧与注意事项

  1. auto 关键字搭配使用:这是最地道的写法。

    1auto widget = std::make_unique<MyWidget>("name", 123);
  2. 传递参数make_unique 的参数就是传递给构造函数参数。

    1class Person {
    2    Person(std::string name, int age);
    3};
    4auto person = std::make_unique<Person>("Alice", 30); // 等价于 new Person("Alice", 30)
    
  3. 管理数组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]);
  4. 自定义删除器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);
  5. 获取原始指针:使用 .get() 方法。注意:不要手动 delete 这个指针!

    1auto ptr = std::make_unique<MyClass>();
    2MyClass* raw_ptr = ptr.get(); // 用于调用一些需要原始指针的API
    3some_legacy_api(raw_ptr);
  6. 重置与释放

    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