详解 C++ unique_ptr:独占式智能指针
1. unique_ptr 的核心概念与定义
1.1 什么是 unique_ptr
std::unique_ptr 是 C++11 标准库中引入的一种智能指针,其核心设计目标是提供一种对动态分配对象的独占所有权(Exclusive Ownership) 管理机制。
与原始指针(raw pointer)不同,unique_ptr 遵循 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 原则,确保其管理的内存在其生命周期结束时能够被自动、可靠地释放,从而有效防止了内存泄漏、悬空指针等常见的内存管理错误。
unique_ptr 对象本身是一个轻量级的封装,它在大多数情况下与原始指针具有相同的性能开销,但提供了更强的安全保障和更清晰的语义。它通过禁止拷贝操作,只允许移动操作,来强制实现所有权的唯一性,即在任何时刻,只有一个 unique_ptr 可以拥有并负责释放一个特定的对象。
1.1.1 独占所有权语义
unique_ptr 的”独占所有权”是其最核心的特性。这意味着一个 unique_ptr 对象对其所指向的资源拥有唯一的控制权。当 unique_ptr 被销毁时(例如,离开其作用域),它所管理的对象也会被自动销毁。
这种机制确保了资源的释放是确定性的,并且与 unique_ptr 的生命周期紧密绑定。为了实现这种独占性,unique_ptr 明确禁止了拷贝构造函数和拷贝赋值运算符。任何试图复制一个 unique_ptr 的操作都会导致编译错误。
然而,所有权是可以被转移(Transfer) 的。C++11 引入的移动语义(Move Semantics) 为 unique_ptr 提供了完美的支持。通过 std::move(),可以将一个 unique_ptr 的所有权转移给另一个 unique_ptr,而原指针则会被置为空(nullptr),不再拥有任何资源。
1  | std::unique_ptr<int> ptr1(new int(42));  | 
这种所有权的转移机制,使得 unique_ptr 在函数参数传递和返回值中非常有用,可以清晰地表达所有权的转移意图。
1.1.2 RAII(资源获取即初始化)原则的体现
unique_ptr 是 RAII(Resource Acquisition Is Initialization) 原则的绝佳范例。RAII 是一种编程范式,它将资源的生命周期与对象的生命周期绑定。
资源(如内存、文件句柄、锁等)在对象的构造函数中获取,并在对象的析构函数中释放。unique_ptr 完美地遵循了这一原则:当通过 new 创建一个对象并用其初始化 unique_ptr 时,资源被获取;当 unique_ptr 对象离开其作用域并被销毁时,其析构函数会自动调用,从而释放所管理的资源。
这种机制极大地简化了资源管理,尤其是在处理异常时。即使在函数执行过程中抛出异常,导致函数提前退出,局部作用域中的 unique_ptr 对象依然会被正确销毁,其管理的资源也会被安全释放,从而避免了因异常处理不当而导致的资源泄漏。
1.1.3 与原始指针的区别与优势
与原始指针相比,unique_ptr 提供了显著的优势:
- 安全性:原始指针需要程序员手动调用 
delete来释放内存,这极易出错,容易导致内存泄漏(忘记delete)或悬空指针(delete后继续使用)。unique_ptr通过自动管理内存,从根本上消除了这类问题。 - 清晰的语义:通过其独占所有权的特性,代码的意图变得更加明确:一个 
unique_ptr明确表示它拥有并负责释放该对象。这使得代码更易于理解和维护。 - 扩展性:
unique_ptr还支持自定义删除器,使其能够管理不仅仅是通过new分配的资源,例如文件句柄、网络连接等,进一步扩展了其应用范围。 - 性能:尽管 
unique_ptr提供了这些额外的功能,但其性能开销极小,在大多数情况下与原始指针相当。 
| 特性 | 原始指针 (T*) | std::unique_ptr | 
|---|---|---|
| 所有权 | 不明确,需要程序员手动管理 | 独占所有权,通过移动语义转移 | 
| 生命周期 | 不明确,需要手动 delete | 与 unique_ptr 对象的生命周期绑定,自动释放 | 
| 内存泄漏风险 | 高,容易忘记 delete | 无,自动释放 | 
| 悬垂指针风险 | 高,delete 后指针仍有效 | 低,释放后 unique_ptr 变为 nullptr | 
| 重复释放风险 | 高,可能多次 delete 同一指针 | 无,所有权唯一 | 
| 拷贝 | 支持浅拷贝,可能导致多个指针指向同一资源 | 不支持拷贝,仅支持移动 | 
| 大小 | 与平台指针大小相同 | 通常与原始指针大小相同(得益于空基类优化) | 
| 自定义删除器 | 需要手动调用 | 支持在模板参数中指定 | 
1.2 模板类定义与参数
std::unique_ptr 是一个类模板,其定义位于 <memory> 头文件中。它有两种形式:一种用于管理单个对象,另一种用于管理动态分配的数组。
1.2.1 模板参数 T:被管理的对象类型
第一个模板参数 T 指定了 unique_ptr 所管理的对象的类型。例如,std::unique_ptr<int> 管理一个 int 类型的对象,而 std::unique_ptr<MyClass> 则管理一个 MyClass 类型的对象。
在 unique_ptr 的内部,这个类型被别名为 element_type。对于数组特化版本 unique_ptr<T[]>,T 表示数组元素的类型。
需要注意的是,如果 T 是一个引用类型,那么实例化 unique_ptr<T> 的程序是格式错误的。
1.2.2 模板参数 Deleter:自定义删除器类型
第二个模板参数 Deleter 是一个可选参数,用于指定一个可调用对象(函数对象或函数指针),该对象定义了如何销毁被管理的资源。这个类型在 unique_ptr 内部被别名为 deleter_type。
默认情况下,Deleter 是 std::default_delete<T>,它简单地调用 delete 来释放内存。然而,通过提供自定义的删除器,unique_ptr 可以管理任何需要特殊清理逻辑的资源。
Deleter 必须是一个可调用对象,能够接受一个 unique_ptr::pointer 类型的参数。将 Deleter 作为模板参数是实现零开销抽象的关键,因为它允许编译器在编译时内联删除器的调用,从而避免了运行时的额外开销。
1.2.3 默认删除器 std::default_delete
std::default_delete<T> 是 unique_ptr 的默认删除器。它是一个无状态的函数对象(functor),其 operator() 简单地调用 delete 来销毁其管理的对象。
对于数组特化版本 unique_ptr<T[]>,default_delete<T[]> 会调用 delete[] 来释放数组。
default_delete 的设计非常高效,因为它是一个空类(empty class),不包含任何数据成员。这使得 unique_ptr 可以利用空基类优化(EBO) 技术,在不增加额外内存开销的情况下存储删除器。
当使用默认删除器时,unique_ptr 的大小通常与原始指针相同,保证了其轻量级的特性。
需要注意的是,如果使用 default_delete,被管理的类型 T 在 unique_ptr 的析构函数、移动赋值运算符和 reset 成员函数被实例化的地方必须是完整类型(complete type)。
1.3 内部类型定义
为了方便使用和提高代码的可读性,unique_ptr 定义了几个内部类型别名。
1.3.1 element_type:被管理对象的类型别名
element_type 是模板参数 T 的别名。它代表了 unique_ptr 所管理的对象的类型。例如,在 std::unique_ptr<int> 中,element_type 就是 int。
这个别名在编写泛型代码时非常有用,因为它允许在不明确知道 T 的具体类型的情况下,引用被管理对象的类型。
1.3.2 deleter_type:删除器的类型别名
deleter_type 是模板参数 Deleter 的别名。它代表了用于销毁被管理对象的删除器的类型。例如,在 std::unique_ptr<int, MyDeleter> 中,deleter_type 就是 MyDeleter。
这个别名同样有助于编写泛型代码,使得代码可以灵活地处理不同类型的删除器。
1.3.3 pointer:指针类型别名
pointer 是一个更复杂的类型别名,它定义了 unique_ptr 内部使用的指针类型。它的定义如下:
- 如果 
Deleter类型(去除引用后)有一个名为pointer的内部类型,那么unique_ptr::pointer就是这个类型。 - 否则,
pointer就是T*。 
这个设计允许 Deleter 指定一个”智能”指针类型(例如,一个用于共享内存的偏移指针),而不仅仅是普通的 T*。这使得 unique_ptr 能够与更广泛的资源管理方案集成。
pointer 类型必须满足 NullablePointer 的要求,这意味着它可以被默认构造、与 nullptr 进行比较,并且可以像原始指针一样使用。
2. unique_ptr 的常用接口与用法
unique_ptr 提供了一套丰富的接口,用于管理其生命周期和所有权。这些接口的设计旨在提供安全、高效且直观的资源管理方式。
2.1 构造与析构
unique_ptr 的构造函数和析构函数是其资源管理功能的核心。
2.1.1 默认构造函数:创建空的 unique_ptr
unique_ptr 的默认构造函数创建一个不拥有任何对象的 unique_ptr,其内部指针被初始化为 nullptr。
1  | std::unique_ptr<int> ptr; // ptr 不管理任何对象,get() 返回 nullptr  | 
这个构造函数是 noexcept 的,并且要求删除器类型 Deleter 是可默认构造的,且其构造过程不抛出异常。
此外,unique_ptr 还有一个接受 std::nullptr_t 的构造函数,其效果与默认构造函数相同,这使得 unique_ptr 可以被显式地初始化为 nullptr。
2.1.2 带指针的构造函数:接管原始指针
unique_ptr 提供了一个显式的构造函数,用于从一个原始指针接管所有权。
1  | std::unique_ptr<int> ptr(new int(42)); // ptr 现在拥有这个 int 对象  | 
这个构造函数将传入的指针存储在 unique_ptr 内部,并负责在析构时释放它。这个构造函数也是 noexcept 的,并且要求删除器是可默认构造的。
需要注意的是,一旦将一个原始指针交给 unique_ptr 管理,就不应该再手动 delete 它,也不应该再使用这个原始指针,因为 unique_ptr 会在其生命周期结束时自动处理释放。
2.1.3 移动构造函数:转移所有权
unique_ptr 的核心特性之一是其移动语义。它提供了移动构造函数,允许将所有权从一个 unique_ptr 转移给另一个。
1  | std::unique_ptr<int> ptr1(new int(42));  | 
移动构造函数是 noexcept 的,并且要求删除器是可移动构造的(如果 Deleter 不是引用类型)。
此外,unique_ptr 还支持从一个不同类型的 unique_ptr 进行移动构造,只要源指针类型可以隐式转换为目标指针类型,并且源删除器类型可以转换为目标删除器类型。这种跨类型的移动构造在处理继承层次结构时非常有用。
2.1.4 析构函数:自动释放资源
unique_ptr 的析构函数是其 RAII 机制的关键。当一个 unique_ptr 对象被销毁时,其析构函数会检查它是否拥有一个对象。如果拥有,析构函数会调用其存储的删除器来销毁该对象。
例如,对于默认删除器,它会调用 delete。析构函数要求 get_deleter()(get()) 这个表达式是合法的,并且不会抛出异常。
这种自动释放机制确保了即使在发生异常的情况下,资源也能被正确清理,从而极大地提高了代码的健壮性。
2.2 所有权管理核心接口
unique_ptr 提供了一组核心接口,用于在不转移所有权的情况下观察和操作其管理的对象。
2.2.1 get():获取原始指针(不转移所有权)
get() 成员函数返回 unique_ptr 内部存储的原始指针,但不会转移所有权。调用 get() 后,unique_ptr 仍然拥有该对象,并将在其析构时负责释放它。
这个函数通常用于需要与只接受原始指针的旧版 API 进行交互的场景。
1  | std::unique_ptr<int> ptr(new int(42));  | 
get() 是 noexcept 的,并且返回的指针类型是 unique_ptr::pointer。
2.2.2 release():释放所有权并返回原始指针
release() 成员函数会断开 unique_ptr 与其管理对象之间的关联,并返回该对象的原始指针。调用 release() 后,unique_ptr 变为空(get() == nullptr),而调用者则获得了该对象的所有权,并需要负责在适当的时候手动释放它。
1  | std::unique_ptr<int> ptr(new int(42));  | 
release() 是 noexcept 的,它首先将内部指针设置为 nullptr,然后返回其先前的值。
这个函数在需要将所有权转移给一个不接受 unique_ptr 的函数或 API 时非常有用。
2.2.3 reset():重置管理的对象
reset() 成员函数用于替换 unique_ptr 当前管理的对象。它首先会销毁当前管理的对象(如果存在),然后接管新传入的指针的所有权。
1  | std::unique_ptr<int> ptr(new int(42));  | 
reset() 可以接受一个原始指针参数,也可以不带参数(默认为 nullptr)。如果传入的指针与当前管理的指针相同,reset() 会先存储新指针,然后删除旧指针,这可能导致传入的指针被立即删除。
reset() 是 noexcept 的,并且要求删除器的调用不抛出异常。
2.2.4 swap():交换两个 unique_ptr 的管理对象
swap() 成员函数用于交换两个 unique_ptr 对象的内容,包括它们管理的指针和删除器。
1  | std::unique_ptr<int> ptr1(new int(42));  | 
swap() 是 noexcept 的,并且通常具有常数时间复杂度。
此外,标准库还提供了一个非成员的 swap 函数模板,专门用于 unique_ptr,它也会调用成员 swap 函数。
2.3 其他重要操作符与函数
除了上述核心接口,unique_ptr 还提供了一些操作符和辅助函数,使其使用起来更像一个原始指针。
2.3.1 operator=:移动赋值运算符
unique_ptr 提供了移动赋值运算符,允许将一个 unique_ptr 的所有权转移给另一个。与移动构造函数类似,它会销毁左侧 unique_ptr 当前管理的对象,然后接管右侧 unique_ptr 的所有权。
1  | std::unique_ptr<int> ptr1(new int(42));  | 
移动赋值运算符是 noexcept 的,并且要求删除器是可移动赋值的(如果 Deleter 不是引用类型)。
它还支持从一个不同类型的 unique_ptr 进行移动赋值,只要满足相应的类型转换要求。
2.3.2 operator* 与 operator->:解引用操作
对于非数组版本的 unique_ptr,提供了 operator* 和 operator->,使其可以像原始指针一样进行解引用。
operator*返回对所管理对象的引用。operator->返回内部存储的原始指针,允许访问对象的成员。
1  | std::unique_ptr<MyClass> ptr(new MyClass());  | 
这些操作符使得 unique_ptr 的使用体验与原始指针非常相似,但提供了额外的安全保障。
2.3.3 operator bool:检查是否管理对象
unique_ptr 提供了一个转换到 bool 类型的操作符,用于检查其是否管理一个对象。如果 unique_ptr 不为空(即 get() != nullptr),则返回 true;否则返回 false。
1  | std::unique_ptr<int> ptr(new int(42));  | 
这个操作符使得检查 unique_ptr 的状态变得非常直观和方便。
2.3.4 get_deleter():获取删除器
get_deleter() 成员函数返回一个对 unique_ptr 内部存储的删除器的引用。这允许在运行时访问和修改删除器的状态(如果删除器不是无状态的)。
1  | auto deleter = [](int* p) { std::cout << "Deleting int\n"; delete p; };  | 
get_deleter() 有两个重载版本,一个返回非常量引用,另一个返回常量引用。
3. unique_ptr 的内部实现逻辑
unique_ptr 的高效性和强大功能源于其精巧的内部实现,特别是其对空基类优化(EBO)的利用。
3.1 核心数据结构
unique_ptr 的内部实现主要围绕两个核心数据成员:一个用于存储被管理对象的原始指针,另一个用于存储删除器。
3.1.1 存储原始指针
unique_ptr 必须存储一个指向其管理对象的原始指针。这个指针的类型由 unique_ptr::pointer 定义,通常是 T*,但在使用自定义删除器时可能是其他类型。
这个指针是 unique_ptr 的核心,所有对管理对象的访问都通过这个指针进行。
3.1.2 存储删除器
unique_ptr 还需要存储其删除器。删除器的类型是 Deleter,它可以是任何可调用对象。
将删除器作为 unique_ptr 的一部分,使得 unique_ptr 能够灵活地管理各种需要特殊清理逻辑的资源。然而,如果删除器是一个有状态的、非空的对象,直接将其作为 unique_ptr 的成员会增加 unique_ptr 的大小,从而引入额外的内存开销。
为了解决这个问题,unique_ptr 采用了空基类优化技术。
3.2 空基类优化(EBO)
空基类优化(Empty Base Optimization, EBO) 是 C++ 中一项重要的优化技术,它允许一个空类(即没有非静态数据成员的类)作为基类时,不占用任何额外的内存空间。unique_ptr 利用 EBO 来优化其内部存储,特别是当使用默认的无状态删除器时。
3.2.1 压缩空指针(Compressed Pair)技术
为了实现 EBO,unique_ptr 的实现通常依赖于一个名为 compressed_pair(或类似名称)的内部辅助类。这个类模板旨在存储两个对象,并利用 EBO 来消除其中一个对象(如果它是空的)的存储开销。
compressed_pair 的实现通常涉及模板特化和继承。它会检查两个模板参数的类型,如果其中一个类型是空的且不是 final 的,它就会将该类型作为基类,而另一个类型作为成员变量。这样,如果删除器是一个空类(如 default_delete),它就不会占用任何额外的内存空间。
3.2.2 优化空删除器的存储开销
在 unique_ptr 的典型实现中,其唯一的成员变量是一个 compressed_pair<pointer, deleter_type>。当使用默认的 default_delete<T> 时,deleter_type 是一个空类。compressed_pair 会检测到这一点,并将 deleter_type 作为其基类。由于 EBO,这个基类不会增加 compressed_pair 的大小。
因此,unique_ptr 的最终大小就只有一个指针的大小,与原始指针完全相同,实现了零开销抽象。
这种优化是 unique_ptr 将 Deleter 作为模板参数而非构造函数参数的关键原因,也是其性能优于 shared_ptr 的重要因素之一。
3.3 移动语义的实现
unique_ptr 的独占所有权语义是通过禁用拷贝操作并启用移动操作来实现的。
3.3.1 删除拷贝构造函数和拷贝赋值运算符
为了确保所有权的唯一性,unique_ptr 明确地将拷贝构造函数和拷贝赋值运算符声明为 delete。这意味着任何试图复制 unique_ptr 的代码都会在编译时失败,从而从根本上防止了所有权的共享。
3.3.2 实现移动构造函数和移动赋值运算符
unique_ptr 提供了移动构造函数和移动赋值运算符,它们允许将所有权从一个 unique_ptr 转移给另一个。这些函数的实现通常涉及以下步骤:
- 从源 
unique_ptr中获取原始指针(通过release())。 - 将获取的指针存储到目标 
unique_ptr中。 - 将源 
unique_ptr的删除器移动或复制到目标unique_ptr中。 - 确保源 
unique_ptr在操作后变为空(get() == nullptr)。 
通过这种方式,unique_ptr 实现了所有权的明确转移,保证了在任何时候都只有一个 unique_ptr 实例负责管理一个特定的资源。
4. unique_ptr 与 shared_ptr 的区别
unique_ptr 和 shared_ptr 是 C++ 标准库中两种主要的智能指针,它们都用于自动管理动态分配的资源,但其底层所有权模型、性能和适用场景有显著不同。
4.1 所有权模型对比
所有权模型是 unique_ptr 和 shared_ptr 最根本的区别。
4.1.1 unique_ptr 的独占所有权
unique_ptr 遵循独占所有权模型。在任何给定时刻,只有一个 unique_ptr 实例可以拥有一个特定的对象。当这个 unique_ptr 被销毁时,它所拥有的对象也会被销毁。
这种模型保证了资源释放的确定性和即时性。所有权的转移只能通过移动语义(std::move)来完成,这使得所有权的转移在代码中非常明确。
4.1.2 shared_ptr 的共享所有权与引用计数
shared_ptr 遵循共享所有权模型。多个 shared_ptr 实例可以同时拥有同一个对象。为了跟踪有多少个 shared_ptr 正在共享一个对象,shared_ptr 内部使用了一个引用计数器。
每当一个新的 shared_ptr 被创建并指向该对象时,引用计数加一;每当一个 shared_ptr 被销毁或重置时,引用计数减一。当引用计数变为零时,意味着没有 shared_ptr 再拥有该对象,此时对象会被自动销毁。
这种模型提供了更大的灵活性,但也引入了额外的开销和潜在的循环引用问题。
4.2 复制与移动语义对比
所有权模型的差异直接导致了两者在复制和移动语义上的不同。
4.2.1 unique_ptr 仅支持移动
unique_ptr 明确禁止拷贝,只支持移动。拷贝构造函数和拷贝赋值运算符被声明为 delete,以防止所有权的意外共享。移动操作则高效地转移所有权,源 unique_ptr 在操作后会变为空。
4.2.2 shared_ptr 支持复制和移动
shared_ptr 既支持拷贝也支持移动。
- 拷贝:当一个 
shared_ptr被拷贝时,新的shared_ptr会与原shared_ptr共享同一个对象,并且引用计数会加一。 - 移动:当一个 
shared_ptr被移动时,所有权会从源shared_ptr转移到目标shared_ptr,源shared_ptr会变为空。与拷贝不同,移动操作不会修改引用计数,因此性能更高。 
4.3 性能与开销对比
性能和开销是选择 unique_ptr 还是 shared_ptr 时需要考虑的重要因素。
4.3.1 unique_ptr 的轻量级特性
unique_ptr 是一个非常轻量级的智能指针。在大多数情况下,它的大小与原始指针相同,因为它利用了空基类优化来消除无状态删除器的存储开销。
它的操作(如构造、析构、移动)通常与原始指针的操作一样高效,没有额外的运行时开销。
4.3.2 shared_ptr 的引用计数开销
shared_ptr 的性能开销相对较大。这主要源于其内部的引用计数机制。
- 内存开销:
shared_ptr需要为每个管理的对象分配一个额外的控制块(control block)来存储引用计数和弱引用计数。这使得shared_ptr本身的大小通常是原始指针的两倍(一个指针指向对象,一个指针指向控制块)。 - 运行时开销:引用计数的增减必须是原子操作,以保证线程安全。这些原子操作会带来一定的性能开销,尤其是在多线程环境下频繁创建和销毁 
shared_ptr时。 
| 特性 | std::unique_ptr | std::shared_ptr | 
|---|---|---|
| 所有权模型 | 独占所有权 | 共享所有权 | 
| 复制语义 | 不支持(被删除) | 支持(增加引用计数) | 
| 移动语义 | 支持(转移所有权) | 支持(转移所有权,不增加计数) | 
| 内存开销 | 一个指针大小(轻量级) | 两个指针大小 + 控制块(较重) | 
| 性能开销 | 极小 | 引用计数的原子操作开销 | 
| 循环引用 | 不存在 | 可能导致内存泄漏(需配合 weak_ptr) | 
| 适用场景 | 独占资源、性能敏感、工厂函数 | 共享资源、复杂数据结构、异步操作 | 
4.4 适用场景对比
基于以上区别,unique_ptr 和 shared_ptr 适用于不同的场景。
4.4.1 何时选择 unique_ptr
- 独占所有权:当资源的生命周期应该由单个所有者明确管理时,应首选 
unique_ptr。例如,在工厂函数中创建并返回一个对象,或者在类中作为成员变量管理一个动态分配的子对象。 - 性能敏感:在对性能要求较高的场景下,应优先使用 
unique_ptr,因为它没有引用计数的开销。 - RAII 封装:当需要封装任何需要特殊清理逻辑的资源(如文件句柄、数据库连接等)时,
unique_ptr配合自定义删除器是一个非常好的选择。 
4.4.2 何时选择 shared_ptr
- 共享所有权:当多个对象需要共享同一个资源,并且资源的生命周期应该由最后一个使用者决定时,应使用 
shared_ptr。例如,在复杂的对象关系图中,或者在缓存系统中。 - 需要复制:当需要将一个指针传递给多个接收者,并且每个接收者都可能延长该对象的生命周期时,
shared_ptr的拷贝语义非常有用。 - 与 weak_ptr 配合使用:当需要打破循环引用,或者需要观察一个对象但不想影响其生命周期时,
shared_ptr可以与weak_ptr配合使用。 
5. unique_ptr 的实际应用场景
unique_ptr 凭借其高效、安全和语义清晰的特性,在现代 C++ 编程中有着广泛的应用。
5.1 管理动态分配的对象
unique_ptr 最常见的用途是管理通过 new 动态分配的对象,确保其内存能够被自动释放。
5.1.1 作为类成员变量(pImpl 惯用法)
在类设计中,将 unique_ptr 作为成员变量是实现 pImpl(Pointer to Implementation) 惯用法的理想选择。pImpl 惯用法通过将实现细节隐藏在一个不透明的指针后面,来减少编译依赖和接口的稳定性。
使用 unique_ptr 作为这个指针,可以自动管理实现对象的生命周期,无需在类的析构函数中手动 delete。
1  | // MyClass.h  | 
这种方式不仅简化了代码,还提供了强大的异常安全保障。
5.1.2 在函数中传递和返回所有权
unique_ptr 非常适合在函数之间传递和返回动态分配对象的所有权。
- 作为返回值:工厂函数可以返回一个 
unique_ptr,将新创建对象的所有权转移给调用者。 
1  | std::unique_ptr<MyClass> createMyClass() {  | 
- 作为参数:函数可以通过值或右值引用的方式接收 
unique_ptr,以接管所有权。 
1  | void processMyClass(std::unique_ptr<MyClass> ptr) {  | 
这种方式使得所有权的转移在代码中非常明确,避免了所有权的混淆。
5.2 管理动态数组
unique_ptr 提供了对动态数组的特化版本 unique_ptr<T[]>,用于管理通过 new[] 分配的数组。
5.2.1 使用 unique_ptr<T[]>
unique_ptr<T[]> 的特化版本重载了 operator[],并确保在析构时调用 delete[] 而不是 delete。
1  | std::unique_ptr<int[]> arr(new int[10]);  | 
这使得管理动态数组变得像管理单个对象一样简单和安全。
5.2.2 访问数组元素
对于 unique_ptr<T[]>,可以使用 operator[] 来访问数组的元素,就像使用普通数组一样。
1  | std::unique_ptr<int[]> arr(new int[10]);  | 
这种方式比使用 std::vector 更轻量,但功能也更有限,因为它不支持动态调整大小。
5.3 与标准库容器结合使用
unique_ptr 可以作为标准库容器(如 std::vector)的元素类型,用于创建多态对象容器。
5.3.1 在 vector 中存储 unique_ptr
由于 unique_ptr 不可拷贝,但可移动,因此可以将其存储在 std::vector 中。这使得可以创建一个拥有其元素的容器。
1  | std::vector<std::unique_ptr<MyClass>> vec;  | 
当 vec 被销毁时,其所有元素(即 unique_ptr 对象)也会被销毁,从而自动释放所有管理的 MyClass 对象。
5.3.2 实现多态容器
通过将基类的 unique_ptr 存储在容器中,可以创建一个多态容器,用于存储不同派生类的对象。
1  | std::vector<std::unique_ptr<Base>> poly_vec;  | 
当 poly_vec 被销毁时,所有 Derived1 和 Derived2 对象都会被自动正确地销毁。
需要注意的是,如果通过基类 unique_ptr 删除派生类对象,基类的析构函数必须是虚函数(virtual),否则会导致未定义行为。
5.4 自定义删除器的应用
unique_ptr 的第二个模板参数 Deleter 使其能够管理任何需要特殊清理逻辑的资源,而不仅仅是通过 new 分配的内存。
5.4.1 管理文件句柄(FILE*)
这是一个经典的自定义删除器应用场景。C 标准库中的文件操作使用 FILE* 句柄,需要通过 fclose 来关闭。
1  | // 定义一个自定义删除器  | 
5.4.2 管理其他需要特殊清理的资源
除了文件句柄,自定义删除器还可以用于管理各种其他资源,例如:
- 网络套接字:删除器可以调用 
close或closesocket来关闭套接字。 - 数据库连接:删除器可以调用相应的 API 来断开数据库连接。
 - 互斥锁:删除器可以调用 
unlock来释放锁,确保即使在异常情况下锁也能被正确释放。 - 共享内存:删除器可以调用 
shm_unlink或类似的函数来清理共享内存段。 
通过这种方式,unique_ptr 成为了一个通用的 RAII 包装器,可以用于管理任何需要显式清理的资源,极大地提高了代码的健壮性和安全性。
- 本文作者: 迪丽惹Bug
 - 本文链接: https://lyroom.github.io/2025/09/23/详解-C-unique-ptr:独占式智能指针/
 - 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!