浅析 C++ 智能指针

在 c 和 c++ 中,通过访问指针对象存储的地址,可以实现对内存的直接操作
但在实际工程中,由于复杂情况下意料外的程序跳转,程序很可能出现内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
int* p = new int(1);

//若干代码

if (p)
{
return;
}

//若干代码

delete p;

因此我们引入「智能指针」

智能指针的历史

智能指针是 RAII(Resource Acquistion Is Initialization)(资源分配就初始化)思想的产物
可以理解为:智能指针 = 指针 + RAII。

实际上, RAII 对于指针即为将指针封装为一个类,通过构造函数和析构函数管理指针,防止上述情况的内存泄漏。
一个典型的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}

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

private:
T* _ptr;
};

作为一个智能指针,上述代码并不合格,至少还应实现赋值等操作。

auto_ptr

第一个被实现的智能指针 auto_ptr 于c++98被引入,实现了 RAII,通过「管理权转移|实现拷贝构造、赋值。

但 auto_ptr 并不被建议使用,其通过直接将堆上数据地址转移到新指针地址的方式实现拷贝构造、赋值,此时误访问原指针时很容易引起程序的崩溃。
参考以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class T>
class auto_ptr
{
public:
//已省略不必要的代码
auto_ptr(auto_ptr<T> &a):_p(a._p)
{
a._p = NULL;
}

auto_ptr<T>& operator= (auto_ptr<T> a)
{
_ptr = a._ptr;
a._ptr = NULL;
return *this;
}

private:
T* _p;
}

针对上述缺陷, Boost 库引入 scoped_ptr ,后来也被 c++ 标准库(名为 unique )纳入。

scoped_ptr

scoped_ptr 通过限制拷贝构造和赋值运算的方式规避 auto_ptr 的缺陷。
具体:不对函数进行定义,使用 private 防止类外实现。

参考代码

1
2
3
4
5
6
7
8
9
10
template<class T>
class auto_ptr
{
public:
//已省略不必要的代码
private:
auto_ptr(auto_ptr<T> &a);
auto_ptr<T>& operator= (auto_ptr<T> a);
T* _p;
}

scoped_ptr 终究是一个先天不全的智能指针。
目前的最终版本是 shared_ptr & weak_ptr。

shared_ptr

shared_ptr 是目前使用最广泛的智能指针,随 Boost 引入并在 C++11 被加入标准库
通过「引用计数」 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
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
template<class T>
class shared_ptr
{
public:
friend class weak_ptr<T>; //随后会用到

shared_ptr(T* ptr = NULL):
_ptr(ptr)
,_refCount(int new(1)){}

shared_ptr(const shared_ptr<T> &p):
_ptr(p._ptr),
_refCount(p._refCount)
{
(*_refCount)++;
}

~shared_ptr()
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
}

shared_ptr<T>& operator= (const shared_ptr<T> &p) //重载赋值
{
if (_ptr != p._ptr)
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}

_ptr = p._ptr;
_refCount = p._refCount;
}
}

T& operator* (const shared_ptr<T> &p) //重载解引用
{
return *_ptr;
}

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

private:
T* _ptr;
int* _refCount;

循环引用问题

如下代码运行时,程序会出现无限循环问题,代码如下

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
template<class T>
class shared_ptr
{
//略
}

typedef shared_ptr<Node> test_ptr;
struct Node //链表节点的定义
{
dataType data;
test_ptr prev;
test_ptr next;
}


int main(void)
{
test_ptr p1(new Node);
test_ptr p2(new Node);

p1 -> next = p2;
p2 -> prev = p1;

return 0;
}

如下图所示,在 main 函数结束时,两个智能指针生命周期结束时会依次析构,但在在两者交叉相互指向时,会陷入死循环中:

分析过程容易发现:
main函数执行到末尾时,计数器对 p1, p2 的计数都是2

p2 结束生命周期时,需要先析构 p2-> prev( p1 的空间,计数-1),再析构 p2 本身的空间,计数-1.
p1 结束生命周期时,需要先析构 p1-> next( p2 的空间,计数器将置0,需要 delete p2 )

但delete p2 需要先析构 p2-> prev 即 p1,p1 的析构又需要先析构 p1-> next 即delete p2
故而程序死循环。

循环引用解决方案 —> weak_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class T>
class weak_ptr
{
public:
weak_ptr(T* p = NULL):
_ptr(p){}

weak_ptr(const shared_ptr<T> &p):
_ptr(p._ptr){}

weak_ptr<T>& operator= (const shared_ptr<T> &p)
{
_ptr = p._ptr;
return *this;
}

//.....

private:
T* _ptr;
}

此时,将循环引用问题中的节点定义中的智能指针从 shared_ptr 改为 weak_ptr 即可。

1
2
3
4
5
6
//...

//typedef shared_ptr<Node> test_ptr;
typedef weak_ptr<Node> test_ptr;

//...