随着 C++11 标准的引入,智能指针成为了现代 C++ 编程中资源管理的核心工具。其中,unique_ptr 是一种独占式的智能指针,负责管理动态分配的对象,确保在不再需要时自动释放内存,避免内存泄漏。本文将全面、准确地介绍 unique_ptr 的用法、特点以及在实际编程中的应用和注意事项。

一、什么是 unique_ptr

unique_ptr 是 C++11 标准库中的智能指针,位于头文件 <memory> 中。它实现了独占式所有权,即同一时间内只有一个 unique_ptr 指向某个动态分配的对象。当 unique_ptr 被销毁时,所管理的对象也会被自动销毁,避免了内存泄漏。

特点:

  • 独占所有权unique_ptr 独自拥有所指向的对象,不能复制或共享。
  • 移动语义支持:支持移动构造和移动赋值,可以转移所有权。
  • 轻量级:无需引用计数,开销较小。

二、unique_ptr 的基本用法

2.1 初始化 unique_ptr

方法一:直接使用构造函数

1
2
3
#include <memory>

std::unique_ptr<int> p1(new int(10));

方法二:使用 std::make_unique(C++14 引入)

1
2
3
#include <memory>

auto p2 = std::make_unique<int>(20);

方法三:从已有的原始指针初始化(不推荐)

1
2
int* raw_ptr = new int(30);
std::unique_ptr<int> p3(raw_ptr); // 不推荐

注意: 方法三存在风险,如果多个 unique_ptr 管理同一原始指针,会导致未定义行为。

2.2 使用 unique_ptr

  • 访问所管理的对象

    1
    2
    3
    
    std::unique_ptr<AA> ptr = std::make_unique<AA>("对象名称");
    ptr->成员函数();
    (*ptr).成员函数();
    
  • 禁止复制和赋值

    1
    2
    3
    
    std::unique_ptr<int> p1(new int(10));
    std::unique_ptr<int> p2 = p1; // 错误,不能复制
    p2 = p1;                      // 错误,不能赋值
    
  • 移动所有权

    1
    
    std::unique_ptr<int> p3 = std::move(p1); // p1 资源转移给 p3
    

2.3 函数参数中的 unique_ptr

  • 传递所有权

    如果函数需要接管 unique_ptr 的所有权,可以通过 移动语义 传递:

    1
    2
    3
    4
    
    void func(std::unique_ptr<AA> ptr) {
        // 接管所有权
    }
    func(std::move(p1));
    
  • 共享访问

    如果函数只需要访问对象,不需要所有权,可以传递 引用原始指针

    1
    2
    
    void func(const std::unique_ptr<AA>& ptr); // 传引用
    void func(const AA* ptr);                  // 传原始指针
    

三、unique_ptr 的高级特性

3.1 所有权转移与移动语义

unique_ptr 禁止复制构造和复制赋值,但支持移动构造和移动赋值。这意味着可以通过 std::move 转移所有权。

1
2
std::unique_ptr<AA> p1 = std::make_unique<AA>("对象1");
std::unique_ptr<AA> p2 = std::move(p1); // p1 被置空,p2 接管所有权

注意: 转移所有权后,原来的 unique_ptr 将不再指向对象,应避免再次使用。

3.2 自定义删除器

unique_ptr 支持自定义删除器,以替代默认的 delete 操作。

1
2
3
4
5
6
void custom_deleter(AA* ptr) {
    // 自定义删除逻辑
    delete ptr;
}

std::unique_ptr<AA, decltype(&custom_deleter)> p(new AA, custom_deleter);

3.3 管理动态数组

unique_ptr 提供了针对数组的特化版本,可以管理动态分配的数组。

1
2
std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
arr[0] = 10; // 支持下标操作

注意: 管理数组时,unique_ptr 会使用 delete[] 来释放内存。


四、unique_ptr 的成员函数与操作

  • operator*operator->

    1
    2
    
    *ptr;       // 解引用,访问对象
    ptr->func(); // 通过指针访问成员函数
    
  • get()

    返回所管理对象的原始指针。

    1
    
    AA* raw_ptr = ptr.get();
    
  • release()

    释放所有权,返回原始指针,unique_ptr 置为空。

    1
    
    AA* raw_ptr = ptr.release(); // 需要手动 delete raw_ptr
    
  • reset()

    重置 unique_ptr,释放当前对象并接管新对象。

    1
    2
    
    ptr.reset(new AA("新对象"));
    ptr.reset(); // 释放对象,置为空
    
  • swap()

    交换两个 unique_ptr 所管理的对象。

    1
    
    ptr1.swap(ptr2);
    

五、示例代码解析

5.1 示例一:unique_ptr 在函数中的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <memory>
using namespace std;

class AA {
public:
    string m_name;
    AA(const string& name) : m_name(name) {
        cout << "构造函数 AA(" << m_name << ") 被调用。\n";
    }
    ~AA() {
        cout << "析构函数 ~AA(" << m_name << ") 被调用。\n";
    }
};

// 函数需要一个原始指针,不负责释放
void func1(const AA* a) {
    cout << "func1: " << a->m_name << endl;
}

// 函数需要一个原始指针,负责释放
void func2(AA* a) {
    cout << "func2: " << a->m_name << endl;
    delete a;
}

// 函数需要一个 unique_ptr 引用,不负责释放
void func3(const unique_ptr<AA>& a) {
    cout << "func3: " << a->m_name << endl;
}

// 函数需要一个 unique_ptr,负责释放(转移所有权)
void func4(unique_ptr<AA> a) {
    cout << "func4: " << a->m_name << endl;
}

int main() {
    unique_ptr<AA> pu = make_unique<AA>("西施");

    cout << "开始调用函数。\n";
    // func1(pu.get());         // 传递原始指针,不负责释放
    // func2(pu.release());     // 传递原始指针,负责释放,pu 被置空
    // func3(pu);               // 传递引用,不转移所有权
    func4(move(pu));            // 转移所有权,pu 被置空

    cout << "调用函数完成。\n";
    if (!pu) cout << "pu 是空指针。\n";

    return 0;
}

运行结果:

1
2
3
4
5
6
构造函数 AA(西施) 被调用。
开始调用函数。
func4: 西施
析构函数 ~AA(西施) 被调用。
调用函数完成。
pu 是空指针。

解析:

  • 使用 make_unique 创建一个 unique_ptr,指向对象 “西施”。
  • 调用 func4,通过 move 转移所有权,pu 被置空。
  • 函数结束后,func4 内的 unique_ptr 被销毁,自动调用析构函数。

5.2 示例二:使用 unique_ptr 管理数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>
using namespace std;

class AA {
public:
    string m_name;
    AA() { cout << "默认构造函数 AA() 被调用。\n"; }
    AA(const string& name) : m_name(name) {
        cout << "构造函数 AA(" << m_name << ") 被调用。\n";
    }
    ~AA() {
        cout << "析构函数 ~AA(" << m_name << ") 被调用。\n";
    }
};

int main() {
    // 使用 unique_ptr 管理动态数组
    unique_ptr<AA[]> parr(new AA[2]);
    parr[0].m_name = "西施";
    parr[1].m_name = "貂蝉";

    cout << "parr[0]: " << parr[0].m_name << endl;
    cout << "parr[1]: " << parr[1].m_name << endl;

    // 无需手动 delete,unique_ptr 会自动释放内存
    return 0;
}

运行结果:

1
2
3
4
5
6
默认构造函数 AA() 被调用。
默认构造函数 AA() 被调用。
parr[0]: 西施
parr[1]: 貂蝉
析构函数 ~AA() 被调用。
析构函数 ~AA() 被调用。

解析:

  • 创建一个 unique_ptr<AA[]>,管理一个大小为 2 的 AA 数组。
  • 通过下标操作赋值和访问数组元素。
  • 程序结束时,unique_ptr 自动调用 delete[] 释放内存。

六、注意事项与最佳实践

  1. 禁止复制操作

    unique_ptr 禁止复制构造和复制赋值,确保独占所有权。

  2. 慎用原始指针初始化

    避免使用同一个原始指针初始化多个 unique_ptr,会导致多次释放同一内存。

  3. 不要管理非动态分配的内存

    unique_ptr 应该只管理通过 new 分配的内存,不要用于栈内存或其他方式分配的内存。

  4. 转移所有权时使用 std::move

    当需要将 unique_ptr 作为函数参数并转移所有权时,使用 std::move

  5. 管理数组时使用特化版本

    使用 unique_ptr<T[]> 来管理动态数组,支持下标操作符。

  6. 自定义删除器

    如果需要自定义资源释放方式,可以在 unique_ptr 中指定自定义删除器。

  7. 避免空悬指针

    unique_ptr 被销毁或转移所有权后,应避免再次使用。


七、总结

unique_ptr 是现代 C++ 中管理动态内存的首选工具之一。通过独占式的所有权模型和对移动语义的支持,unique_ptr 提供了高效、安全的资源管理方式。在实际编程中,合理使用 unique_ptr,可以显著减少内存泄漏和空悬指针等问题,提高代码的可靠性和可维护性。