突破编程_C++_C++11新特性(智能指针unique_ptr)

1 std::unique_ptr 的概述

std::unique_ptr 是 C++11 标准库中引入的一种智能指针,它表示对动态分配对象的独占所有权。这意味着同一时间只能有一个 std::unique_ptr 指向一个特定的对象。当 std::unique_ptr 被销毁(例如,离开其作用域或被重新赋值)时,它所指向的对象也会被自动删除。这种特性使得 std::unique_ptr 成为管理独占资源(如动态分配的内存、文件句柄等)的理想选择。

1.1 std::unique_ptr 的基本特性

(1)独占所有权: 一个 std::unique_ptr 在任何时候都独占其所指向对象的所有权。不能复制一个 std::unique_ptr,但是可以移动它,从而转移所有权。

(2)自动内存管理: 当 std::unique_ptr 的生命周期结束时(例如,超出其作用域),它会自动删除所指向的对象,释放内存。这消除了手动管理内存的需要,减少了内存泄漏的风险。

(3)可定制删除器: std::unique_ptr 允许指定一个自定义的删除器,用于在释放对象时执行特定的清理操作。这提供了更大的灵活性,可以处理不同类型的资源。

(4)与原始指针的互操作性: 可以通过 get() 成员函数获取 std::unique_ptr 所指向的原始指针,但应谨慎使用,以避免破坏 std::unique_ptr 的所有权语义。

(5)不可拷贝但可移动: 由于 std::unique_ptr 表示独占所有权,因此它不支持拷贝构造和拷贝赋值。但是,它支持移动构造和移动赋值,允许将一个 std::unique_ptr 的所有权转移给另一个。

1.2 std::unique_ptr 的原理

std::unique_ptr 的原理是基于独占式所有权和 RAII(Resource Acquisition Is Initialization,资源获取即初始化)技术,确保动态分配的资源在不再需要时能够被自动释放。其核心实现原理如下:

(1)内部结构:
std::unique_ptr 内部通常包含一个原始指针,指向动态分配的对象。此外,它还可能包含一个删除器(deleter),用于在释放对象时执行特定的清理操作。删除器可以是一个函数指针或仿函数,允许用户自定义资源的释放方式。

(2)自动资源管理:
std::unique_ptr 通过 RAII 技术实现资源的自动管理。当 unique_ptr 对象被创建时,它负责获取资源(例如,通过 new 操作符分配内存)。当 unique_ptr 对象的生命周期结束时(例如,离开其作用域或被销毁),它的析构函数会被自动调用,进而调用删除器释放所管理的资源。这种机制确保了资源的适时释放,无需程序员手动调用 delete。

(3)移动语义:
由于 std::unique_ptr 表示独占所有权,它不支持拷贝操作,以避免资源被多个智能指针共享。然而,它支持移动操作。通过移动语义,可以将一个 unique_ptr 的所有权转移给另一个 unique_ptr,而不会导致资源的重复释放或遗漏释放。移动操作通常通过移动构造函数和移动赋值操作符实现,这些操作会转移资源的所有权,并将原 unique_ptr 置为 nullptr。

2 std::unique_ptr 的创建与初始化

2.1 使用 std::make_unique 创建

在 C++14 及以后的版本中,推荐使用 std::make_unique 函数来创建 std::unique_ptr。std::make_unique 不仅语法简洁,而且更加安全高效。它接受与 new 相同的参数,并返回一个相应的 unique_ptr。例如:

std::unique_ptr<int> ptr = std::make_unique<int>(12);

这行代码创建了一个unique_ptr,它指向一个通过 new int(12)动态分配的整数。如果内存分配失败,std::make_unique 会抛出 std::bad_alloc 异常,从而避免了可能的未定义行为。

2.2 直接初始化

尽管不推荐,但在某些情况下,可能需要直接初始化 std::unique_ptr。这可以通过将原始指针传递给 unique_ptr 的构造函数来实现。然而,这种方式要求程序员手动管理原始指针的生命周期,因此更容易出错。例如:

int* raw_ptr = new int(12);  
std::unique_ptr<int> ptr(raw_ptr);

注意,在这种情况下,需要确保在创建 unique_ptr 之前,raw_ptr 指向的是有效分配的内存,并且在 unique_ptr 接管所有权后,不再使用 raw_ptr。否则,可能会导致双重释放或其他内存管理问题。

2.3 自定义删除器

std::unique_ptr 允许指定一个自定义的删除器,用于在释放资源时执行特定的清理操作。删除器可以是一个函数指针、函数对象或 Lambda 表达式。例如:

void custom_deleter(int* ptr) {  
    // 自定义的清理逻辑  
    delete ptr;  
}  
  
std::unique_ptr<int, decltype(&custom_deleter)> ptr(new int(12), custom_deleter);

在这个例子中,unique_ptr 使用 custom_deleter 作为删除器来释放它所管理的资源。当 ptr 被销毁时,custom_deleter 会被调用。

2.4 初始化列表中的使用

在类的初始化列表中,也可以使用 std::make_unique 来初始化成员变量。这通常比在构造函数体内初始化更安全,因为它避免了潜在的异常安全问题:

class MyClass {  
public:  
    MyClass() : my_ptr(std::make_unique<int>(12)) {}  
  
private:  
    std::unique_ptr<int> my_ptr;  
};

在这个例子中,my_ptr 成员变量在 MyClass 的构造函数初始化列表中通过 std::make_unique 进行初始化。

2.5 移动初始化

由于 std::unique_ptr 不支持拷贝操作,但支持移动操作,因此你可以通过移动一个已存在的 unique_ptr 来初始化另一个。这通常发生在函数返回 unique_ptr 或需要将所有权从一个对象转移到另一个对象时:

std::unique_ptr<int> ptr1 = std::make_unique<int>(12);
std::unique_ptr<int> ptr2 = std::move(ptr1);

在这个例子中,通过 std::move 的操作,将资源从 ptr1 移动至 ptr2,此时 ptr1 为 nullptr。由于这里涉及的是移动操作,所以不会发生资源的重复释放。

3 std::unique_ptr 的所有权转移

std::unique_ptr 的所有权转移是 C++ 中内存管理的一个重要概念,它确保了资源在智能指针之间的安全传递。由于 std::unique_ptr 表示独占所有权,即同一时间内只能有一个 unique_ptr 指向某个特定的对象,因此其所有权的转移具有特定的规则和机制。

(1)所有权转移的规则

  • 不可复制:std::unique_ptr 不支持复制操作,包括拷贝构造函数和拷贝赋值操作符。这是因为如果允许复制,就会导致多个 unique_ptr 指向同一个对象,违反其独占所有权的特性。
  • 可移动:尽管 unique_ptr 不能复制,但它支持移动操作。这意味着可以通过移动构造函数和移动赋值操作符将一个 unique_ptr 的所有权转移给另一个 unique_ptr。

(2)所有权转移的方式(使用 std::move)

通常,使用 std::move 函数可以将一个 unique_ptr 的所有权转移给另一个 unique_ptr。std::move 实际上并不移动任何东西,它只是将其参数转换为右值引用,从而允许使用移动构造函数或移动赋值操作符。

std::unique_ptr<int> ptr1 = std::make_unique<int>(12);  
std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权

在上面的代码中,ptr1 原本指向一个动态分配的整数。通过调用 std::move(ptr1),可以将 ptr1 的所有权转移给 ptr2。转移后,ptr1 变为 nullptr,因为它不再拥有任何资源的所有权。

(3)函数返回值

当 unique_ptr 作为函数的返回值时,它也会自动进行所有权转移。这是通过移动构造函数实现的,因为函数返回的是右值。

std::unique_ptr<int> createUniquePtr() {  
    return std::make_unique<int>(12);  
}  
  
// ...  
  
std::unique_ptr<int> ptr = createUniquePtr(); // 所有权从函数返回时转移

在这个例子中,createUniquePtr 函数返回一个 unique_ptr,其所有权在返回时自动转移给调用者的 ptr。

(4)注意事项

  • 避免野指针:在转移所有权后,原始的 unique_ptr 将变为 nullptr。因此,在转移所有权后,不应再尝试访问或操作原始 unique_ptr 所指向的资源。
  • 避免多次移动:一旦 unique_ptr 的所有权被转移,它就不再拥有任何资源。试图再次移动一个已经是 nullptr 的 unique_ptr 是安全的,但通常是无意义的。
  • 与 shared_ptr 的交互:unique_ptr 可以转换为 shared_ptr,以实现所有权从独占式到共享式的转换。但是,shared_ptr 不能转换为 unique_ptr,因为 shared_ptr 允许多个指针指向同一个对象,这与 unique_ptr 的独占所有权特性相冲突。

4 std::unique_ptr 的自定义删除器

std::unique_ptr 的自定义删除器是 C++ 中一种强大的特性,它允许程序员在释放资源时执行自定义的清理操作。通过指定一个可调用对象作为删除器,你可以控制如何销毁 unique_ptr 所管理的对象。

自定义删除器可以是一个函数指针、函数对象(functor)或 Lambda 表达式。它必须满足特定的要求:即必须有一个形参类型为被管理对象的指针类型,返回值类型为 void 的 operator() 成员函数。这样,当 unique_ptr 的析构函数被调用时,它会使用指定的的删除器来释放资源。

4.1 使用函数指针作为删除器

函数指针是比较常用的自定义删除器。例如,假设有一个自定义的数据结构,它需要一个特定的清理函数来释放内存:

struct MyStruct {  
    // ... 成员变量和成员函数 ...  
};  
  
void customDelete(MyStruct* ptr) {  
    // 执行特定的清理逻辑  
    delete ptr;  
}  
  
// 使用函数指针作为删除器  
std::unique_ptr<MyStruct, decltype(&customDelete)> ptr(new MyStruct, customDelete);

在这个例子中,customDelete 函数被用作 unique_ptr 的删除器。通过 decltype 获取 customDelete 的类型,并将其作为 unique_ptr 的第二个模板参数。这样,当 ptr 被销毁时,customDelete 函数会被调用。

4.2 使用函数对象作为删除器

函数对象(也称为仿函数)也可以作为自定义删除器。函数对象是一个重载了 operator() 的类实例,它可以像函数一样被调用。

struct MyStruct {  
    // ... 成员变量和成员函数 ...  
}; 

struct CustomDeleter {  
    void operator()(MyStruct* ptr) {  
        // 执行特定的清理逻辑  
        delete ptr;  
    }  
};  
  
std::unique_ptr<MyStruct, CustomDeleter> ptr(new MyStruct);

在这个例子中,定义了一个名为 CustomDeleter 的函数对象,它重载了 operator() 以执行清理逻辑。然后,我们将 CustomDeleter 的实例作为 unique_ptr 的第二个模板参数。

4.3 使用 Lambda 表达式直接作为删除器

还可以直接使用匿名 Lambda 表达式作为删除器,而无需给它命名:

std::unique_ptr<MyStruct, std::function<void(MyStruct*)>> ptr(  
    new MyStruct,  
    [](MyStruct* ptr) {  
        // 执行特定的清理逻辑  
        delete ptr;  
    }  
);

在这个例子中,使用了 std::function 来包装 Lambda 表达式,并将其作为删除器传递给 unique_ptr。std::function 是一个通用的、类型擦除的函数包装器,它可以接受任何可调用对象。注意,这种方式需要引入 #include <functional> 。

4.4 自定义删除器的应用场景与注意事项

自定义删除器在多种场景下都非常有用。例如,当使用第三方库或 API,并且需要特定的清理逻辑来释放资源时,就可以通过自定义删除器来确保资源得到正确管理。此外,当需要管理不同类型的资源(如文件句柄、网络连接等)时,自定义删除器也非常有用。

使用自定义删除器的注意事项如下:

  • 确保删除器的正确性:自定义删除器必须正确实现资源的释放逻辑,以避免资源泄漏或其他错误。
  • 删除器的生命周期:自定义删除器的生命周期必须至少与 unique_ptr 的生命周期一样长。否则,如果删除器在 unique_ptr 之前被销毁,将导致未定义行为。
  • 避免重复释放:一旦 unique_ptr 被销毁并释放了资源,就不应再尝试访问或释放相同的资源。这可能导致双重释放错误。

5 std::unique_ptr 与数组

5.1 使用 std::unique_ptr 的一个特化版本(推荐)

对于动态分配的数组,不能简单地使用默认的 std::unique_ptr<T>,因为默认的删除器(delete)是用于单个对象的,而不是数组。为了管理数组,需要使用 std::unique_ptr 的一个特化版本,它接受一个指向数组的指针,并使用 delete[] 来释放数组。

特化版本的 std::unique_ptr 的声明如下:

template< class T, class Deleter = std::default_delete<T[]> >  
class unique_ptr<T[]>;

可以通过传递一个指向数组的指针来构造这样的 unique_ptr:

std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5}); // 创建一个指向 int 数组的 unique_ptr  
// 当 arr 离开作用域时,它所指向的 int 数组会被自动删除[]

在这个例子中,std::unique_ptr<int[]> 使用 delete[] 来释放数组,这是正确的做法。

5.2 自定义删除器与数组

就像普通的 std::unique_ptr 一样,也可以为数组版本的 std::unique_ptr 提供自定义删除器。这在某些情况下可能很有用,比如需要使用自定义的内存管理策略时。

void customArrayDelete(int* ptr) {  
    // 自定义的数组删除逻辑  
    delete[] ptr;  
}  
  
std::unique_ptr<int[], decltype(&customArrayDelete)> arr(new int[5]{1, 2, 3, 4, 5}, customArrayDelete);

在这个例子中,传递了一个自定义的删除器 customArrayDelete 给 std::unique_ptr。当 arr 被销毁时,它会调用 customArrayDelete 来释放数组。

5.3 注意事项

  • 不要混用 new 和 new[]:当使用 new[] 分配数组时,确保使用 std::unique_ptr<T[]> 来管理它,而不是 std::unique_ptr<T>。反之亦然,如果使用 new 分配单个对象,那么应该使用 std::unique_ptr<T>。混用它们会导致未定义行为。
  • 避免使用原始指针与 unique_ptr 交互:一旦你将原始指针传递给 unique_ptr,就不要再尝试使用那个原始指针了。unique_ptr 在其析构函数中会删除它所指向的对象或数组,再次使用原始指针会导致双重释放或其他未定义行为。
  • 不要复制 unique_ptr:unique_ptr 是不可复制的,这是因为它表示独占所有权。如果需要复制智能指针,那么应该考虑使用 std::shared_ptr。
  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值