unique_ptr

unique_ptr相较于shared_ptr来说要简单很多,它是独占型的指针,不允许拷贝和赋值操作,只能够进行移动。

基本使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 构造
unique_ptr<A> a(new A());
unique_ptr<A> a = make_unique<A>();
// unique_ptr可以管理数组,下面语句是合法的,创建有20个int的数组
unique_ptr<int[]> a = make_unique<int[]>(20);
// 注意,unique_ptr可以被多次make_unique赋值
// 每次赋值后,都会析构之前的资源,类似于 a.reset(p) 这种
auto a = make_unique<int>(10);
a = make_unique<int>(20); // 这句会析构10,之后管理20

// 获取原生指针,用于传参使用,注意不要delete get返回的指针,会二次析构
a.get();
// 不再管理指针,注意并不会释放内存
auto ptr = a.release();
// 析构之前的资源
a.reset();
// or
a = nullptr;
// 析构之前的资源,并管理P的资源
a.reset(new P());

// 通过移动语义转移所有权,之后a为nullptr
auto ptr2 = std::move(a);

删除器

unique_ptr与shared_ptr定义删除器方法不同。unique_ptr定义的方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void deleteObj(A* obj)
{
    if (obj)
    {
        printf("will delete obj\n");
        delete obj;
    }
}

int main()
{
    // 这里需要在模板参数中指定删除器的类型
    unique_ptr<A, decltype(deleteObj)*> b(new A(1, 2, 3), deleteObj);
 
    return 0;
}

更为详细的方法可以参考链接unique_ptr指定删除器

删除器原理了解

下面阅读了unique_ptr代码删除器方面的相关内容,主要看它是如何析构的。这里的代码实现以vs2015的为例,其他vs版本应该差不多,不过其他平台的话,估计会有些差别。

看代码,unique_ptr实际继承了_Unique_ptr_base,注意_Dx模版参数,在未指定的情况下,这个_Dx实际上就是默认删除器(看注释后面的= default_delete,STL对其进行重命名了)。

1
2
3
4
5
template<class _Ty,
    class _Dx>    // = default_delete<_Ty>
    class unique_ptr
        : public _Unique_ptr_base<_Ty, _Dx>
    {    // non-copyable pointer to an object

在构造函数中,使用参数初始化了父类_Mybase(typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;),有两个版本的构造函数,分别是只传递指针和同时传递指针以及删除器的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
explicit unique_ptr(pointer _Ptr) _NOEXCEPT
    : _Mybase(_Ptr)
    {    // construct with pointer
    static_assert(!is_pointer<_Dx>::value,
        "unique_ptr constructed with null deleter pointer");
    }

unique_ptr(pointer _Ptr,
    typename _If<is_reference<_Dx>::value, _Dx,
        const typename remove_reference<_Dx>::type&>::type _Dt) _NOEXCEPT
    : _Mybase(_Ptr, _Dt)
    {    // construct with pointer and (maybe const) deleter&
    }

在_Unique_ptr_base类的构造函数中,会使用参数用来初始化_Compressed_pair成员变量,_Compressed_pair类有两个版本,分别对应默认删除器和使用自定义删除器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

template<class _Ptr2,
    class _Dx2>
    _Unique_ptr_base(_Ptr2 _Ptr, _Dx2&& _Dt)
    : _Mypair(_One_then_variadic_args_t(), _STD forward<_Dx2>(_Dt), _Ptr)
    {    // construct with compatible pointer and deleter
    }

template<class _Ptr2>
    constexpr _Unique_ptr_base(_Ptr2 _Ptr)
    : _Mypair(_Zero_then_variadic_args_t(), _Ptr)
    {    // construct with compatible pointer
    }
    
_Compressed_pair<_Dx, pointer> _Mypair;

在使用默认删除器的版本中,其只有一个成员变量,即原始指针,同时这个_Compressed_pair类会继承默认删除器,默认删除器是default_delete,default_delete也有两个版本,分别是针对数组的删除器和针对普通变量的删除器。

1
2
3
4
5
6
7
8
9
template<class _Ty1,
    class _Ty2,
    bool = is_empty<_Ty1>::value && !is_final<_Ty1>::value>
    class _Compressed_pair final
        : private _Ty1

    {    // store a pair of values, deriving from empty first
private:
    _Ty2 _Myval2;

default_delete的实现也很简单,代码如下:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
template<class _Ty>
    struct default_delete
    {    // default deleter for unique_ptr
    constexpr default_delete() _NOEXCEPT = default;

    template<class _Ty2,
        class = typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,
            void>::type>
        default_delete(const default_delete<_Ty2>&) _NOEXCEPT
        {    // construct from another default_delete
        }

    void operator()(_Ty *_Ptr) const _NOEXCEPT
        {    // delete a pointer
        static_assert(0 < sizeof (_Ty),
            "can't delete an incomplete type");
        delete _Ptr;
        }
    };

template<class _Ty>
    struct default_delete<_Ty[]>
    {    // default deleter for unique_ptr to array of unknown size
    constexpr default_delete() _NOEXCEPT = default;

    template<class _Uty,
        class = typename enable_if<is_convertible<_Uty(*)[], _Ty(*)[]>::value,
            void>::type>
        default_delete(const default_delete<_Uty[]>&) _NOEXCEPT
        {    // construct from another default_delete
        }

    template<class _Uty,
        class = typename enable_if<is_convertible<_Uty(*)[], _Ty(*)[]>::value,
            void>::type>
        void operator()(_Uty *_Ptr) const _NOEXCEPT
        {    // delete a pointer
        static_assert(0 < sizeof (_Uty),
            "can't delete an incomplete type");
        delete[] _Ptr;
        }
    };

在使用默认删除器时,unique_ptr的析构函数中,会调用父类_Unique_ptr_base的get_deleter方法(_Compressed_pair成员的_Get_first)获取到对应删除器,之后调用其()重载。在_Compressed_pair成员的_Get_first函数中,返回的就是_Compressed_pair类自身,因为它继承了default_delete类,所以实际上返回的就是默认删除器。之后通过()重载释放内存。整个代码流程如下图所示。

1
2
3
4
5
    ~unique_ptr() _NOEXCEPT
        {    // destroy the object
        if (get() != pointer())
            this->get_deleter()(get());
        }

_Unique_ptr_base。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    const _Dx& get_deleter() const _NOEXCEPT
        {    // return const reference to deleter
        return (_Mypair._Get_first());
        }

    pointer& _Myptr() _NOEXCEPT
        {    // return reference to pointer
        return (_Mypair._Get_second());
        }

    const pointer& _Myptr() const _NOEXCEPT
        {    // return const reference to pointer
        return (_Mypair._Get_second());
        }

    _Compressed_pair<_Dx, pointer> _Mypair;

_Compressed_pair。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<class _Ty1,
    class _Ty2,
    bool = is_empty<_Ty1>::value && !is_final<_Ty1>::value>
    class _Compressed_pair final
        : private _Ty1

    _Ty1& _Get_first() _NOEXCEPT
        {    // return reference to first
        return (*this);
        }

在使用自定义删除器的_Compressed_pair版本中,其有两个成员变量,其中_Myval1则保存自定义删除器,_Myval2用来保存原始指针,所以其大小为2个原生指针大小。当析构时,依旧通过_Get_first方法获取自定义删除器,这里获取的就是_Myval1成员变量,之后通过()重载析构。

1
2
3
4
5
6
7
8
template<class _Ty1,
    class _Ty2>
    class _Compressed_pair<_Ty1, _Ty2, false> final

    {    // store a pair of values, not deriving from first
private:
    _Ty1 _Myval1;
    _Ty2 _Myval2;

删除器跨堆使用

在使用不同堆的模块间传递unique_ptr时,如果使用的是默认的删除器,那么相当于在当前代码中直接调用delete代码,在跨模块堆不一致的情况下,将会导致崩溃问题。

而采用自定义删除器时,unique_ptr中会保存删除器的指针,虽然两个模块间的堆不同,但在删除时,调用的还是原有模块,即原有堆的代码,因此可以保证内存正确释放。

shared_ptr与weak_ptr

基本使用方法

 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
28
29
30
31
32
33
// 构造类成员
shared_ptr<A> a(new A());
shared_ptr<A> b(a); 
// 上面语句执行后,对象的引用计数增1,即a、b的引用计数均为2

// 推荐的构造方法
shared_ptr<A> a = make_shared<A>();
// 有构造参数时,直接通过make_shared参数传递
shared_ptr<A> a = make_shared<A>(1, 2, 3);
// 对于数组,了解到C++标准还不支持,即使是new那种方式也不行,原因是其不支持delete[]删除方式
// 因此下面这个语句是错误的
shared_ptr<int[]> a = make_shared<int[]>(10);  // ×
// 若没有中括号,则只是一个值,参数中的值为指针的值
shared_ptr<int> a = make_shared<int>(20);

// 注意,shared_ptr可以被多次make_shared,
// 每次赋值后,之前资源的引用计数会减1,类似于 a.reset(new A()) 这种
auto a = make_shared<int>(10);
a = make_shared<int>(20); // 该句会给10的引用计数减1,之后管理20的内存

// 构造数组
shared_ptr<int[]> a(new int[10]));
// 这只是构造一个值
shared_ptr<int> a(new int(20));
// 通过reset方法不再管理资源,其引用计数会减一,如果减为0,那么会释放资源
a.reset();
// or
a.reset(new A());
// 除了使用reset方法外,也可以将nullptr赋给a
a = nullptr;

// 通过use_count()方法获取shared_ptr的引用计数
a.use_count()

关于shared_ptr指针与动态数组的关系可以参考这篇博客,其有较为详细的说明:

  1. shared_ptr和动态数组

而这些问题对于unique_ptr来说是没有问题的。

指定shared_ptr的删除器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void deleteObj(A* obj)
{
    if (obj) {
        printf("will delete obj\n");
        delete obj;
    }
}

shared_ptr<A> a(new A(), deleteObj);
// 当a析构时,将会自动调用deleteObj删除a
// 对于文件来说,也可以通过这种方式关闭文件
// 当然,也可以考虑使用lambda表达式
shared_ptr<A> a(new A(), [](A* a) { 
    if (a) 
        delete a; 
});

this指针传递

首先要明白,不能够把一个指针同时给多个shared_ptr,原因在于多个shared_ptr会重复对该指针进行析构,因此这点要切记。

但这个需求又是有必要的,在获取类的this指针时,比如下面这个场景,而这里使用shared_ptr又会有问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Y
{
public:
    std::shared_ptr<Y> f() {
        return std::shared_ptr<Y>(this);
    }

    ~Y()
    {
        printf("Y deconstruct\n");
    }
};

int main()
{
    // 这里明显有问题,对象y将会被释放两次
    shared_ptr<Y> ptr(new Y());
    auto ptr1 = ptr->f();
    return 0;
}

因此要用下面这个方式来解决。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意这个继承
class Y:public std::enable_shared_from_this<Y>
{
public:
    std::shared_ptr<Y> f() {
        // 注意返回时调用的是shared_from_this方法
        return shared_from_this();
    }

    ~Y()
    {
        printf("Y deconstruct\n");
    }
};

int main()
{
    shared_ptr<Y> ptr(new Y());
    auto ptr1 = ptr->f();
    return 0;
}

shared_ptr在析构虚析构函数的问题

一般情况下,当创建一个子类并将其指针赋给一个父类指针后,父类的析构一定得是虚析构函数,否则将会发生内存泄露问题。但在shared_ptr中,则不会发生这个问题。

详细的可先参见这两篇文章:

  1. 你不一定知道的智能指针细节
  2. 当虚析构函数遇上智能指针

shared_ptr在跨堆时的使用

在一个模块中申请的内存需要在另一个模块中释放,且另外一个模块的堆不同,那么将会发生崩溃。而shared_ptr则解决了这个问题,使用shared_ptr会调用原模块中对应的删除代码,保证了堆的一致。

这里看了下vs2015中shared_ptr的代码实现,了解其具体是如何实现的。

shared_ptr继承自_Ptr_base类,在_Ptr_base类中有两个成员,其中_Ptr就是原始指针,而_Rep则是指向一个_Ref_count_base对象的指针。

1
2
_Ty *_Ptr;
_Ref_count_base *_Rep;

在_Ref_count_base类中有两个成员变量,分别表示shared_ptr和weak_ptr的引用计数。

1
2
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;

从shared_ptr的析构函数开始看,当调用析构时,会调用_Ref_count_base类的_Decref方法,在该方法中,首先会使用_MT_DECR宏原子性的减小shared_ptr的引用计数,之后判断引用计数是否为0,如果为0,那么将会调用_Destroy和_Decwref方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
~shared_ptr() _NOEXCEPT
    {    // release resource
    this->_Decref();
    }

void shared_ptr::_Decref()
    {    // decrement reference count
    if (_Rep != 0)
        _Rep->_Decref();
    }
    
void _Ref_count_base::_Decref()
    {    // decrement use count
       // _MT_DECR宏会原子性的对引用计数减1
    if (_MT_DECR(_Uses) == 0)
        {    // destroy managed resource, decrement weak reference count
        _Destroy();
        _Decwref();
        }
    }

下面的代码中,_Ref_count_obj继承自_Ref_count_base。在_Destroy方法中,会调用_Getptr方法获取shared_ptr保存的指针,之后调用其析构函数。这里注意一点,在_Getptr函数中,返回的是_Storage的指针,这个_Storage是_Ref_count_obj的成员变量,在使用make_shared方法创建时,则会将T的整个内存都分配到_Storage中,从内存构造上来看,_Uses、_Weaks、_Storage这三者是连在一起的一片内存,降低内存分配次数。

1
2
3
4
5
6
7
8
9
_Ty *_Ref_count_obj::_Getptr() const
    {    // get pointer
    return ((_Ty *)&_Storage);
    }

virtual void _Ref_count_obj::_Destroy() _NOEXCEPT
    {    // destroy managed resource
    _Getptr()->~_Ty();
    }

在_Decwref方法中,当_Weaks变量为0时,将会调用_Delete_this()方法,在该方法中,会delete this,这会将控制块,也即存储_Uses、_Weaks、_Storage这3者的内存释放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void _Ref_count_base::_Decwref()
    {    // decrement weak reference count
    if (_MT_DECR(_Weaks) == 0)
        _Delete_this();
    }
    
virtual void _Ref_count_obj::_Delete_this() _NOEXCEPT
    {    // destroy self
    delete this;
    }

上面的过程都是使用make_shared方法创建的指针的析构流程,而对于使用shared_ptr(new)的形式创建的指针来说,其内部的_Ref_count_base指针指向的则是_Ref_count类,该类同样继承自_Ref_count_base类,_Ref_count类的实现非常简单,如下代码所示。其只保存了原始的指针,当析构时调用_Destroy函数时,直接对原始指针调用delete方法。而在_Decwref方法中,将只对控制块,即保存_Uses、_Weaks变量的内存进行释放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<class _Ty>
    class _Ref_count
    : public _Ref_count_base
    {    // handle reference counting for object without deleter
public:
    _Ref_count(_Ty *_Px)
        : _Ref_count_base(), _Ptr(_Px)
        {    // construct
        }

private:
    virtual void _Destroy() _NOEXCEPT
        {    // destroy managed resource
        delete _Ptr;
        }

    virtual void _Delete_this() _NOEXCEPT
        {    // destroy self
        delete this;
        }

    _Ty * _Ptr;
    };

总的来说,当shared_ptr析构时,都未在当前模块中调用delete代码,而是先在定义了shared_ptr的模块中调用了_Decref方法,并在其中调用delete的代码,由于是在定义shared_ptr的模块中delete,这样便不会出现两个模块堆不一致的问题。

weak_ptr

weak_ptr与shared_ptr配合使用,可将shared_ptr指针赋给weak_ptr,但weak_ptr并不会增加引用计数,同时weak_ptr也没有*、->方法供使用。但是可以通过weak_ptr的lock方法获取到一个shared_ptr,同时增加引用计数,当然通过lock方法获取成功的前提是资源还未释放,若资源已经被释放,那么lock拿到的将是空指针。

示例程序如下:

 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
28
29
30
31
32
33
34
35
36
37
38
39
int main()
{
    shared_ptr<A> a = make_shared<A>(1, 2, 3);
    weak_ptr<A> w = a;

    printf("%d\n", a.use_count());
    auto p = w.lock();
    printf("%d\n", a.use_count());
    if (p)
        printf("ok\n");
    else
        printf("error\n");

    printf("======================\n");

    auto aa = make_shared<A>(1, 2, 3);
    weak_ptr<A> bb(aa);
    printf("%d\n", aa.use_count());
    aa.reset();
    auto pp = bb.lock();
    if (pp)
        printf("ok\n");
    else
        printf("error\n");

    return 0;
}

//运行结果为:
A construct 1 2 3
// 1
// 2
// ok
// ======================
// A construct 1 2 3
// 1
// A deconstruct
// error
// A deconstruct

借助weak_ptr不增加引用计数的特性,便可以解决循环引用问题。

另外需要注意到,在上面的shared_from_this函数中使用的也是weak_ptr,通过lock方法获取一个shared_ptr指针并返回。

循环引用

在下面这个程序中,若父子两个类中使用的都是shared_ptr,那么这两个类最终都会无法被析构释放资源,原因在于相互引用导致引用计数均不为0。但可以将任意一个类的成员指针换成weak_ptr,在使用的时候将weak_ptr转为shared_ptr进行使用,在函数返回时,引用计数减1,对外来说,引用计数不变,从而在最后可以使得引用计数仅为1,从而释放资源。

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
class Parent;
typedef std::shared_ptr<Parent> ParentPtr;
typedef std::weak_ptr<Parent> WeakParentPtr;
class Child {
public:
    WeakParentPtr father;
    ~Child() {
        std::cout << "bye child" << std::endl;
    }

    void do_things()
    {
        cout << "child do things" << endl;
    }
};

typedef std::shared_ptr<Child> ChildPtr;
typedef std::weak_ptr<Child> WeakChildPtr;
class Parent {
public:
    WeakChildPtr son;
    //ChildPtr son;
    ~Parent() {
        std::cout << "bye parent" << std::endl;
    }

    void do_things()
    {
        auto p = son.lock();
        if (p)
            p->do_things();
        cout << "in do count " << p.use_count() << endl;
    }
};

void testParentAndChild() {
    ParentPtr p(new Parent());  // 1  资源A
    ChildPtr c(new Child());  // 2   资源B
    p->son = c;     // 3      c.use_count() == 2 and p.use_count() == 1
    c->father = p;    //  4   c.use_count() == 2 and p.use_count() == 1

    cout << "c count" << c.use_count() << endl;
    p->do_things();
    cout << "c count" << c.use_count() << endl;
}

int main()
{
    testParentAndChild();

    return 0;
}

参考文章

  1. [c++11]智能指针学习笔记
  2. STL中的智能指针(Smart Pointer)及其源码剖析: std::unique_ptr
  3. C++11 智能指针实现分析
  4. c++ unique_ptr源代码分析(from visual studio 2017)