C++智能指针:原理与实现

文章目录
  1. 1. 裸指针有什么问题?
  2. 2. 秘技:引用计数
  3. 3. 智能指针设计
    1. 3.1. 构造函数
    2. 3.2. 析构函数
    3. 3.3. 引用计数管理函数
    4. 3.4. 操作符重载
  4. 4. 测试
  5. 5. 其它讨论
    1. 5.1. weak_ptr
    2. 5.2. unique_ptr
  6. 6. 写在后面

若问起 Java 与 C++ 在使用体验上的差别,许多人都会提到「垃圾回收」这个词,Java 自带垃圾回收机制,而 C++ 没有。同样是面向对象的程序语言,这一点上的设计理念却如此大相径庭。有个古老的段子说,Java 的设计者认为:“内存管理这么重要的事情怎么能交给愚蠢的程序员呢!”,而 C++ 设计者认为:“内存管理这么重要的事情怎么能交给愚蠢的机器呢!”二者好像都有些道理。

C/C++ 的指针把计算机底层暴露给了程序员,在使其变得灵活强大的同时也增加了程序员的负担,特别是对初学者来说,内存忘记释放、野指针满天飞都是常见问题,而且没那么容易克服。即使是对有经验的程序员来说,随着项目规模变大,资源管理也会成为一个令人头疼的问题。

为了解决内存泄漏的难题,C++ 11 标准引入了三种智能指针:std::shared_ptrstd::unique_ptrstd::weak_ptr。但只会用是不足够的,这篇文章就来说说智能指针的基本思想与实现方法。注意,文章中的代码自然与 std 的实现有所不同,旨在抓住思想本质,免去为了适应特殊情况而引入的更多细节。若实际项目中真遇到那些所谓「特殊情况」,则仔细研读 std 的代码是很好的方案。

裸指针有什么问题?

首先要明确的是我们究竟想要解决什么问题。在 C++ 中通过 new 关键字在堆上申请空间后,除非使用 delete 关键字释放这块资源,否则这块资源对系统来说就是被占用的,直到整个程序结束才会被回收。即使当前程序已经不再需要这块资源,或者已经丧失了对这块资源的控制,也是如此。一个简单的例子:

1
2
for(int i=0; i<100; i++)
char* p = new char[100];

这段代码如果出现在程序中,运行完毕之后计算机可用内存会减少 10000 个字节,这 10000 个字节就像凭空蒸发了一样,既无法在本程序的其它地方使用(p 的作用域仅限 for 循环内),也无法被其它程序使用(系统不会回收这块内存),这就是内存泄漏(memory leak)。如果你的程序多次运行了上面的代码,只要次数够多,迟早耗尽物理内存导致崩溃。这是忘了写 delete 的情况,还有一种情况:

1
2
3
4
5
6
7
int* p = new int[100];
try {
// 这里抛出了异常
} catch (...) {
// 这里程序结束
}
delete []p;

上面的例子中,由于抛出了异常而进入了 catch 块,程序无法执行后面代码而导致了内存泄漏。这就是异常不安全的问题。

分析问题需抓住本质。内存泄漏的根本原因是:指针变量与其指向的资源不一定会同时被释放。比如上面的例子,指针变量 p 离开 for 语句块则立即被释放,但是指向的资源却没有释放,这就导致没有指针是指向那块资源的,也就是程序丧失了对资源的控制。

从指针的行为来理解,这是理所当然的:如果有多个指针指向同一块资源,不可能其中一个指针变量释放了就去把资源也释放掉,因为如果这样的话,下面这段代码就会变得莫名其妙了:

1
2
3
4
5
char* getArray(int n) {
char *p = new char[n];
return p;
}
char* pArray = getArray(100); // 申请 100 个字节的资源

若指针行为真变成前面所说,即指针变量总是与指向资源同时释放,那么上面这段代码显然是错误的:p 在离开 getArray 函数后就被释放了,对应的那 100 字节也被释放掉,那么 pArray 就根本没用了。

由此可见指针与其指向的资源需要保持一定的独立性,不能做过分严格的绑定。那么如何做内存管理?

聪明的程序员们发现,虽然指针不应该与其对应资源同生同死,但是反过来想有一点是明确的:任何一块 new 出来的内存资源,如果没有任何指针指向它,那么它肯定是没用的,应该被释放掉。这就是智能指针的设计基础。

秘技:引用计数

如果 C++ 足够聪明,每块 new 出来的资源都知道去数一数有多少个指针变量指向它,并且在没有指针指向它的时候释放资源就好了。可惜 C++ 没有那么聪明,此事需要程序员自己动手。

Idea 已经有了,也就是对每一块 new 出来的资源都维护一个计数,保存所有指向它的指针数量,这个数就叫做引用计数。新增指针指向这块资源,引用计数自增;指针释放或者改去指向别的地方,引用计数自减。当引用计数减到零,就去释放资源。具有这个性质的指针即可称为「智能指针」。

智能指针设计

智能指针肯定不是一个简单结构,它至少要有以下性质:

  1. 包含两个信息:引用计数与所指向资源的裸指针
  2. 表现出指针的行为,对程序员来说它是透明的
  3. 对所有的数据类型都适用

这些需求决定智能指针一定是类的对象,并且为了让它适用于所有类型,它还得是模板类的对象。基本的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
class SmartPointer
{
public:
SmartPointer();
~SmartPointer();
// other methods
// ...
private:
T* m_Pointer; // 指向资源的指针
int* m_RefPointer; // 指向引用计数的指针
}

由于每块资源都只对应一个引用计数,因此智能指针中的引用计数要存储为其指针,这样每个智能指针都能操作这内存中唯一的一个引用计数。

现在要做的是围绕这个结构,完善其成员函数。

构造函数

通常,我们至少需要实现这几种构造函数:默认构造函数;从对象指针构造;从另一智能指针构造。为了方便使用,可再实现一种静态方法,用于替代 C++ 的 new 关键字,使封装性更高。

默认构造函数

1
2
3
template<typename T>
SmartPointer<T>::SmartPointer()
: m_Pointer(nullptr), m_RefPointer(nullptr){}

默认的,不指向任何资源,不存在引用计数。

从对象指针构造

1
2
3
4
5
6
template<typename T>
SmartPointer<T>::SmartPointer(T* target)
:m_RefPointer(nullptr), m_Pointer(target)
{
addReference();
}

从对象指针构造时,初始化成员指针为对象指针,并初始化引用计数为 1。这里特别需要注意,由代码实现可见,引用计数实际上保存的是指向资源的智能指针数,并不是所有指针数。这个问题的原因以及带来的问题及解决方法后面会提到。

从另一智能指针构造

1
2
3
4
5
6
template<typename T>
SmartPointer<T>::SmartPointer(const SmartPointer<T>& from)
: m_RefPointer(from.m_RefPointer), m_Pointer(from.m_Pointer)
{
addReference();
}

从另一智能指针构造时,除了初始化成员变量,由于增加了新的智能指针指向对应资源,还应该增加引用计数。

静态 New 方法

这本身不属于构造函数之列,但是有必要单独说明。观察上文的「从对象指针构造」的构造函数,构造完成后引用计数是 1,而不是 2。这说明引用计数保存的是指向资源的智能指针数,并不包括裸指针。这是因为只有智能指针才能维护引用计数,因此要避免裸指针与智能指针的混用。因此一般推荐再多实现一个静态的 New() 方法,提高智能指针的封装程度,避免直接使用 C++ 的 new 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
// 声明
static T* New();

// 定义
template<typename T>
T* SmartPointer<T>::New()
{
return new T();
}

// 使用(这里需要重载 = 操作符,后文会说)
SmartPointer<T> pointerT = SmartPointer<T>::New();

上面第 11 行执行时会调用「从对象指针构造」的构造函数,但是并没有出现指向所开辟资源的裸指针,并且封装了 new 关键字,提高安全性的同时使智能指针的使用体验更统一。

析构函数

析构函数需要完成两件事:引用计数自减,若减至零,则释放资源。

1
2
3
4
5
template<typename T>
SmartPointer<T>::~SmartPointer()
{
removeReference();
}

引用计数管理函数

至少需要包括两个部分:增加引用计数与减少引用计数。

增加引用计数

1
2
3
4
5
6
7
8
template<typename T>
void SmartPointer<T>::addReference()
{
if (m_RefPointer)
(*m_RefPointer)++;
else
m_RefPointer = new int(1);
}

减少引用计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
void SmartPointer<T>::removeReference()
{
if (m_RefPointer)
{
(*m_RefPointer)--;
if (*m_RefPointer == 0) // 若引用计数归零
{
delete m_RefPointer; // 删除引用计数
delete m_Pointer; // 释放指向资源
m_RefPointer = nullptr;
m_Pointer = nullptr;
}
}
}

操作符重载

为了使智能指针表现如同普通指针,需要对某些操作符进行重载。

相等判断

判断两个智能指针是否相等,也即是否指向相同的资源。

1
2
3
4
5
template<typename T>
bool SmartPointer<T>::operator == (const SmartPointer<T>& other) const
{
return m_Pointer == other.m_Pointer;
}

对应的有不等判断:

1
2
3
4
5
template<typename T>
bool SmartPointer<T>::operator != (const SmartPointer<T>& other) const
{
return !operator==(other);
}

作为赋值语句的左值

此时代表本智能指针不再指向原来的资源了,需要将原本资源的引用计数减一,并对新指向资源的引用计数加一。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
SmartPointer<T>& SmartPointer<T>::operator = (const SmartPointer<T>& that)
{
if (this != &that) // 避免自己给自己赋值
{
removeReference();
this->m_Pointer = that.m_Pointer;
this->m_RefPointer = that.m_RefPointer;
addReference();
}
return *this;
}

解引用

重载 * 操作符,实现裸指针的 * 方法。

1
2
3
4
5
template<typename T>
T& SmartPointer<T>::operator*() const
{
return *m_Pointer;
}

公共成员调用方法

重载 -> 操作符。

1
2
3
4
5
template<typename T>
T* SmartPointer<T>::operator->() const
{
return m_Pointer;
}

测试

以上就是最基本的一个智能指针实现了,完整的代码可以查看:

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
128
129
130
131
132
133
#pragma once
/* 声明 */
template<typename T>
class SmartPointer
{
public:
SmartPointer(); // 默认构造函数
SmartPointer(T* target); // 从对象创建
SmartPointer(const SmartPointer<T>& from); // 从智能指针创建
~SmartPointer();

static T* New(); // 静态方法

// 操作符重载
bool operator==(const SmartPointer<T>& other) const;
bool operator!=(const SmartPointer<T>& other) const;
SmartPointer<T>& operator=(const SmartPointer<T>& that);
T& operator*() const;
T* operator->() const;

// 获取引用计数
int getRefCount();

protected:
void addReference();
void removeReference();

private:
T* m_Pointer;
int* m_RefPointer;
};


/* 实现 */
template<typename T>
SmartPointer<T>::SmartPointer()
: m_Pointer(nullptr), m_RefPointer(nullptr){}

template<typename T>
SmartPointer<T>::SmartPointer(const SmartPointer<T>& from)
: m_RefPointer(from.m_RefPointer), m_Pointer(from.m_Pointer)
{
addReference();
}

template<typename T>
SmartPointer<T>::SmartPointer(T* target)
:m_RefPointer(nullptr), m_Pointer(target)
{
addReference();
}

template<typename T>
SmartPointer<T>::~SmartPointer()
{
removeReference();
}

template<typename T>
T* SmartPointer<T>::New()
{
return new T();
}

template<typename T>
void SmartPointer<T>::addReference()
{
if (m_RefPointer)
(*m_RefPointer)++;
else
m_RefPointer = new int(1);
}

template<typename T>
void SmartPointer<T>::removeReference()
{
if (m_RefPointer)
{
(*m_RefPointer)--;
if (*m_RefPointer == 0) // 若引用计数归零
{
delete m_RefPointer; // 删除引用计数
delete m_Pointer; // 释放指向资源
m_RefPointer = nullptr;
m_Pointer = nullptr;
}
}
}

template<typename T>
bool SmartPointer<T>::operator == (const SmartPointer<T>& other) const
{
return m_Pointer == other.m_Pointer;
}

template<typename T>
bool SmartPointer<T>::operator != (const SmartPointer<T>& other) const
{
return !operator==(other);
}

template<typename T>
SmartPointer<T>& SmartPointer<T>::operator = (const SmartPointer<T>& that)
{
if (this != &that) // 避免自己给自己赋值
{
removeReference();
this->m_Pointer = that.m_Pointer;
this->m_RefPointer = that.m_RefPointer;
addReference();
}
return *this;
}

template<typename T>
T& SmartPointer<T>::operator*() const
{
return *m_Pointer;
}

template<typename T>
T* SmartPointer<T>::operator->() const
{
return m_Pointer;
}

template<typename T>
int SmartPointer<T>::getRefCount()
{
if (m_RefPointer)
return *m_RefPointer;
return -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
#include <iostream>
#include "SmartPointer.h"

using namespace std;

class A // a class for test
{
public:
A() { cout << "创建对象:" << this << endl; };
~A() { cout << "释放对象:" << this << endl; };

void Print()
{
cout << "智能指针对程序员是透明的,当前对象地址:" << this << endl;
}
};

int main()
{
SmartPointer<A> sp_outer;

{
cout << "开始一个作用域\n";
SmartPointer<A> sp_1 = SmartPointer<A>::New();
cout << "从对象新建智能指针后,引用计数:" << sp_1.getRefCount() << endl;

SmartPointer<A> sp_2 = sp_1;
cout << "从智能指针复制后,引用计数:" << sp_2.getRefCount() << endl;

sp_1 = SmartPointer<A>::New();
cout << "智能指针作为左值被赋值,原对象引用计数:" << sp_2.getRefCount()
<< ",新对象引用计数:" << sp_1.getRefCount() << endl;

sp_1->Print();

sp_outer = sp_1;

cout << "\n********\n走出作用域,自动释放对象:\n";
}

cout << "\n********\n保存在外部作用域的资源在程序结束时才会释放:\n";
}

编译运行后输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开始一个作用域
创建对象:013F07B0
从对象新建智能指针后,引用计数:1
从智能指针复制后,引用计数:2
创建对象:013F0900
智能指针作为左值被赋值,原对象引用计数:1,新对象引用计数:1
智能指针对程序员是透明的,当前对象地址:013F0900

********
走出作用域,自动释放对象:
释放对象:013F07B0

********
保存在外部作用域的资源在程序结束时才会释放:
释放对象:013F0900
请按任意键继续. . .

可见目的已经达到。

其它讨论

上文所编写的 SmartPointer 类即是一个智能指针雏形,与之起相似作用的是 std::shared_ptr。但是 C++11 标准还引入了两种指针:std::unique_ptrstd::weak_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
34
class ClassB;

class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
SmartPointer<ClassB> pb; // 在A中引用B
};

class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
SmartPointer<ClassA> pa; // 在B中引用A
};

int main()
{
{
// 创建对象,spa 指向对象引用计数为 1
SmartPointer<ClassA> spa = SmartPointer<ClassA>::New();
// 创建对象,spb 指向对象引用计数为1
SmartPointer<ClassB> spb = SmartPointer<ClassB>::New();
spa->pb = spb; // 复制指针,spb 与 spa->pb 指向相同对象,引用计数为 2
spb->pa = spa; // 复制指针,spa 与 spb->pa 指向相同对象,引用计数为 2
}
// 作用域结束
// spa 销毁,spa 指向对象引用计数为 1
// spb 销毁,spb 指向对象引用计数为 1
// 引用计数都没有归零,因此不会释放资源
cout<< "作用域结\n" << endl;
}

运行一下就知道,在作用域结束后,资源并不会被释放。这就是环形引用,由于环的存在,引用计数始终不能清零,导致无法释放资源,造成内存泄漏。

环形引用是不能通过程序语言的设计来解决的,我们只能打破这个环,来避免这个情况,这就是 weak_ptr 的作用。

严格来说 weak_ptr 并不能算作是一个智能指针,因为它不能影响对象的生命周期,也就是它的创建与释放不会影响引用计数,它只是一个观测者而已。一个环形引用,若其中一个节点被换成了 weak_ptr,那么这个环就不能称之为环,问题便解决了。

weak_ptr 并没有重载 *-> 操作符,程序员是不能用它直接访问对象方法的。这是合理的,因为既然 weak_ptr 不能影响引用计数,就有可能存在一种情况:weak_ptr 指向的对象被销毁了,但自身还存在。此时需要特别地实现一种方法,来检测其指向的对象是否存在,若存在,返回一个真正的智能指针。std 的做法是 weak_ptr::lock() 方法。

unique_ptr

unique_ptr 是一种更严格的、更自私的智能指针,当它自身被析构时,它所指向的对象也一并被析构。 并且还有一个性质:它不允许除它以外的智能指针指向同一对象(也就是没有实现 copy 方法),不过可以通过 move 来实现转移所有权,也就是转移到另一个unique_ptr 来管理对象。unique_ptr 有利于异常安全,但是似乎用的稍微少一些。

写在后面

最近重新梳理 C++ 中的一些基础知识,总结出一个规律:越是安全越是高级的语言,越是不信任程序员。这些语言的设计理念就是要把可能出错的地方都自动处理好,而不是指望程序员处理。

所谓程序语言是否安全,难道是指机器在执行代码时出错的可能性吗?这种理解是错误的。一旦代码写定,机器执行二进制指令都是一样的,没有 C++ 编译出的 binary 比 Java 的 binary 更容易出错的说法。机器几乎不会出错,不论是在跑什么语言。

所谓安全性,是指程序员把事情搞砸的可能性。高级语言的从语言特性、语法规则设计的角度,尽可能地去把程序员瞎写带来的隐患降低,这是提高安全性的本质。这一点在 C++ 的 const 关键字上体现得尤为明显。降低了自由度是肯定的,但是带来了更强的鲁棒性,一切也变得和平起来。

这么一想,其中的政治意味竟然是如此浓厚。