智能指针

​ C++程序设计中,使用堆内存是很繁琐的操作——堆内存的申请和释放都需要程序员自己去管理。虽然说程序员自己管理内存可以提高程序的效率,但是整体来说程序员手动管理内存是比较麻烦的,而且容易出现内存泄漏,异常安全(如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏)等问题。而在C++11中引入了智能指针的概念

来管理堆内存。

​ 智能指针的实现采用了一种RAII(利用对象生命周期来控制程序资源)的技术对普通的指针进行封装,使得智能指针实际是一个对象,其行为表现的却像一个指针。也就是说,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。那么我们就不需要显式地去释放资源,且对象所需要的资源在生命周期内始终有效。

auto_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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
template<class T>
class AutoPtr
{
private:
T* _ptr;
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}

AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}

AutoPtr<T>& operator=(AutoPtr<T>& sp)
{
if(this != sp)
{
if(_ptr)
delete _ptr;
//转移资源到当前对象
_ptr = sp._ptr;
sp._ptr = nullptr;
}
}

~AutoPtr()
{
if(_ptr)
delete _ptr;
}

T& operator*()
{
return *_ptr;
}

T& operator->()
{
return _ptr;
}

T& Get()
{
return _ptr;
}

};

由以上代码可以看到,auto_ptr的实现是利用了管理权转移的思想——一旦发生拷贝,就将ap/sp中资源转移到当前对象中,然后令ap/sp与其所管理资源断开联系,这样就解决了一块空间被多个对象使用而造成程序奔溃问题。

但是, 通过实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题,于是,引出了一种资源管理权转移的方式

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
54
55
56
57
58
59
60
61
62
63
64
65
66
template<class T>
class Autoptr
{
public:
Autoptr(T* ptr=nullptr)
:_ptr(ptr)
,_owner(false)
{
if(_ptr)
_owner = true;
}

Autoptr(const Autoptr<T>& ap)
:_ptr(ap._ptr)
,_owner(ap._owner)
{
ap._owner = false;
}

Autoptr<T>& operator=(const Autoptr<T>& sp)
{
if(this !=&sp)
{
Test();
_ptr = sp._ptr;
_owner = sp._owner;
sp._owner = false;
}

return this;
}

~Autoptr()
{
Test();
}

T& operator*()
{
return *_ptr;
}

T& operator->()
{
return _ptr;
}

T& Get()
{
return _ptr;
}

private:
void Test()
{
if(_ptr && _owner)
{
delete _ptr;
_owner = false;
}
}

private:
T* _ptr;
mutable bool _owner;
};

unique_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
34
35
36
模拟实现:
template<calss T>
class Uniqueptr
{
public:
Uniqueptr(T* ptr = nullptr)
:_ptr(ptr)
{}

~Uniqueptr()
{
if(_ptr)
delete _ptr;
}

T* operator*()
{
return *_ptr;
}

T* operator->()
{
return _ptr;
}
private:
//C98格式
Uniqueptr(const Uniqueptr<T>& );
Uniqueptr<T> operator=(const Uniqueptr& );


//C11格式
Uniqueptr(const Uniqueptr<T>& ) = delete;
Uniqueptr<T> operator=(const Uniqueptr& ) = delete;
private:
T* _ptr;
};

shared_str:

在C++11中,引入了shared_str这一更靠谱且支持拷贝的智能指针。

shared_str的原理是:

  1. 通过引用计数的方式来实现多个shared_str对象之间共享资源。也就是说:shared_str在其内部,为每个资源都维护了一份计数,用来记录该资源被几个对象共享。
  2. 在对象被销毁时,就说明该对象不使用这一资源了,对象的引用计数减一。
  3. 当引用计数减为0时,说明自己是最后一个使用该资源的对象必须释放该资源。
  4. 如果不是0,说明该资源还被其他的对象使用着,就不能释放该资源,否则其他对象就会成为野指针

    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
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    //采用引用计数的方式
    //为了线程安全,需要上锁,但是可能会造成死锁(锁内调用的其他代码如果抛异常则会造成死锁),守卫锁,进入时创建对象,函数退出时自动销毁
    // 模拟实现一份简答的SharedPtr,了解原理
    #include <thread>
    #include <mutex>
    template <class T>
    class SharedPtr
    {
    public:
    SharedPtr(T* ptr = nullptr)
    : _ptr(ptr)
    , _pRefCount(new int(1))
    , _pMutex(new mutex)
    {
    // 如果是一个空指针对象,则引用计数给0
    if (_ptr == nullptr)
    *_pRefCount = 0;
    }
    ~SharedPtr()
    {
    Release();
    }

    SharedPtr(const SharedPtr<T>& sp)
    : _ptr(sp._ptr)
    , _pRefCount(sp._pRefCount)
    , _pMutex(sp._pMutex)
    {
    // 如果是一个空指针对象,则不加引用计数,否则才加引用计数
    if (_ptr)
    AddRefCount();
    }

    // sp1 = sp2
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
    //if (this != &sp)
    if (_ptr != sp._ptr)
    {
    // 释放管理的旧资源
    Release();
    // 共享管理新对象的资源,并增加引用计数
    _ptr = sp._ptr;
    _pRefCount = sp._pRefCount;
    _pMutex = sp._pMutex;
    if (_ptr)
    AddRefCount();
    }
    return *this;
    }

    T& operator*()
    {
    return *_ptr;
    }

    T* operator->()
    {
    return _ptr;
    }

    int UseCount()
    {
    return *_pRefCount;
    }

    T* Get()
    {
    return _ptr;
    }

    int AddRefCount()
    {
    // 加锁或者使用加1的原子操作
    _pMutex->lock();
    ++(*_pRefCount);
    _pMutex->unlock();
    return *_pRefCount;
    }

    int SubRefCount()
    {
    // 加锁或者使用减1的原子操作
    _pMutex->lock();
    --(*_pRefCount);
    _pMutex->unlock();
    return *_pRefCount;
    }

    private:
    void Release()
    {
    // 引用计数减1,如果减到0,则释放资源
    if (_ptr && SubRefCount() == 0)
    {
    delete _ptr;
    delete _pRefCount;
    }
    }

    private:
    int* _pRefCount; // 引用计数
    T* _ptr; // 指向管理资源的指针
    mutex* _pMutex; // 互斥锁
    };

    int main()
    {
    SharedPtr<int> sp1(new int(10));
    SharedPtr<int> sp2(sp1);
    *sp2 = 20;

    cout << sp1.UseCount() << endl;
    cout << sp2.UseCount() << endl;

    SharedPtr<int> sp3(new int(10));
    sp2 = sp3;
    cout << sp1.UseCount() << endl;
    cout << sp2.UseCount() << endl;
    cout << sp3.UseCount() << endl;

    sp1 = sp3;
    cout << sp1.UseCount() << endl;
    cout << sp2.UseCount() << endl;
    cout << sp3.UseCount() << endl;
    return 0;
    }

std::shared_str的线程安全问题:

通过以上的代码细心的同学就会发现了:在对引用计数进行加1,减1操作时我们对其进行了上锁操作。

那么为什么要对其进行上锁操作呢?细想一下其实也很简单:

1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。

2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题

std::shared_str的循环引用问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}

在上边代码上可以看出一个问题——循环引用,分析如下:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
    成员,所以这就叫循环引用,谁也不会释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
0%