C++智能指针4——唯一指针unique_ptr详解-程序员宅基地

技术标签: C++  智能指针  # 智能指针  

目录

unique_ptr设计目标

使用unique_ptr

通过unique_ptr转移所有权

源和槽

unique_ptr作为类成员

处理数组

default_delete<>类

其他关联资源的删除器

unique_ptr详细信息


C++11标准库提供的唯一指针unique_ptr有助于避免发生异常时的资源泄漏。它实现了专有所有权的概念,这意味着它可以确保一个对象及其关联资源一次只能由一个指针"拥有''。当此所有者被销毁或变空或开始拥有另一个对象时,先前拥有的对象也将被销毁,所有相关资源都将被释放。

唯一指针unique_ptr继承了自动指针auto_ptr(该类最初是C++ 98引入的,但现在已弃用)。 唯一指针unique_ptr提供了一个简单明了的接口,与自动指针auto_ptr相比,它更不容易出错。

unique_ptr设计目标

函数通常以以下步骤运行:

  1. 获取一些资源;
  2. 执行一些操作;
  3. 释放获得的资源。

如果获取的资源已经绑定到本地对象,则在进入时获取的资源会在函数退出时自动释放,因为函数退出时调用了这些本地对象的析构函数。但是如果资源是手动获取的,并且没有绑定到任何对象,则必须手动释放它们。使用指针时通常会手动管理资源。

以这种方式使用指针的典型示例是使用new和delete创建和销毁对象:

void f()
{
    ClassA* ptr = new ClassA; //手动创建一个对象
    ... //执行一些操作
    delete ptr; //清理:手动销毁对象
}

上面代码的一个明显问题是,对象的销毁可能会被遗忘,尤其是在函数内部有return语句的情况下。 还有一种不太明显的危险就是可能发生的异常将导致函数立即退出,而不会调用末尾的delete语句,最终导致资源泄漏。

为了避免这种资源泄漏通常要求函数捕获所有异常。 例如:

void f()
{
    ClassA* ptr = new ClassA; //手动创建一个对象
    try {
        ... //执行一些操作
    }
    catch (...) { //处理异常
        delete ptr; //清理
        throw; //重新抛出异常
    }
    delete ptr; //正常退出时清理
}

为了在发生异常时正确处理此对象的删除,代码变得复杂和冗余。如果以这种方式处理第二个对象,或者使用了多个捕获子句,问题将变得更加严重。这不是一种好的编程风格,应避免使用,因为它很复杂且容易出错。

自动指针unique_ptr可以解决这个问题。只要自动指针本身被销毁,它就可以释放其指向的数据。此外,由于它是一个局部变量,所以退出函数时唯一指针会自动销毁,无论退出是正常的还是由于异常导致的。

unique_ptr是一个指针,作为它所引用的对象的唯一所有者。当对象的唯一指针unique_ptr被销毁时,对象将自动销毁。对意味唯一指针unique_ptr的要求是其对象只有一个所有者。

下面是前面的示例改为使用唯一指针unique_ptr的代码:

#include <memory>
void f()
{
    //创建并初始化一个unique_ptr指针
    std::unique<ClassA> ptr(new ClassA);
    ... //执行一些其他操作
}

这样修改之后就不需要删除语句和catch子句。

使用unique_ptr

唯一指针unique_ptr具有与普通指针几乎相同的接口;

//创建并初始化指向字符串的unique_ptr指针
std::unique_ptr<std::string> up(new std::string("Tom"));
(*up)[0] = ’C’; //替换第一个字母
up->append("ming"); //追加字符串
std::cout << *up << std::endl; //打印整个字符串

但唯一指针没有定义诸如++之类的指针算法(因为指针算法是麻烦的根源)。
注意,唯一指针unique_ptr不允许使用赋值语法进行初始化,而必须使用普通指针直接初始化:

std::unique_ptr<int> up = new int; //错误
std::unique_ptr<int> up(new int); //正确

唯一指针unique_ptr可以为空。例如使用默认构造函数初始化或用nullptr对唯一指针unique_ptr进行赋值或调用reset()::

std::unique_ptr<std::string> up;
up = nullptr;
up.reset();

另外,可以调用release()让唯一指针unique_ptr返回其拥有的对象,并放弃所有权,以便调用方对返回的对象负责:

std::unique_ptr<std::string> up(new std::string("Tom"));
...
std::string* sp = up.release(); //up失去拥有权

检查唯一指针unique_ptr是否拥有对象的一些方法:

  1. 调用操作符bool();
  2. 与nullptr进行比较;
  3. 查询唯一指针unique_ptr中的原始指针是否为空。
if (up) { //如果up不为空
    std::cout << *up << std::endl;
}
if (up != nullptr) //如果up不为空
if (up.get() != nullptr) //如果up不为空

通过唯一指针转移所有权

唯一指针unique_ptr提供排他的所有权语义,但由程序员确保同一指针不会初始化两个唯一指针unique_ptr:

std::string* sp = new std::string("hello");
std::unique_ptr<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp); //错误:up1和up2拥有相同的数据

不幸的是,这是一个运行时错误无法在编译时发现,因此需要依靠程序员自己避免这种错误。

使用普通的复制语义无法复制或给唯一指针unique_ptr赋值,但可以使用C++ 11提供的move语义。在这种情况下,构造函数或赋值运算符会将所有权转移到另一个唯一指针unique_ptr。

例如,考虑以下拷贝构造函数的用法:

//使用一个新对象初始化一个unique_ptr
std::unique_ptr<ClassA> up1(new ClassA);
//复制unique_ptr
std::unique_ptr<ClassA> up2(up1); //错误:编译不通过
//转移unique_ptr的所有权
std::unique_ptr<ClassA> up3(std::move(up1)); //正确

在第一条语句之后,up1拥有使用new运算符创建的对象。 第二条语句尝试调用拷贝构造函数将导致一个编译时错误,因为up2不能成为该对象的另一个所有者。第三条语句将所有权从up1转移到up3。 因此,之后up3拥有使用new创建的对象,而up1不再拥有该对象。

赋值运算符的行为类似:

//用新对象初始化一个unique_ptr
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2; //创建另一个unique_ptr
up2 = up1; //错误:编译通不过
up2 = std::move(up1); //将up1的所有权转移到up2

移动赋值将所有权从up1转移到up2。如果up2在分配前拥有一个对象,则对该对象调用delete:

std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2(new ClassA);
up2 = std::move(up1); //销毁up2关联的对象,将up1关联对象的所有权转移给up2

在没有获得新所有权的情况下失去对象所有权的唯一指针unique_ptr表示没有对象。

要将新值赋给唯一指针unique_ptr,该新值也必须是唯一指针unique_ptr而不能是普通指针:

std::unique_ptr<ClassA> ptr;
ptr = new ClassA; //错误
ptr = std::unique_ptr<ClassA>(new ClassA); //正确
up = nullptr; //正确,相当于调用reset()

源和槽

所有权转移意味着函数可以使用唯一指针unique_ptr将所有权转移给其他函数。 可以有两种使用方式:

  1. 函数可以充当数据的接收器——通过使用std::move()创建的右值引用将唯一指针unique_ptr作为参数传递给函数。 在这种情况下,被调用函数的参数将获得唯一指针unique_ptr的所有权。
    void sink(std::unique_ptr<ClassA> up) //形参up获得对象的所有权
    {
        ...
    }
    
    std::unique_ptr<ClassA> up(new ClassA);
    ...
    sink(std::move(up)); //up失去关联对象的所有权
    ...
    

     

  2. 函数可以充当数据源——返回唯一指针unique_ptr时,返回值的所有权将转移到调用上下文中。 以下示例显示了此技术:
    std::unique_ptr<ClassA> source()
    {
        std::unique_ptr<ClassA> ptr(new ClassA);
        ...
        return ptr; //将ptr关联对象的所有权转移给调用函数
    }
    
    void g()
    {
        std::unique_ptr<ClassA> p;
        for (int i=0; i<10; ++i) {
            p = source(); //p获得返回对象的所有权
        }
    }
    

     

每次调用source()时,它都会使用new创建一个对象,并将该对象及其所有权返回给调用者。将返回值赋给p会将所有权转移给p。

在第二遍及其他遍历循环中,对p的赋值将删除p先前拥有的对象。离开g(),从而销毁p,导致p拥有的最后一个对象的销毁。无论如何,都不会发生资源泄漏。即使抛出异常,拥有资源的任何unique_ptr也会确保删除该资源。

source()的return语句中不需要std::move()的原因是,根据C++ 11的语言规则,编译器将自动尝试移动。

unique_ptr作为类成员

通过在类中使用唯一指针unique_ptr指针可以避免资源泄漏。

如果使用唯一指针unique_ptr而不是普通的指针,则不再需要析构函数,因为对象会随着成员的删除而被删除。

此外,unique_ptr有助于避免对象初始化期间引发的异常引起的资源泄漏。因为只有在完成构造后才调用析构函数,所以如果构造函数内部发生异常,则仅针对已完全构造的对象调用析构函数。如果在构造过程中第一个new执行成功而第二个new没有成功,则可能导致具有多个原始指针的类的资源泄漏。

例如:

#include <iostream>
#include <string>
#include <memory>
#include <sstream>

using namespace std;

class ClassA {
public:
    ClassA(const string & sName, const string & sOwnerName, int nVal)
        : m_sName(sName), m_sOwnerName(sOwnerName)
    {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”开始构造" << endl;
        if (0 == nVal)
        {
            runtime_error oRtEx("值不能为0\n");
            throw oRtEx;
        } else {
            m_dVal = 1.0 / nVal;
        }
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”完成构造" << endl;
    }

    ClassA(const ClassA & o2BeCopy) {
        m_dVal = o2BeCopy.m_dVal;
        m_sName = o2BeCopy.m_sName;
        m_sOwnerName = o2BeCopy.m_sOwnerName;
    }

    ~ClassA() {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”析构" << endl;
    }

    void setOwnerName(const string & sOwnerName) { m_sOwnerName = sOwnerName; }

private:
    double m_dVal;
    string m_sName;
    string m_sOwnerName;
};

class ClassB {
public:
    //如果ptr2的初始化抛出异常将导致资源泄露
    ClassB (int nVal1, int nVal2, const string & sName)
    {
        cout << "名为“" << sName << "”的ClassB对象开始构造" << endl;
        m_ptr1 = new ClassA("m_ptr1", sName, nVal1);
        m_ptr2 = new ClassA("m_ptr2", sName, nVal2);
        m_sName = sName;
        cout << "名为“" << sName << "”的ClassB对象完成构造" << endl;
    }

    //拷贝构造
    //如果ptr2的初始化之前抛出异常将导致资源泄露
    ClassB (const ClassB& x)
    {
        cout << "名为“" << m_sName << "”的ClassB对象开始拷贝构造" << endl;
        m_ptr1 = new ClassA(*(x.m_ptr1));
        m_ptr1->setOwnerName("拷贝构造");
        ostringstream oss;
        oss << "名为“" << m_sName << "”的ClassB对象拷贝构造出现异常\n";
        runtime_error oRtEx(oss.str());
        throw oRtEx;
        m_ptr2 = new ClassA(*(x.m_ptr2));
        m_ptr2->setOwnerName("拷贝构造");
        cout << "名为“" << m_sName << "”的ClassB对象完成拷贝构造" << endl;
    }

    //赋值运算符
    const ClassB& operator= (const ClassB& x) {
        *m_ptr1 = *x.m_ptr1;
        *m_ptr2 = *x.m_ptr2;
        return *this;
    }

    ~ClassB () {
        cout << "名为“" << m_sName << "”的ClassB对象开始析构" << endl;
        delete m_ptr1;
        delete m_ptr2;
        cout << "名为“" << m_sName << "”的ClassB对象完成析构" << endl;
    }

private:
        ClassA* m_ptr1; //指针成员
        ClassA* m_ptr2;
        string m_sName;
};

int main()
{
    try {
        ClassB oB(1, 0, "oB");
    } catch (const exception & ex) {
        cout << "ClassB oB(1, 0)执行出现异常,具体原因:" << ex.what();
    }

    cout << "=====================" << endl;

    try {
        ClassB oB1(1, 2, "oB1");
        ClassB oB2(oB1);
    } catch (const exception & ex) {
        cout << "ClassB oB2(oB1)执行出现异常,具体原因:" << ex.what();
    }
    return 0;
}

运行结果如下:

名为“oB”的ClassB对象开始构造
“oB”的ClassA对象“m_ptr1”开始构造
“oB”的ClassA对象“m_ptr1”完成构造
“oB”的ClassA对象“m_ptr2”开始构造
ClassB oB(1, 0)执行出现异常,具体原因:值不能为0
=====================
名为“oB1”的ClassB对象开始构造
“oB1”的ClassA对象“m_ptr1”开始构造
“oB1”的ClassA对象“m_ptr1”完成构造
“oB1”的ClassA对象“m_ptr2”开始构造
“oB1”的ClassA对象“m_ptr2”完成构造
名为“oB1”的ClassB对象完成构造
名为“”的ClassB对象开始拷贝构造
名为“oB1”的ClassB对象开始析构
“oB1”的ClassA对象“m_ptr1”析构
“oB1”的ClassA对象“m_ptr2”析构
名为“oB1”的ClassB对象完成析构
ClassB oB2(oB1)执行出现异常,具体原因:名为“”的ClassB对象拷贝构造出现异常

为了避免这种可能的资源泄漏,可以使用唯一指针unique_ptr指针:

#include <iostream>
#include <string>
#include <memory>
#include <sstream>

using namespace std;

class ClassA {
public:
    ClassA(const string & sName, const string & sOwnerName, int nVal)
        : m_sName(sName), m_sOwnerName(sOwnerName)
    {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”开始构造" << endl;
        if (0 == nVal)
        {
            runtime_error oRtEx("值不能为0\n");
            throw oRtEx;
        } else {
            m_dVal = 1.0 / nVal;
        }
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”完成构造" << endl;
    }

    ClassA(const ClassA & o2BeCopy) {
        m_dVal = o2BeCopy.m_dVal;
        m_sName = o2BeCopy.m_sName;
        m_sOwnerName = o2BeCopy.m_sOwnerName;
    }

    ~ClassA() {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”析构" << endl;
    }

    void setOwnerName(const string & sOwnerName) { m_sOwnerName = sOwnerName; }

private:
    double m_dVal;
    string m_sName;
    string m_sOwnerName;
};

class ClassB {
public:
    //如果ptr2的初始化抛出异常将导致资源泄露
    ClassB (int nVal1, int nVal2, const string & sName)
    {
        cout << "名为“" << sName << "”的ClassB对象开始构造" << endl;
        m_ptr1 = unique_ptr<ClassA>(new ClassA("m_ptr1", sName, nVal1));
        m_ptr2 = unique_ptr<ClassA>(new ClassA("m_ptr2", sName, nVal2));
        m_sName = sName;
        cout << "名为“" << sName << "”的ClassB对象完成构造" << endl;
    }

    //拷贝构造
    //如果ptr2的初始化之前抛出异常将导致资源泄露
    ClassB (const ClassB& x)
    {
        cout << "名为“" << m_sName << "”的ClassB对象开始拷贝构造" << endl;
        m_ptr1 = unique_ptr<ClassA>(new ClassA(*(x.m_ptr1)));
        m_ptr1->setOwnerName("拷贝构造");
        ostringstream oss;
        oss << "名为“" << m_sName << "”的ClassB对象拷贝构造出现异常\n";
        runtime_error oRtEx(oss.str());
        throw oRtEx;
        m_ptr2 = unique_ptr<ClassA>(new ClassA(*(x.m_ptr2)));
        m_ptr2->setOwnerName("拷贝构造");
        cout << "名为“" << m_sName << "”的ClassB对象完成拷贝构造" << endl;
    }

    //赋值运算符
    const ClassB& operator= (const ClassB& x) {
        *m_ptr1 = *x.m_ptr1;
        *m_ptr2 = *x.m_ptr2;
        return *this;
    }

private:
        unique_ptr<ClassA> m_ptr1; //指针成员
        unique_ptr<ClassA> m_ptr2;
        string m_sName;
};

int main()
{
    try {
        ClassB oB(1, 0, "oB");
    } catch (const exception & ex) {
        cout << "ClassB oB(1, 0)执行出现异常,具体原因:" << ex.what();
    }

    cout << "=====================" << endl;

    try {
        ClassB oB1(1, 2, "oB1");
        ClassB oB2(oB1);
    } catch (const exception & ex) {
        cout << "ClassB oB2(oB1)执行出现异常,具体原因:" << ex.what();
    }

    return 0;
}

运行结果如下:

名为“oB”的ClassB对象开始构造
“oB”的ClassA对象“m_ptr1”开始构造
“oB”的ClassA对象“m_ptr1”完成构造
“oB”的ClassA对象“m_ptr2”开始构造
“oB”的ClassA对象“m_ptr1”析构
ClassB oB(1, 0)执行出现异常,具体原因:值不能为0
=====================
名为“oB1”的ClassB对象开始构造
“oB1”的ClassA对象“m_ptr1”开始构造
“oB1”的ClassA对象“m_ptr1”完成构造
“oB1”的ClassA对象“m_ptr2”开始构造
“oB1”的ClassA对象“m_ptr2”完成构造
名为“oB1”的ClassB对象完成构造
名为“”的ClassB对象开始拷贝构造
“拷贝构造”的ClassA对象“m_ptr1”析构
“oB1”的ClassA对象“m_ptr2”析构
“oB1”的ClassA对象“m_ptr1”析构
ClassB oB2(oB1)执行出现异常,具体原因:名为“”的ClassB对象拷贝构造出现异常

现在可以不需要析构函数了,因为唯一指针unique_ptr会完成资源的释放。 不过使用了唯一指针unique_ptr之后就必须实现拷贝构造函数和重载赋值运算符,因为默认情况下两者都将尝试复制成员或为成员进行赋值,但由于唯一指针unique_ptr会让两者发生错误。 如果不提供它们,则ClassB也将仅提供移动语义。

处理数组

默认情况下,如果唯一指针失去所有权,则对其拥有的对象调用delete。 不幸的是,由于源于C的语言规则,C++无法区分指向一个对象的指针的类型和对象的数组的类型。 但是,根据数组的语言规则,必须调用运算符delete []而不是delete。 因此,以下代码可以编译通过,但会导致运行时错误:

std::unique_ptr<std::string> up(new std::string[10]); //运行时错误

幸运的是,C++标准库为唯一指针unique_ptr针对数组提供了特殊处理,当指针失去对所引用对象的所有权时,该类将对引用对象调用delete []。

因此,只需要声明:

std::unique_ptr<std::string[]> up(new std::string[10]);

注意,唯一指针unique_ptr针对数组的接口与针对普通指针的接口略有不同——不提供运算符*和->,而是提供运算符[]来访问引用数组内的对象:

std::unique_ptr<std::string[]> up(new std::string[10]);
...
std::cout << *up << std::endl; //错误:没有为数组定义*运算符
std::cout << up[0] << std::endl;

与普通数组的索引一样,由程序员来确保索引有效。使用无效的索引会导致未定义的行为。

还要注意,唯一指针unique_ptr不允许通过派生类型的数组进行初始化。 这反映了多态不适用于普通数组的事实。

default_delete<>类

让我们看一下unique_ptr类的声明。 从概念上讲,此类声明为以下内容:

namespace std {
    //初始模板
    template <typename T, typename D = default_delete<T>>
    class unique_ptr
    {
    public:
        ...
        T& operator*() const;
        T* operator->() const noexcept;
        ...
    };

    //针对数组进行偏特化
    template<typename T, typename D>
    class unique_ptr<T[], D>
    {
        public:
        ...
        T& operator[](size_t i) const;
        ...
    }
}

可以看到有一个特殊版本的唯一指针unique_ptr用于处理数组。 该版本提供了运算符[]而不是运算符*和->来处理数组而不是单个对象,但都使用类std :: default_delete<>作为删除器,它本身专门用于调用delete []而不是对数组的delete:

namespace std {
    //初始模板
    template <typename T>
    class default_delete 
    {
    public:
        void operator()(T* p) const; //调用delete p

        ...
    };

    //针对数组进行偏特化
    template <typename T> 
    class default_delete<T[]> 
    {
    public:
        void operator()(T* p) const; //调用delete[] p
        ...
    };
}

默认模板参数也自动适用于偏特化。

其他关联资源的删除器

当唯一指针unique_ptr引用的对象销毁时需要进行除delete或delete []之外的其他操作时,必须自定义删除器。 unique_ptr定义删除器的方法与shared_ptr略有不同——必须将删除器的类型指定为第二个模板参数。

该类型可以是对函数,函数指针或函数对象的引用。如果使用了函数对象,则应声明其“函数调用操作符”()以指向该对象的指针。

例如,以下代码在删除对象之前会打印一条附加消息:

#include <iostream>
#include <memory>

using namespace std;

class ClassA {};

class ClassADeleter
{
public:
    void operator () (ClassA* p) {
        cout << "调用ClassA对象的删除器" << std::endl;
        delete p;
    }
};

int main()
{
    unique_ptr<ClassA, ClassADeleter> oUp(new ClassA());
    return 0;
}

要指定函数或lambda表达式,必须将删除程序的类型声明为void(*)(T *)或std::function <void (T *)>或使用decltype。 例如,要将自定义删除器用于整数数组,其代码如下所示:

std::unique_ptr<int,void(*)(int*)> up(new int[10],
                                      [](int* p) {
                                          ...
                                          delete[] p;
                                      });


std::unique_ptr<int,std::function<void(int*)>> up(new int[10],
                                                  [](int* p) {
                                                      ...
                                                      delete[] p;
                                                  });

auto l = [](int* p) {
    ...
    delete[] p;
};
std::unique_ptr<int,decltype(l)>> up(new int[10], l);

为避免在传递函数指针或lambda时指定删除器的类型,还可以使用别名模板,这是C++ 11提供的一种语言功能:

template <typename T>
using uniquePtr = std::unique_ptr<T,void(*)(T*)>; //别名模板
...
uniquePtr<int> up(new int[10], [](int* p) {//此处使用
    ...
    delete[] p;
});

这样使用与shared_ptrs大致相同的接口来指定删除器。
这是使用自定义删除器的完整示例:

#include <iostream>
#include <string>
#include <memory>
#include <dirent.h>
#include <cstring>
#include <cerrno>

using namespace std;

class DirCloser
{
public:
    void operator () (DIR* dp) {
        if (closedir(dp) != 0) {
            std::cerr << "closedir()失败" << std::endl;
        }
    }
};

int main()
{
    //打开当前目录
    unique_ptr<DIR, DirCloser> pDir(opendir("."));
    //处理目录中每个项目(文件或目录)
    struct dirent *dp;
    while ((dp = readdir(pDir.get())) != nullptr) {
        string filename(dp->d_name);
        cout << "处理" << filename << endl;
    }
}

在main()内部使用opendir(),readdir()和closedir()的标准POSIX接口处理当前目录的条目。 为了确保在任何情况下都由closedir()关闭打开的目录,定义了一个unique_ptr,每当引用打开目录的句柄被销毁时,都会导致DirCloser被调用。 唯一指针的删除器可能不会抛出异常。 因此,仅打印错误消息。

使用unique_ptr的另一个优点是不可复制。 请注意,readdir()并非无状态,因此最好确保在使用句柄处理目录时,句柄的副本不能修改其状态。

如果您不想处理closedir()的返回值,也可以传递closedir()直接作为函数指针,指定删除器为函数指针。

unique_ptr<DIR,int(*)(DIR*)> pDir(opendir("."), closedir); //可能失效

注意以上代码不能保证可移植,因为closedir具有外部的“C”链接,因此在C++代码中,不能保证将其转换为int(*)(DIR *)。 对于可移植代码,需要定义下面的中间类型:

extern "C" typedef int(*DIRDeleter)(DIR*);
unique_ptr<DIR, DIRDeleter> pDir(opendir("."), closedir); //正确

closedir()返回一个int,因此必须指定int(*)(DIR *)作为Deleter的类型。 注意,通过函数指针进行的调用是间接调用,很难进行优化。

unique_ptr详细信息

unique_ptr具有专有所有权的概念——当其拥有排他控制权后,程序将无法创建多个unique_ptr拥有相同的关联对象。

unique_ptr的主要目标是确保在指针生命周期结束时删除关联的对象(或清理其资源)。这尤其有助于提供异常安全性。 与共享指针shared_ptr相反,此类的重点是最小的空间和时间开销。

对类unique_ptr<>进行模板化,以指定初始指针所指向的对象的类型及其变量:

namespace std {
    template <typename T, typename D = default_delete<T>>
    class unique_ptr
    {
    public:
        typedef ... pointer; //可能是D::pointer
        typedef T element_type;
        typedef D deleter_type;
        ...
    };
}

提供了数组的偏特化(根据语言规则,它具有相同的默认变量,即default_delete <T []>):

namespace std {
    template <typename T, typename D>
    class unique_ptr<T[], D>
    {
    public:
        typedef ... pointer; //可能是D::pointer
        typedef T element_type;
        typedef D deleter_type;
        ...
    };
}

元素类型T可能为void,因此unique_ptr拥有一个类型未指定的对象,就像void *。还要注意,虽然定义了pointer类型,但其不一定为T *。如果删除程序D定义了pointer类型,则将使用此类型。在这种情况下,模板参数T仅具有类型标签的作用,因为在类unique_ptr <>中没有成员依赖于T;一切都取决于pointer。这样做的优点是unique_ptr可以容纳其他智能指针。

下表列出了为唯一指针提供的所有操作。

操作 结果
unique_ptr<...> up 默认构造函数;使用默认/传递的删除器类型的实例作为删除器,创建一个空的唯一指针
unique_ptr<T> up(nullptr) 使用默认/传递的删除器类型的实例作为删除器,创建一个空的唯一指针
unique_ptr<...> up(ptr) 使用默认/传递的删除器类型的实例作为删除器,创建拥有* ptr的唯一指针
unique_ptr<...> up(ptr,del) 使用del作为删除器创建拥有* ptr的唯一指针
unique_ptr<T> up(move(up2)) 创建一个拥有up2先前拥有的指针的唯一指针(此后up2为空)
unique_ptr<T> up(move(ap)) 创建一个拥有先前由auto_ptr ap拥有的指针的唯一指针(此后ap为空)
up.~unique_ptr() 析构函数;调用拥有者对象的删除器
up = move(up2) 移动赋值(up2将所有权转移到up)
up = nullptr 调用拥有者对象的删除器,并使其为空(等同于up.reset())
up1.swap(up2) 交换up1和up2的指针和删除器
swap(up1,up2) 交换up1和up2的指针和删除器
up.reset() 调用拥有者对象的删除器,并使其为空(相当于up = nullptr)
up.reset(ptr) 调用拥有者对象的删除器,并将共享指针重新初始化为自己的* ptr
up.release() 将所有权放弃给调用者(不调用删除器就返回拥有的对象)
up.get() 返回存储的指针(拥有的对象的地址;如果没有,则返回nullptr)
*up 仅单个对象;返回拥有的对象(如果没有,则为未定义的行为)
up->... 仅单个对象;提供拥有对象的成员访问权限(如果没有,则为未定义的行为)
up[idx] 仅数组对象;返回具有存储数组的索引idx的元素(如果没有,则为未定义的行为)
if (up) 运算符bool();返回up是否为空
up1 == up2 对存储的指针调用==(可以为nullptr)
up1 != up2 对存储的指针调用!=(可以为nullptr)
up1 < up2 对存储的指针调用<(可以为nullptr)
up1 <= up2 对存储的指针调用<=(可以为nullptr)
up1 > up2 对存储的指针调用>(可以为nullptr)
up1 >= up2 对存储的指针调用>=(可以为nullptr)
up.get_deleter() 返回删除器的引用

对于不同类型,以指针和删除器为参数的构造函数已重载,因此指定了以下行为:

D d; //创建删除器对象
unique_ptr<int, D> p1(new int, D()); //D必须支持移动构造
unique_ptr<int, D> p2(new int, d); //D必须支持拷贝构造
unique_ptr<int, D&> p3(new int, d); //p3保持一个对d的引用
unique_ptr<int, const D&> p4(new int, D()); //错误:右值删除器对象不能具有引用删除器类型

对于单个对象,移动构造函数和赋值运算符是成员模板,因此可以进行类型转换。所有比较运算符都针对不同的元素和变量类型进行了模板化。

所有比较运算符都会调用unique_ptr指针内部使用的原始指针相应的比较运算符(相当于对get()返回的值调用相同的运算符)。它们都将nullptr作为参数进行重载,因此,您可以检查是否存在有效的指针,甚至可以检查原始指针是否小于或大于nullptr。

与单对象接口相比,数组类型的偏特化接口具有以下差异:

  • 提供了运算符[],而不是运算符*和->。
  • 默认删除程序调用delete [],而不是delete。
  • 不支持不同类型之间的转换。指向派生元素类型的指针尤其不可能。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xuyouqiang1987/article/details/104127669

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签