• C++智能指针(三)——unique_ptr初探


    共享指针shared_ptr用于共享对象的目的不同,unique_ptr是用于独享对象。


    1. unqiue_ptr的目的

    首先,如果我们在函数中使用普通指针,会有许多问题,比如下面的函数 void f()

    void f()
    {
    	ClassA* ptr = new ClassA; // create an object explicitly
    	... // perform some operations
    	delete ptr; // clean up(destroy the object explicitly)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    抛开使用普通指针容易忘记使用 delete 导致内存泄漏不谈,如果我们在执行一些操作的时候,出现错误了,那么也会导致内存泄漏。

    那么就要引入异常处理操作,比如 try...catch...,那么程序会看起来冗余且复杂。

    之前已经讨论过智能指针中的两个 shared_ptrweak_ptr 带来的便捷性,而与共享对象的场景不同,unique_ptr 主要用于独享对象所有权的场景,即程序只有一个unique_ptr拥有该对象的所有权,只能转移所有权,不能共享所有权。与 shared_ptr 没有拥有该对象的智能指针后,释放资源与空间。

    比如我们用 unique_ptr 改写上面的代码:

    // header file for unique_ptr
    #include 
    void f()
    {
    	// create and initialize an unique_ptr
    	std::unique<ClassA> ptr(new ClassA);
    	... // perform some operations
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    that’s all,这样我们就省去了delete 与异常处理部分,是不是非常滴nice~


    2. 使用 unique_ptr

    2.1 初始化 unique_ptr

    unique_ptr 不允许使用赋值语法去使用一个普通指针初始化,只能直接将值代入构造函数进行初始化:

    std::unique_ptr<int> up = new int; // ERROR
    std::unique_ptr<int> up(new int); // OK
    
    • 1
    • 2

    当然,一个unique_ptr可以不用一定拥有一个对象,即可以是空的。那就使用默认构造函数进行初始化:

    std::unique_ptr<std::string> up;
    
    • 1

    可以使用 nullptr 赋值,或调用 reset(),这是等价的:

    up = nullptr;
    up.reset();
    
    • 1
    • 2

    2.2 访问数据

    与普通指针类似,可以使用 * 解引用出指向对象,也可以使用 -> 访问一个对象的成员:

    // create and initialize (pointer to) string:
    std::unique_ptr<std::string> up(new std::string("nico"));
    (*up)[0] = ’N’; // replace first character
    up->append("lai"); // append some characters
    std::cout << *up << std::endl; // print whole string
    
    • 1
    • 2
    • 3
    • 4
    • 5

    unique_ptr 不允许指针算术运算,比如 ++,这可以看作是 unique_ptr 的优势,因为指针算术运算容易出错。

    如果想在访问数据前,检查一个 unique_ptr 是否拥有一个对象,可以调用操作符 bool()

    if (up) { // if up is not empty
    std::cout << *up << std::endl;
    }
    
    • 1
    • 2
    • 3

    当然也可以将 unique_ptr 与 nullptr 比较,或者将 get() 得到的原生指针与 nullptr 比较,以达到检查的目的:

    if (up != nullptr) // if up is not empty
    if (up.get() != nullptr) // if up is not empty
    
    • 1
    • 2

    2.3 作为类的成员

    我们将 unique_ptr 应用到类,也可以避免内存泄漏。并且,如果使用 unique_ptr 而不是普通指针,类将不需要析构器,因为对象删除时,其成员也会被自动删除。

    除此之外,正常情况下,析构器只有在构造函数完成的情况下才会调用。如果在一个构造函数内部出现异常,析构器只会给那些完全完成构造的对象调用(所以下面的ClassB在构造函数中出现异常时,此时不会调用析构函数)。这就会导致如果使用原生指针,那么构造函数中第一个new成功,第二个new失败就会产生内存泄漏:

    class ClassB {
    private:
    ClassA* ptr1; // pointer members
    ClassA* ptr2;
    public:
    // constructor that initializes the pointers
    // - will cause resource leak if second new throws
    ClassB (int val1, int val2)
    : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
    }
    // copy constructor
    // - might cause resource leak if second new throws
    ClassB (const ClassB& x)
    : ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
    }
    // assignment operator
    const ClassB& operator= (const ClassB& x) {
    *ptr1 = *x.ptr1;
    *ptr2 = *x.ptr2;
    return *this;
    }
    ~ClassB () {
    delete ptr1;
    delete ptr2;
    }
    ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可以使用 unique_ptr 替换原生指针来避免上述情况:

    class ClassB {
    private:
    std::unique_ptr<ClassA> ptr1; // unique_ptr members
    std::unique_ptr<ClassA> ptr2;
    public:
    // constructor that initializes the unique_ptrs
    // - no resource leak possible
    ClassB (int val1, int val2)
    : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
    }
    // copy constructor
    // - no resource leak possible
    ClassB (const ClassB& x)
    : ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
    }
    // assignment operator
    const ClassB& operator= (const ClassB& x) {
    *ptr1 = *x.ptr1;
    *ptr2 = *x.ptr2;
    return *this;
    }
    // no destructor necessary
    // (default destructor lets ptr1 and ptr2 delete their objects)
    ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    需要注意,如果不提供拷贝和赋值函数,对于默认的拷贝和赋值拷贝构造函数,显然是不可能的(不可能用一个独享指针拷贝构造一个独享指针),所以,默认只提供移动构造函数。

    2.4 处理数组

    默认情况下,unique_ptrs 在失去对象的所有权之后,调用 delete,但对于数组,应该是调用 delete[],所以下面的代码是可以编译的,但是有运行时错误的:

    std::unique_ptr<std::string> up(new std::string[10]); // runtime ERROR
    
    • 1

    但其实我们不必像 shared_ptr 一样,在这种情况下,要通过自定义 deleter 来实现 delete[]。C++标准库已经对 unique_ptr 处理数组进行了特化,所以仅需要声明如下就可以:

    std::unique_ptr<std::string[]> up(new std::string[10]); // OK
    
    • 1

    但是需要注意的是,这个特化会导致,我们不能使用 *-> 访问数组,而是要使用 [],这其实就和我们正常访问数组是一样的了:

    std::unique_ptr<std::string[]> up(new std::string[10]); // OK
    ...
    std::cout << *up << std::endl; // ERROR: * not defined for arrays
    std::cout << up[0] << std::endl; // OK
    
    • 1
    • 2
    • 3
    • 4

    :最后需要注意,索引的合法性是编码人员需要保证的,错误的索引会带来未定义的结果。


    3. 转移所有权

    3.1 简单语法

    根据前面的讲解,我们知道需要保证没有两个unique_ptrs使用同一个指针初始化:

    std::string* sp = new std::string("hello");
    std::unique_ptr<std::string> up1(sp);
    std::unique_ptr<std::string> up2(sp); // ERROR: up1 and up2 own same data
    
    • 1
    • 2
    • 3

    因为只能独有,不能共享,所以肯定也无法进行一般的拷贝和赋值操作。但C++11有了新的语法——移动语义,这可以让我们使用构造函数和赋值操作来在unique_ptrs中转移对象所有权:

    // initialize a unique_ptr with a new object
    std::unique_ptr<ClassA> up1(new ClassA);
    // copy the unique_ptr
    std::unique_ptr<ClassA> up2(up1); // ERROR: not possible
    // transfer ownership of the unique_ptr
    std::unique_ptr<ClassA> up3(std::move(up1)); // OK
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面是使用移动构造函数,下面使用赋值运算符有类似表现:

    // initialize a unique_ptr with a new object
    std::unique_ptr<ClassA> up1(new ClassA);
    std::unique_ptr<ClassA> up2; // create another unique_ptr
    up2 = up1; // ERROR: not possible
    up2 = std::move(up1); // assign the unique_ptr
    // - transfers ownership from up1 to up2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当然,如果up2之前拥有另一个对象的所有权,那么在将up1的所有权转移给up2之后,up2之前拥有的对象就会被释放:

    // initialize a unique_ptr with a new object
    std::unique_ptr<ClassA> up1(new ClassA);
    // initialize another unique_ptr with a new object
    std::unique_ptr<ClassA> up2(new ClassA);
    up2 = std::move(up1); // move assign the unique_ptr
    // - delete object owned by up2
    // - transfer ownership from up1 to up2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,不能是将普通指针赋值给 unique_ptr,我们可以构造一个新的 unique_ptr来赋值:

    std::unique_ptr<ClassA> ptr; // create a unique_ptr
    ptr = new ClassA; // ERROR
    ptr = std::unique_ptr<ClassA>(new ClassA); // OK, delete old object
    // and own new
    
    • 1
    • 2
    • 3
    • 4

    一个特殊的语法,我们可以使用 release()unique_ptr 拥有的对象所有权还给普通指针,当然也可以用于创建新的智能指针:

    std::unique_ptr<std::string> up(new std::string("nico"));
    ...
    std::string* sp = up.release(); // up loses ownership
    
    • 1
    • 2
    • 3

    3.2 函数间转移所有权

    分为将所有权转入函数体内,以及从函数内转移出所有权两种情况。

    3.2.1 转移至函数体内

    下面的代码中,我们使用 sink(std::move(up)) 将函数体外部的up的所有权转移至函数 sink 内:

    void sink(std::unique_ptr<ClassA> up) // sink() gets ownership
    {
    ...
    }
    std::unique_ptr<ClassA> up(new ClassA);
    ...
    sink(std::move(up)); // up loses ownership
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.2.2 转移出函数体

    比如下面的代码中,source() 返回一个 unique_ptr,我们用 p 接收,就能获取对应对象的所有权:

    std::unique_ptr<ClassA> source()
    {
    	std::unique_ptr<ClassA> ptr(new ClassA); // ptr owns the new object
    	...
    	return ptr; // transfer ownership to calling function
    }
    void g()
    {
    	std::unique_ptr<ClassA> p;
    	for (int i=0; i<10; ++i) {
    	p = source(); // p gets ownership of the returned object
    	// (previously returned object of f() gets deleted)
    	...
    	}
    } // last-owned object of p gets deleted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    当然,每一次获取新的对象的所有权,都会把老对象给释放掉,在 g() 函数结束,也会释放最后获得的对象。

    这里,不需要再 source() 函数中,使用移动语义,是因为根据C++11语法规则,编译器会自动尝试进行一个移动


    4. Deleter

    unique_ptr<> 针对初始指针引用对象的类别以及deleter的类型进行模板化:

    namespace std {
    template <typename T, typename D = default_delete<T>>
    class unique_ptr
    {
    public:
    typedef ... pointer; // may be D::pointer
    typedef T element_type;
    typedef D deleter_type;
    ...
    };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于数组的特化,其有相同的默认deleter,即 default_delete

    namespace std {
    template <typename T, typename D>
    class unique_ptr<T[], D>
    {
    public:
    	typedef ... pointer; // may be D::pointer
    	typedef T element_type;
    	typedef D deleter_type;
    	...
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.1 default_delete<>

    下面我们深入研究 unique_ptr 的声明:

    namespace std {
    // primary template:
    template <typename T, typename D = default_delete<T>>
    class unique_ptr
    {
    public:
    ...
    T& operator*() const;
    T* operator->() const noexcept;
    ...
    };
    // partial specialization for arrays:
    template<typename T, typename D>
    class unique_ptr<T[], D>
    {
    public:
    ...
    T& operator[](size_t i) const;
    ...
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    其中,std::default_delete<> 内容如下,对于 T 调用 delete,对于 T[],调用 delete[]

    namespace std {
    // primary template:
    template <typename T> class default_delete {
    public:
    void operator()(T* p) const; // calls delete p
    ...
    };
    // partial specialization for arrays:
    template <typename T> class default_delete<T[]> {
    public:
    void operator()(T* p) const; // calls delete[] p
    ...
    };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.2 其他相关资源的Deleters

    这个在 shared_ptr 的讲解中有提到,与 shared_ptr 一致在我们可以自定义 deleter,不同的是,shared_ptr 需要模板中提供deleter的类别。比如使用一个函数对象,传入类的名称:

    class ClassADeleter
    {
    public:
    void operator () (ClassA* p) {
    std::cout << "call delete for ClassA object" << std::endl;
    delete p;
    }
    };
    ...
    std::unique_ptr<ClassA,ClassADeleter> up(new ClassA());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再比如使用一个函数或lambda表达式,我们可以指定为类似 void(*)(T*)std::function 或使用 decltype

    std::unique_ptr<int,void(*)(int*)> up(new int[10],
    [](int* p) {
    ...
    delete[] p;
    }); // 1
    
    std::unique_ptr<int,std::function<void(int*)>> up(new int[10],
    [](int* p) {
    ...
    delete[] p;
    }); // 2
    
    auto l = [](int* p) {
    ...
    delete[] p;
    };
    std::unique_ptr<int,decltype(l)>> up(new int[10], l); // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    还有一个骚操作:使用别名模板以避免指定deleter的类型

    template <typename T>
    using uniquePtr = std::unique_ptr<T,void(*)(T*)>; // alias template
    ...
    uniquePtr<int> up(new int[10], [](int* p) { // used here
    ...
    delete[] p;
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5. unique_ptr与shared_ptr性能的简单分析

    shared_ptr 类是使用一种非侵入的方式实现的,意味着这个类管理的对象不需要满足一个特定的需求,比如必须一个公共的基类等。这带来的巨大优势就是这个共享指针可以被用于任意类型,包括基础数据类型。因而产生的代价是,shared_ptr 对象内部需要多个成员:

    • 一个指向引用对象的普通指针
    • 一个所有共享指针引用相同的对象的计数器
    • 由于weak_ptr的存在,需要另一个计数器

    因此,shared 和 weak 指针内部需要额外的helper对象,内部有指针引用它。这意味着一些特定的优化是不可能的(包括空基类优化,这允许消除任何内存开销)。

    unique_ptr 不需要这些开销。它的“智能”是基于特有的构造函数和析构函数,以及拷贝语义的去除。对于一个有着无状态的活空的deleter的unique指针,将会消耗与原生指针相同大小的内存。而且比起使用原生指针和进行手动delete,没有额外的运行时开销。

    但是,为了避免不必要开销的引入,你应该使用对于deleters使用函数对象(包括lambda表达式)以带来理想情况下0开销的最佳优化。


    6. 附录

    A. unique_ptr 操作表


    7. 参考文献

    《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

  • 相关阅读:
    USB驱动-debug
    从这几个关键功能,带您认真了解低代码的世界~
    java中的集合
    CSDN话题挑战赛第2期:[一起学Java]
    2023年系统规划与设计管理师-第三章信息技术服务知识
    DataBinding原理
    Qt信号和槽 定时器
    C++ 智能指针常用总结
    C语言游戏实战(4):人生重开模拟器
    hive acid及事务表踩坑学习实录
  • 原文地址:https://blog.csdn.net/weixin_44491423/article/details/133829043