unique_ptr与auto_ptr

ZaynPei Lv6

std::unique_ptr 同样是 C++11 中引入的,用于表示对动态分配对象独占所有权(Exclusive Ownership)。

std::unique_ptr:独占所有权的轻量级管理者

与 shared_ptr 的“共享”理念完全相反,unique_ptr 遵循“独裁”模式:在任何时刻,只能有一个 unique_ptr 指向并拥有一个给定的对象。当这个 unique_ptr 被销毁或重置时,它所拥有的对象也会被立即销毁。

独占所有权 (Exclusive Ownership): 一个资源(内存、文件句柄等)的生命周期由唯一的 unique_ptr 控制。这从根本上杜绝了“谁该删除指针”的混乱问题,所有权模型非常清晰。

轻量级与高性能 (Lightweight & High-Performance): unique_ptr 是一个零成本抽象(Zero-cost Abstraction)。它内部没有引用计数,也没有控制块。在大多数情况下,一个 unique_ptr 的大小与一个原始指针完全相同。且它的操作(如访问成员)与操作原始指针一样快,没有任何额外的性能开销

不可拷贝,但可移动 (Non-copyable, but Movable): 为了保证所有权的“独占性”,unique_ptr 删除了拷贝构造函数拷贝赋值运算符。你不能像 shared_ptr 那样简单地复制它。

1
2
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
// std::unique_ptr<MyClass> ptr2 = ptr1; // 拷贝构造, 编译错误!
不过它拥有移动构造函数和移动赋值运算符。这意味着所有权可以被转移(Transfer)。一旦所有权被转移,原来的 unique_ptr 将变为空指针 (nullptr)。
1
2
3
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 正确,所有权从 ptr1 转移到 ptr2
// 此后,ptr1 等于 nullptr,ptr2 拥有对象

如何使用

首先是创建 unique_ptr (推荐使用 std::make_unique, C++14 中引入)

1
2
3
4
5
6
7
8
9
10
#include <memory>

class MyClass { /* ... */ };

// 推荐方式 (C++14 及以后)
// 优点:代码简洁、异常安全
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();

// C++11 的方式
std::unique_ptr<MyClass> ptr2(new MyClass());

所有权的转移是 unique_ptr 的核心操作模式,最常见的场景是从函数返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::unique_ptr<MyClass> create_widget() {
// 在函数内部创建对象, 对象在堆上
// ...
// 直接返回 unique_ptr,所有权被自动“移动”给调用者
return std::make_unique<MyClass>();
}

void process_widget(std::unique_ptr<MyClass> widget) {
// 这个函数通过移动接收了 widget 的所有权
// ...
} // 函数结束,widget 在此被销毁,其管理的对象也被销毁

int main() {
std::unique_ptr<MyClass> my_widget = create_widget(); // 从工厂函数接收所有权, 因为unique_ptr不可拷贝且移动更高效, 所以直接移动给了my_widget

my_widget->do_something();

process_widget(std::move(my_widget)); // 将所有权转移给 process_widget 函数

// 此后,main 函数中的 my_widget 变为 nullptr
// if (my_widget == nullptr) { /* ... */ }
}
这里在create_widget()函数中, return 语句返回一个临时创建的对象(在例子中是 std::make_unique 的结果)时,这个临时对象被视为一个右值(rvalue)。当用一个右值来初始化一个新的对象时(例如 my_widget = create_widget()),编译器会优先选择使用移动构造函数,而不是拷贝构造函数。

通过返回值和 std::move,unique_ptr 实现了清晰、安全的所有权在不同作用域之间的传递

现代 C++ 编译器通常会做得更极致。它们会使用一种叫做“返回值优化”的技术。在这种情况下,编译器会发现 create_widget 内部创建的指针最终会进入 main 函数的 my_widget 中,于是它会省略掉中间的“移动”步骤,直接在 my_widget 的内存位置上构造那个 unique_ptr。从外部看,就好像 create_widget 函数直接把对象变到了 main 函数里一样。

此外, 还有一些其他函数:

  • 访问:像普通指针一样使用 * 和 ->。
  • 获取原始指针:使用 get() 方法,规则和风险与 shared_ptr 的 get() 类似。
  • 释放所有权:调用 release() 方法。它会放弃所有权并返回原始指针,但不会删除对象。调用者需要手动管理返回的指针。
  • 重置:调用 reset() 方法。它会销毁当前拥有的对象,并可以选择性地接管一个新的对象。

高级特性

unique_ptr 比看起来更灵活,它支持两个强大的高级特性:

自定义删除器 (Custom Deleters)

与 shared_ptr 不同,unique_ptr 的删除器类型是其自身类型的一部分。这使得它仍然是零开销的,但不同删除器类型的 unique_ptr 是不同的类型。这使得 unique_ptr 非常适合用于管理任何需要配对操作的资源,完美实践 RAII(资源获取即初始化)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <cstdio>

// 自定义删除器结构体, 也可以是其他可调用对象如函数, lambda表达式, 封装的std::function
struct FileCloser {
void operator()(FILE* file) const {
if (file) {
fclose(file);
std::cout << "文件已关闭。" << std::endl;
}
}
};

// 使用 using 让类型名更简洁
using UniqueFilePtr = std::unique_ptr<FILE, FileCloser>; // 将删除器这个可调用对象也传入

int main() {
// fopen 返回 FILE*,我们立即将其所有权交给 unique_ptr
UniqueFilePtr file_ptr(fopen("test.txt", "w"));

if (file_ptr) {
fputs("Hello, unique_ptr!", file_ptr.get());
}

} // main 结束,file_ptr 被销毁,它的自定义删除器 FileCloser::operator() 会被自动调用,fclose(file) 得以执行

数组支持

unique_ptr 对动态分配的数组有特殊的重载版本,使用时需要加上 []

  • 创建:std::make_unique<T[]>(size)
  • 析构:它会自动调用 delete[] 而不是 delete,这是正确的数组内存释放方式。
  • 访问:它重载了 operator[] 来访问数组元素,但没有 * 和 ->
    1
    2
    3
    4
    5
    6
    7
    8
    // 创建一个包含 10 个整数的动态数组
    std::unique_ptr<int[]> arr_ptr = std::make_unique<int[]>(10);

    // 通过 operator[] 访问元素
    for (int i = 0; i < 10; ++i) {
    arr_ptr[i] = i * i;
    }
    // arr_ptr 离开作用域时,会自动调用 delete[] arr_ptr.get();

黄金法则:默认使用 std::unique_ptr。

在现代 C++ 中,当你需要动态分配内存时,unique_ptr 应该是你的第一选择。它的所有权模型清晰,性能无损,完全符合 RAII 思想。何时使用 unique_ptr?

  • 当你需要一个指向动态对象的指针,并且该对象的生命周期应该与这个指针的作用域绑定时。
  • 作为工厂函数的返回值,安全地将新创建对象的所有权转移出去。
  • 在类中作为成员,管理一个只属于该类实例的资源(例如,PIMPL 模式的实现)。

只有当你明确需要共享一个资源的所有权,即多个独立的观察者都需要延长该资源的生命周期时,才应该“升级”到使用 std::shared_ptr

std::auto_ptr

什么是auto_ptr? 真不熟

std::auto_ptr 是 std::unique_ptr 的祖先, 它在 C++11 中被不推荐使用(deprecated),并在 C++17 中被彻底移除, 完全被std::unique_ptr代替。