C++11 智能指针 unique_ptr

文章目录

  • 前言
  • 一、简介
    • 1.1 基本用法
    • 1.2 使用std::make_unique创建std::unique_ptr
      • 1.2.1 std::make_unique简介
      • 1.2.2 例程
    • 1.3 用作函数参数和返回值
      • 1.3.1 作为函数参数
      • 1.3.2 作为函数返回值
    • 1.4 定制删除器
      • 1.4.1 函数指针作为自定义删除器
      • 1.4.2 函数对象作为自定义删除器
      • 1.4.3 Lambda 表达式作为自定义删除器
      • 1.4.4 function 包装器作为自定义删除器
  • 二、原理
    • 2.1原理简介
    • 2.2源码分析
      • 2.2.1 默认构造函数
      • 2.2.2 自定义删除器的构造函数
      • 2.2.3 移动构造函数
      • 2.2.4 移动赋值运算符
      • 2.2.5 析构函数
      • 2.2.6 delete 拷贝函数
  • 总结
  • 参考资料

前言

C++ 中的智能指针是一种用于自动管理动态分配内存的指针类型,它们可以自动释放所管理的内存,避免了传统裸指针可能带来的内存泄漏、野指针等问题。C++ 标准库中提供了两种智能指针:std::unique_ptr 和 std::shared_ptr。

本文主要介绍 std::unique_ptr 智能指针。

一、简介

std::unique_ptr 是一个智能指针类模板,它提供了一种管理动态内存的方式,可以自动释放所持有的资源。与 std::shared_ptr 不同,unique_ptr 不能共享所有权,因此被称为“独占式智能指针”。它拥有所指向的对象的唯一所有权。当 std::unique_ptr 被销毁时,它会自动释放所管理的内存。std::unique_ptr 不支持拷贝构造函数和拷贝赋值运算符,因此不能共享所有权。当需要传递所有权时,可以使用 std::move 函数将 std::unique_ptr 转移给另一个 std::unique_ptr。

使用 unique_ptr,可以避免手动管理内存的问题,并且不需要显式地调用 delete 删除对象,因为 unique_ptr 对象会在离开作用域时自动销毁并释放它们所持有的指针。

1.1 基本用法

一个简单的例程:

#include 
#include 

void call_unique()
{
    // 创建一个 int 对象,并将其地址赋给 unique_ptr
    std::unique_ptr<int> p(new int(42)); // 创建一个 int 对象,并将其地址赋给 unique_ptr
    std::cout << *p << std::endl; 

    // 转移所有权
    std::unique_ptr<int> q = std::move(p); 
    std::cout << *q << std::endl; 

    // 此时 p 不再拥有所有权
    // 使用std::move函数后,p变成了一个空指针(nullptr),不能再使用它来访问所管理对象。
    if(p.get() == nullptr){
     std::cout << "p is nullptr_t" << std::endl;
    }

    // 当 q 是局部变量,离开作用域其被销毁时,它会自动释放所管理的内存 
}

int main() 
{
    call_unique();
    return 0;
}

使用std::unique_ptr管理数组:

#include 
#include 

int main() {
    std::unique_ptr<int[]> ptr(new int[3]); // 创建一个std::unique_ptr对象,管理一个int数组
    ptr[0] = 1;
    ptr[1] = 2;
    ptr[2] = 3;

    for (int i = 0; i < 3; ++i) {
        std::cout << "ptr[" << i << "] = " << ptr[i] << std::endl;
    }

    // 当ptr离开作用域时,数组会被自动释放
    return 0;
}

1.2 使用std::make_unique创建std::unique_ptr

1.2.1 std::make_unique简介

C++14开始将std::make_unique加入标准库。

  template<typename _Tp, typename... _Args>
    inline __detail::__unique_ptr_t<_Tp>
    make_unique(_Args&&... __args)
    { 
    	return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); 
    }

make_unique只是将它的参数完美转发到所要创建的对象的构造函数,从new产生的原始指针里面构造出std::unique_ptr,并返回这个std::unique_ptr。

在C++14中,引入了一个新函数std::make_unique,用于创建一个指向动态分配对象的std::unique_ptr。与手动创建std::unique_ptr对象相比,使用std::make_unique可以更简洁、更安全地创建对象,同时还可以获得更好的性能。

std::make_unique的使用方式与new运算符类似,但是它返回的是一个std::unique_ptr对象,而不是一个原始指针。下面是一个使用std::make_unique创建std::unique_ptr对象的例子:

std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(/* constructor arguments */);

在这个例子中,std::make_unique函数创建了一个MyClass对象,并返回一个std::unique_ptr对象来管理该对象。我们可以使用构造函数的参数列表来传递对象的构造参数。

需要注意的是,std::make_unique函数使用了完美转发,可以接受任意数量和类型的构造参数,并将它们转发到MyClass的构造函数中。这使得使用std::make_unique创建带参数的动态对象变得非常方便。

此外,std::make_unique还具有以下优点:
(1)异常安全性:如果在创建对象时抛出异常,std::make_unique会自动释放之前分配的内存,避免内存泄漏的问题。
(2)性能优化:std::make_unique通常比手动创建std::unique_ptr对象更快,因为它可以在单次内存分配中同时分配对象和控制块。
(3)更好的代码可读性:使用std::make_unique可以让代码更加简洁、易读,同时可以避免手动释放内存和错误的使用delete运算符等问题。

使用std::make_unique可以更加方便、安全、高效地创建std::unique_ptr对象。同时,通过使用完美转发,它还可以方便地创建带参数的动态对象。因此,在实际开发中,我们应该尽可能地使用std::make_unique来创建std::unique_ptr对象。

1.2.2 例程

在C++14中,std::make_unique可以用于创建指向动态分配数组的std::unique_ptr。和创建单个对象一样,使用std::make_unique创建动态数组可以更简洁、更安全地创建数组,并且可以获得更好的性能。

下面是一个使用std::make_unique创建动态数组的例子:

std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);

在这个例子中,std::make_unique函数创建了一个包含5个整数的数组,并返回一个std::unique_ptr对象来管理该数组。需要注意的是,std::make_unique函数的参数是数组的大小,而不是数组的元素个数。

在使用std::unique_ptr管理动态数组时,可以使用operator[]来访问数组的元素,就像使用原始指针一样。例如:

for (int i = 0; i < 5; ++i) {
    arr[i] = i;
    std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
}
#include 
#include 

int main() {
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);

    for (int i = 0; i < 5; ++i) {
        arr[i] = i;
        std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
    }

    return 0;
}

在这个例子中,我们使用std::make_unique创建一个包含5个整数的数组,并将其返回的std::unique_ptr对象赋给arr变量。然后,我们使用for循环对数组进行遍历,并打印出每个元素的值。

1.3 用作函数参数和返回值

std::unique_ptr可以用作函数参数和返回值,以提高代码的可读性和安全性。

1.3.1 作为函数参数

std::unique_ptr可以用作函数参数,以提高代码的可读性和安全性。作为函数参数时,std::unique_ptr的行为与其他类型的指针类似,但是它具有独占式所有权语义,可以确保对象在函数内部得到正确的处理,避免内存泄漏和重复释放内存的问题。

(1)
在将std::unique_ptr作为函数参数传递时,应该使用移动语义来转移所有权,以确保对象的所有权在函数内部得到正确的处理。例如:

void myFunc(std::unique_ptr<int> ptr) {
    // do something with the managed object
}

int main() {
    std::unique_ptr<int> ptr(new int(42));
    myFunc(std::move(ptr));
    return 0;
}

在这个例子中,我们定义了一个名为myFunc的函数,它的参数是一个std::unique_ptr。在main函数中,我们创建了一个std::unique_ptr对象来管理一个动态分配的int类型对象,并将其传递给myFunc函数。由于std::unique_ptr是独占式的,所以需要使用std::move函数来将ptr的所有权转移给myFunc函数的参数。在函数内部,我们可以安全地访问所管理的对象,并确保在函数返回时正确地释放对象的内存。

需要注意的是,在将std::unique_ptr作为函数参数传递时,应该使用移动语义来转移所有权,以确保对象的所有权在函数内部得到正确的处理。

(2)
如果函数不需要获取对象的所有权,可以将std::unique_ptr参数声明为常量引用,以提高代码的可读性和安全性。例如:

void myFunc(const std::unique_ptr<int>& ptr) {
    // do something with the managed object
}

int main() {
    std::unique_ptr<int> ptr(new int(42));
    myFunc(ptr);
    return 0;
}

在这个例子中,我们将std::unique_ptr参数声明为常量引用,并将ptr传递给myFunc函数。由于我们不需要获取对象的所有权,所以可以将std::unique_ptr参数声明为常量引用,以提高代码的可读性和安全性。

(3)
总之,std::unique_ptr可以用作函数参数,以提高代码的可读性和安全性。在使用时,需要注意使用移动语义来转移所有权,并确保对象的所有权在函数内部得到正确的处理,以避免内存泄漏和重复释放内存的问题。如果函数不需要获取对象的所有权,可以将std::unique_ptr参数声明为常量引用,以提高代码的可读性和安全性。

1.3.2 作为函数返回值

std::unique_ptr可以作为函数返回值,这是因为它具有独占式所有权语义。独占式所有权语义意味着一个std::unique_ptr对象拥有对它所管理的对象的唯一所有权,其他任何对象都不应该拥有对该对象的所有权。

当一个std::unique_ptr对象被返回时,它会使用移动语义将其所管理的对象的所有权转移给返回值。这意味着返回值中的std::unique_ptr对象将拥有对所管理的对象的唯一所有权,并且在返回值被销毁时,所管理的对象也将被正确地释放。
以下是一个示例,演示了如何在函数中返回std::unique_ptr对象:

#include 
#include 

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called\n";
    }

    ~MyClass() {
        std::cout << "MyClass destructor called\n";
    }

    void doSomething() {
        std::cout << "MyClass doing something\n";
    }
};

std::unique_ptr<MyClass> createMyClass() {
    return std::make_unique<MyClass>();
}

int main() {
    std::unique_ptr<MyClass> ptr = createMyClass();

    ptr->doSomething();
    //(*ptr).doSomething();

    return 0;
}

在这个例子中,我们定义了一个名为MyClass的类,它具有一个默认构造函数和一个doSomething函数。然后,我们定义了一个名为createMyClass的函数,它返回一个std::unique_ptr对象,该对象管理一个MyClass对象。

在main函数中,我们调用createMyClass函数创建一个MyClass对象,并将其返回的std::unique_ptr对象赋给ptr变量。然后,我们通过ptr对象调用doSomething函数,以演示我们可以安全地访问所管理的MyClass对象。

$ ./a.out 
MyClass constructor called
MyClass doing something
MyClass destructor called

这表明我们成功地创建了一个MyClass对象,并且在main函数结束时正确地释放了对象的内存。

需要注意的是,在将std::unique_ptr作为函数返回值传递时,应该使用移动语义来转移所有权,以确保对象的所有权在函数外部得到正确的处理。

在将std::unique_ptr作为函数返回值传递时,实际上是使用移动语义将所管理的对象的所有权从函数内部转移给返回值中的新的std::unique_ptr对象。因此,返回值中的std::unique_ptr对象将具有与原始对象相同的所有权和管理对象,并且在返回值对象被销毁时,所管理的对象也将被正确地释放。

总之,std::unique_ptr可以用作函数返回值,是因为它具有移动语义。使用移动语义可以避免拷贝或赋值操作的额外开销和潜在的错误,并确保对象在返回值对象被销毁时得到正确的处理。

1.4 定制删除器

当我们创建一个 std::unique_ptr 对象时,可以通过提供一个自定义删除器来控制它所管理的资源的释放方式。这个自定义删除器可以是函数指针、函数对象或者 lambda 表达式等可调用对象,它们在 std::unique_ptr 对象超出作用域时被调用,以释放关联的资源。

1.4.1 函数指针作为自定义删除器

如果使用函数指针作为 std::unique_ptr 的自定义删除器,那么函数指针应该指向一个能够正确释放资源的函数。函数指针应该具有与 std::unique_ptr 模板参数中指定的删除器类型相同的签名,以便在 std::unique_ptr 对象超出作用域时被调用。例如,如果 std::unique_ptr 的删除器类型为 void()(int),那么函数指针应该指向一个形如 void myDeleter(int*) 的函数。

例程如下:

#include 
#include 

class MyResource {
public:
    MyResource(int id) : id_(id) {}
    void print() const { std::cout << "id = " << id_ << std::endl; }
private:
    int id_;
};

void myDeleter(MyResource* ptr) {
    std::cout << "Deleting MyResource object with ";
    ptr->print();
    delete ptr;
}

int main() {
    // 使用函数指针作为自定义删除器
    std::unique_ptr<MyResource, void(*)(MyResource*)> ptr(new MyResource(42), myDeleter);
    ptr->print();

    return 0;
}

在这个例子中,我们首先定义了一个名为 MyResource 的类,它表示需要被管理的资源。然后我们定义了一个名为 myDeleter 的函数指针,它将在 std::unique_ptr 对象超出作用域时被调用,以释放关联的资源。myDeleter 函数指针应该指向一个能够正确释放资源的函数,其签名应该与 std::unique_ptr 模板参数中指定的删除器类型相同。

接下来,我们使用 std::unique_ptr 构造函数创建一个具有自定义删除器的 std::unique_ptr)(MyResource)> 对象 ptr,该对象将管理一个动态分配的 MyResource 对象,并设置 id_ 的值为 42。我们将 myDeleter 函数指针作为删除器传递给 std::unique_ptr 对象,以便在 std::unique_ptr 对象超出作用域时被调用。

最后,我们使用 -> 运算符访问 ptr 对象,并调用 print() 函数打印 id_ 的值。当 std::unique_ptr 对象 ptr 超出作用域时,myDeleter 函数指针将被调用来删除动态分配的 MyResource 对象,并打印一条消息到控制台。

需要注意的是,在使用函数指针作为自定义删除器时,我们需要在 std::unique_ptr 模板参数中指定删除器类型为 void()(MyResource),以便编译器能够正确推断删除器类型。

1.4.2 函数对象作为自定义删除器

函数对象是一个类对象,它重载了函数调用运算符 operator(),可以像函数一样被调用。如果使用函数对象作为 std::unique_ptr 的自定义删除器,那么函数对象应该定义一个 operator() 函数,以便在 std::unique_ptr 对象超出作用域时被调用。函数对象可以封装状态,从而允许自定义删除器适应更多的场景。

C++中的函数调用运算符(Function Call Operator)是一种特殊的运算符,它允许对象像函数一样被调用。函数调用运算符是一种重载运算符,它可以被用于自定义类型,使得该类型的对象可以像函数一样被调用。

函数调用运算符的重载使得对象可以像函数一样被调用,这为实现函数对象提供了一种简洁的方式。

下面是一个使用函数调用运算符来实现自定义删除器的std::make_unique的示例:

#include 
#include 

struct MyStruct {
    int x;
    ~MyStruct() { std::cout << "MyStruct destructor called\n"; }
};

struct MyDeleter {
	//
    void operator()(MyStruct* p) const {
        std::cout << "MyDeleter called\n";
        delete p;
    }
};

int main() {
    // 使用 std::unique_ptr 构造函数创建具有自定义删除器的 unique_ptr
    std::unique_ptr<MyStruct, MyDeleter> ptr(new MyStruct(), MyDeleter());
    ptr->x = 42;

    // 使用 unique_ptr
    std::cout << "x = " << ptr->x << std::endl;

    return 0;
}
$ ./a.out 
x = 42
MyDeleter called
MyStruct destructor called

在这个例子中,我们定义了一个MyStruct结构体,它是我们想要使用std::unique_ptr管理的对象类型。我们还定义了一个名为MyDeleter的自定义删除器,它是一个函数对象,当std::unique_ptr超出作用域时将被调用来删除对象。MyDeleter简单地在对象指针上调用delete,并打印一条消息到控制台。

然后,我们使用std::unique_ptr的构造函数创建一个具有自定义删除器的std::unique_ptr对象,该对象将管理一个动态分配的MyStruct对象。我们可以使用->运算符访问该对象,就像使用普通指针一样。

最后,在main函数中,我们演示了我们可以像使用普通指针一样使用std::unique_ptr对象。

备注:通过定制删除器,也可以使用unique_ptr来管理一些通过其他方式分配的资源,比如文件句柄、socket等。

1.4.3 Lambda 表达式作为自定义删除器

Lambda 表达式是一个匿名函数,可以用来实现简单的函数对象。如果使用 Lambda 表达式作为 std::unique_ptr 的自定义删除器,那么 Lambda 表达式应该定义一个参数,以便在 std::unique_ptr 对象超出作用域时被调用。Lambda 表达式可以捕获外部变量,从而允许自定义删除器适应更多的场景。

lambda 表达式可以作为一种简洁的方式来实现自定义删除器:

#include 
#include 

class MyClass {
public:
    MyClass(int x) : x_(x) {}
    void print() const { std::cout << "x = " << x_ << std::endl; }
private:
    int x_;
};

int main() {
    // 使用 lambda 表达式实现自定义删除器
    auto myDeleter = [](MyClass* ptr) {
        std::cout << "Deleting MyClass object with x = ";
        ptr->print();
        delete ptr;
    };
    
    std::unique_ptr<MyClass, decltype(myDeleter)> ptr(new MyClass(42), myDeleter);
    ptr->print();

    return 0;
}
$ ./a.out 
x = 42
Deleting MyClass object with x = 42
MyClass destructor called

这个例子使用了 lambda 表达式实现自定义删除器,并将其用于 std::unique_ptr 对象。lambda 表达式可以作为一种简洁的方式来实现自定义删除器。

在这个例子中,我们首先定义了一个 lambda 表达式 myDeleter,它是一个函数对象,当 std::unique_ptr 超出作用域时将被调用来删除 MyClass 对象,并打印一条消息到控制台。我们将 lambda 表达式的类型作为 std::unique_ptr 的第二个模板参数,以便告诉 std::unique_ptr 使用这个 lambda 表达式作为删除器。

然后,我们使用 std::unique_ptr 构造函数创建一个具有自定义删除器的 std::unique_ptr 对象 ptr,该对象将管理一个动态分配的 MyClass 对象,并设置 x_ 的值为 42。然后我们使用 -> 运算符访问该对象,并调用 print() 函数打印 x_ 的值。

最后,当 std::unique_ptr 对象超出作用域时,lambda 表达式将被调用来删除动态分配的 MyClass 对象。

1.4.4 function 包装器作为自定义删除器

可以使用 std::function 来实现具有自定义删除器的 std::unique_ptr 对象。std::function 是一个函数对象的封装器,可以用来存储和调用任何可调用对象,包括函数指针、函数对象和 lambda 表达式等。下面是一个使用 std::function 实现具有自定义删除器的 std::unique_ptr 对象的例子:

#include 
#include 
#include 

class MyClass {
public:
    MyClass(int x) : x_(x) {}
    void print() const { std::cout << "x = " << x_ << std::endl; }
private:
    int x_;
};

void myDeleter(MyClass* ptr) {
    std::cout << "Deleting MyClass object with ";
    ptr->print();
    delete ptr;
}

int main() {
    // 使用 std::function 封装自定义删除器函数
    std::function<void(MyClass*)> deleter = myDeleter;

    // 使用 std::unique_ptr 构造函数创建具有自定义删除器的 unique_ptr
    std::unique_ptr<MyClass, decltype(deleter)> ptr(new MyClass(42), deleter);
    ptr->print();

    return 0;
}

在这个例子中,我们首先定义了一个名为 myDeleter 的自定义删除器函数,它将删除 MyClass 对象并打印一条消息到控制台。然后,我们使用 std::function 将 myDeleter 函数封装为一个函数对象 deleter,并将其作为 std::unique_ptr 的删除器。

接下来,我们使用 std::unique_ptr 构造函数创建一个具有自定义删除器的 std::unique_ptr 对象 ptr,该对象将管理一个动态分配的 MyClass 对象,并设置 x_ 的值为 42。然后我们使用 -> 运算符访问该对象,并调用 print() 函数打印 x_ 的值。

最后,当 std::unique_ptr 对象超出作用域时,自定义删除器函数将被调用来删除动态分配的 MyClass 对象。

这个例子演示了如何使用 std::function 将自定义删除器函数封装为函数对象,并将其用于 std::unique_ptr 对象,以实现具有自定义删除器的 std::unique_ptr 对象。使用 std::function 可以使代码更加灵活,因为它可以封装任何可调用对象,包括函数指针、函数对象和 lambda 表达式等。这样,我们可以根据需要选择不同的自定义删除器,并将其传递给 std::unique_ptr 对象,以实现不同的对象管理行为。

二、原理

2.1原理简介

C++的unique_ptr是一个智能指针,用于管理动态分配的内存。它的原理是利用RAII(资源获取即初始化)的概念,在对象构造时获取资源,在对象析构时释放资源。

unique_ptr通过模板类实现,其模板参数指定了指针所指向的对象类型。unique_ptr拥有一个指向堆上分配内存的原始指针(raw pointer),并在析构时自动释放该指针所指向的内存。当unique_ptr被移动时,所有权也会被转移。

unique_ptr使用了禁止拷贝的技术(使用delete操作符来禁止拷贝构造函数和拷贝赋值运算符),以确保每个unique_ptr只能拥有一个指向对象的指针。这意味着,无法通过拷贝unique_ptr实例来共享指向同一对象的指针。这种限制确保了程序的安全性,防止了指向对象的多个指针被错误地释放或修改。

unique_ptr还提供了自定义删除器(deleter)的功能,可以在释放内存时调用自定义的删除函数。删除器可以是函数指针、函数对象或lambda表达式,用于执行特定的清理操作,例如释放内存、关闭文件等。

总之,unique_ptr的原理是通过RAII和禁止拷贝来确保以安全和高效的方式管理动态分配的内存,同时提供了自定义删除器的功能,使得unique_ptr更加灵活和适应不同的需求。

2.2源码分析

2.2.1 默认构造函数

std::unique_ptr<int> p(new int(42)); // 创建一个 int 对象,并将其地址赋给 unique_ptr
  /// A move-only smart pointer that manages unique ownership of a resource.
  /// @headerfile memory
  /// @since C++11
  template <typename _Tp, typename _Dp = default_delete<_Tp>>
    class unique_ptr
    {

      __uniq_ptr_data<_Tp, _Dp> _M_t;

    public:
      // Constructors.	

      /** Takes ownership of a pointer.
       *
       * @param __p  A pointer to an object of @c element_type
       *
       * The deleter will be value-initialized.
       */
      template<typename _Del = _Dp, typename = _DeleterConstraint<_Del>>
	_GLIBCXX23_CONSTEXPR
	explicit
	unique_ptr(pointer __p) noexcept
	: _M_t(__p)
        { }
	}

在构造函数中,首先使用一个指针__p作为参数来初始化_M_t成员变量,__p是指向动态分配内存的原始指针,表示unique_ptr即将拥有的指针。_M_t是unique_ptr的一个成员变量,它的类型是一个std::tuple,其中包含了一个指针成员和一个删除器成员。

由于这是一个默认构造函数,因此删除器会使用默认构造函数进行初始化。如果用户需要使用自定义删除器,可以在构造函数中使用一个额外的参数来指定。

需要注意的是,unique_ptr的构造函数是一个noexcept函数,并且使用了constexpr关键字来保证在编译时计算,从而提高了程序的性能和安全性。

2.2.2 自定义删除器的构造函数

std::unique_ptr<MyClass, decltype(myDeleter)> ptr(new MyClass(42), myDeleter);
      /** Takes ownership of a pointer.
       *
       * @param __p  A pointer to an object of @c element_type
       * @param __d  A reference to a deleter.
       *
       * The deleter will be initialized with @p __d
       */
      template<typename _Del = deleter_type,
	       typename = _Require<is_copy_constructible<_Del>>>
	_GLIBCXX23_CONSTEXPR
	unique_ptr(pointer __p, const deleter_type& __d) noexcept
	: _M_t(__p, __d) { }

这段代码是unique_ptr的带有自定义删除器的构造函数实现,它接收两个参数:一个指向动态分配内存的原始指针__p和一个自定义删除器__d。__p表示unique_ptr即将拥有的指针,__d表示unique_ptr所使用的自定义删除器。

在构造函数中,使用__p初始化_M_t成员变量,表示unique_ptr即将拥有该指针。同时,使用__d初始化_M_t成员变量中的删除器,表示unique_ptr所使用的删除器是用户自定义的。

这个构造函数还包含了一个约束条件typename = _Require>,它用于确保删除器类型是可复制的。这个约束条件使用了另外一个辅助模板类_Require,它的作用是在编译时检查模板参数是否满足特定的条件。在这个约束条件中,_Del必须是可复制构造的类型,因为unique_ptr需要在移动时使用删除器。如果删除器类型不满足这个条件,则编译器会将该构造函数从候选列表中排除,从而避免不正确的使用。

2.2.3 移动构造函数

      /// Move constructor.
      unique_ptr(unique_ptr&&) = default;

      /** @brief Converting constructor from another type
       *
       * Requires that the pointer owned by @p __u is convertible to the
       * type of pointer owned by this object, @p __u does not own an array,
       * and @p __u has a compatible deleter type.
       */
      template<typename _Up, typename _Ep, typename = _Require<
               __safe_conversion_up<_Up, _Ep>,
	       __conditional_t<is_reference<_Dp>::value,
			       is_same<_Ep, _Dp>,
			       is_convertible<_Ep, _Dp>>>>
	_GLIBCXX23_CONSTEXPR
	unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept
	: _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter()))
	{ }

这段代码是unique_ptr的移动构造函数和转换构造函数的实现。移动构造函数接收一个右值引用,使用默认的移动构造函数实现,即将_M_t成员变量使用std::move转移所有权,从而完成所有权的转移。

转换构造函数接收一个右值引用__u,并使用__u.release()释放该unique_ptr实例所拥有的指针,并获取其原始指针。同时,调用__u.get_deleter()获取其删除器,并使用std::forward<_Ep>(__u.get_deleter())将删除器转发给_M_t成员变量中的删除器。需要注意的是,这个转换构造函数会检查__u所拥有的指针是否可以转换为该unique_ptr实例所拥有的指针类型,并且__u不拥有一个数组,并且__u和该unique_ptr实例使用兼容的删除器类型。

在转换构造函数中,还使用了一些辅助模板类和函数来实现类型转换和类型检查。例如,__safe_conversion_up模板类用于检查是否可以将_Up类型的指针转换为_Ep类型的指针,它需要_Up类型的指针可以隐式转换为_Ep类型的指针。此外,还使用了is_reference和is_same等模板类和函数来检查删除器类型是否为引用类型以及__u和该unique_ptr实例是否使用相同的删除器类型。

总之,unique_ptr的移动构造函数和转换构造函数利用了C++11的移动语义和模板机制,为用户提供了一个灵活和可定制的接口。移动构造函数使用默认的移动构造函数实现,而转换构造函数则使用类型转换和类型检查来确保指针类型和删除器类型的正确性,从而避免不正确的使用。同时,通过RAII和noexcept机制,保证了以安全和高效的方式管理动态分配的内存。

2.2.4 移动赋值运算符

      // Assignment.

      /** @brief Move assignment operator.
       *
       * Invokes the deleter if this object owns a pointer.
       */
      unique_ptr& operator=(unique_ptr&&) = default;

      /** @brief Assignment from another type.
       *
       * @param __u  The object to transfer ownership from, which owns a
       *             convertible pointer to a non-array object.
       *
       * Invokes the deleter if this object owns a pointer.
       */
      template<typename _Up, typename _Ep>
	_GLIBCXX23_CONSTEXPR
        typename enable_if< __and_<
          __safe_conversion_up<_Up, _Ep>,
          is_assignable<deleter_type&, _Ep&&>
          >::value,
          unique_ptr&>::type
	operator=(unique_ptr<_Up, _Ep>&& __u) noexcept
	{
	  reset(__u.release());
	  get_deleter() = std::forward<_Ep>(__u.get_deleter());
	  return *this;
	}

这段代码是unique_ptr的移动赋值操作符和转换赋值操作符的实现。移动赋值操作符使用默认的移动赋值操作符实现,即将_M_t成员变量使用std::move转移所有权,从而完成所有权的转移。同时,如果该unique_ptr实例所拥有的指针不为空,则会调用其删除器来释放该指针。

转换赋值操作符接收一个右值引用__u,并使用__u.release()释放该unique_ptr实例所拥有的指针,并获取其原始指针。同时,调用__u.get_deleter()获取其删除器,并使用std::forward<_Ep>(__u.get_deleter())将删除器转发给该unique_ptr实例中的删除器。需要注意的是,这个转换赋值操作符会检查__u所拥有的指针是否可以转换为该unique_ptr实例所拥有的指针类型,并且__u所拥有的指针不是数组,并且__u和该unique_ptr实例使用兼容的删除器类型。

在转换赋值操作符中,使用了一些辅助模板类和函数来实现类型转换和类型检查。例如,__safe_conversion_up模板类用于检查是否可以将_Up类型的指针转换为_Ep类型的指针,它需要_Up类型的指针可以隐式转换为_Ep类型的指针。此外,还使用了is_assignable模板类来检查删除器类型是否可以从_Ep类型的右值引用进行赋值操作。

总之,unique_ptr的移动赋值操作符和转换赋值操作符利用了C++11的移动语义和模板机制,为用户提供了一个灵活和可定制的接口。移动赋值操作符使用默认的移动赋值操作符实现,而转换赋值操作符则使用类型转换和类型检查来确保指针类型和删除器类型的正确性,从而避免不正确的使用。同时,通过RAII和noexcept机制,保证了以安全和高效的方式管理动态分配的内存。

2.2.5 析构函数

      ~unique_ptr() noexcept
      {
	static_assert(__is_invocable<deleter_type&, pointer>::value,
		      "unique_ptr's deleter must be invocable with a pointer");
	auto& __ptr = _M_t._M_ptr();
	if (__ptr != nullptr)
	  get_deleter()(std::move(__ptr));
	__ptr = pointer();
      }

这段代码是unique_ptr的析构函数的实现。在析构函数中,首先使用static_assert检查删除器类型是否可以接受一个指针类型的参数,以确保在调用删除器时可以正确地释放资源。

在析构函数中,首先获取_M_t中的指针成员,即该unique_ptr实例所拥有的指针。如果该指针不为空,则调用get_deleter()获取其删除器,并使用该删除器释放该指针。在释放指针后,将_M_t中的指针成员设置为nullptr,以确保该unique_ptr实例不再拥有该指针。

需要注意的是,析构函数是一个noexcept函数,表示它不会抛出异常。这种设计有助于提高程序的性能和安全性,因为它可以使编译器在编译时进行优化,从而减少程序的运行时开销。此外,noexcept还有助于提高程序的可靠性,因为它可以使程序更容易进行错误处理和调试。

总之,unique_ptr的析构函数利用了RAII和noexcept机制,保证了以安全和高效的方式管理动态分配的内存。在析构函数中,使用删除器释放该unique_ptr实例所拥有的指针,并将指针成员设置为nullptr,以确保该unique_ptr实例不再拥有该指针。同时,使用static_assert检查删除器类型是否可以接受指针类型的参数,以确保在调用删除器时可以正确地释放资源。

2.2.6 delete 拷贝函数

      // Disable copy from lvalue.
      unique_ptr(const unique_ptr&) = delete;
      unique_ptr& operator=(const unique_ptr&) = delete;

这段代码是unique_ptr的禁止拷贝构造函数和禁止拷贝赋值操作符的实现。通过将拷贝构造函数和拷贝赋值操作符声明为delete,可以禁止使用拷贝语义来管理unique_ptr实例所拥有的指针。这是因为unique_ptr实例具有独占所有权,即同一时刻只能有一个unique_ptr实例拥有该指针。如果允许拷贝构造函数和拷贝赋值操作符,则可能会导致多个unique_ptr实例同时拥有同一个指针,从而出现不正确的使用。

需要注意的是,通过将拷贝构造函数和拷贝赋值操作符声明为delete,可以确保在编译时就可以捕获到这种错误的使用,而不是在运行时出现问题。同时,禁止拷贝语义也有助于提高程序性能和安全性,因为它可以使unique_ptr实例的所有权更加清晰和明确,从而避免不必要的内存泄漏或悬挂指针等问题。

总之,禁止拷贝构造函数和拷贝赋值操作符是unique_ptr的一个重要设计特点,它体现了C++11的移动语义和独占所有权的概念。禁止拷贝语义可以确保unique_ptr实例具有唯一的所有权,从而避免出现不正确的使用。

总结

C++11中引入了unique_ptr智能指针,它是一个轻量级的RAII(资源获取即初始化)类模板,用于管理动态分配的内存。unique_ptr实例拥有独占所有权,即同一时刻只能有一个unique_ptr实例拥有该指针。使用unique_ptr可以避免内存泄漏、悬挂指针、重复释放等常见的动态内存管理问题,从而提高程序的可靠性和安全性。

unique_ptr的特点包括:

(1)独占所有权:每个unique_ptr实例只能拥有一个指针,其它unique_ptr实例不能共享该指针。

(2)移动语义:unique_ptr支持移动语义,即可以将所有权从一个unique_ptr实例转移给另一个unique_ptr实例,而无需进行复制和分配新的内存。

(3)自定义删除器:unique_ptr支持自定义删除器,即可以使用用户提供的函数或函数对象来释放指针所占用的内存。

(4)禁止拷贝语义:unique_ptr禁止拷贝构造函数和拷贝赋值操作符,只支持移动构造函数和移动赋值操作符,从而确保每个unique_ptr实例具有唯一的所有权。

其他特性:

(1)模板参数:unique_ptr是一个模板类,需要指定指针类型和删除器类型作为模板参数。删除器类型可以是可调用对象,或者是持有可调用对象的类,例如函数指针、lambda表达式、自定义的函数对象等。

(2)make_unique:C++14中引入了make_unique工厂函数,可以方便地创建unique_ptr实例并初始化指针所指向的对象,避免了使用new运算符手动分配内存的繁琐操作。

(3)nullptr:unique_ptr支持nullptr,即可以使用nullptr来初始化或重置unique_ptr实例,这样可以避免使用空指针或未初始化的指针。

(4)自动类型推导:C++14中引入了auto关键字的变体auto&&,可以用于推导unique_ptr的类型,从而避免手动指定模板参数。

(5)与STL容器的配合:unique_ptr可以与STL容器(例如vector、list、map等)配合使用,从而实现动态分配的对象的管理。在STL容器中,unique_ptr通常用于实现容器元素的动态分配和释放,避免了手动管理指针的麻烦和安全风险。

参考资料

Chatgpt

https://cplusplus.com/reference/memory/unique_ptr/
https://blog.csdn.net/moter/article/details/121075200

你可能感兴趣的:(modern,C++,c++,开发语言)