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

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签