cpp智能指针

什么是智能指针

智能指针是一个管理指针。用来存储指向动态分配对象(在堆区 new 出来的)的指针。在它的生命周期结束时,它可以负责自动释放动态分配的对象,调用析构函数,以达到防止堆内存泄露的目的。

智能指针发展史

  1. C++98 引入 auto_ptr (已废弃)
  2. C++11 引入 unique_ptr 和 shared_ptr

原理

RAII

RAII 代表 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父提出的设计理念,核心是把资源和对象的生命周期绑定,对象创建获取资源,销毁释放资源。这样做有两大好处:

  1. 不需要显式的释放资源。
  2. 对象所需资源在其生命周期始终保持有效。

实现一个简单的智能指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

template<class T>
class smartPtr {
public:
smartPtr(T* ptr = nullptr) : ptr_(ptr) {}
~smartPtr() {
if (ptr_) {
delete ptr_;
}
cout << "~smartPtr called!" << endl;
}
private:
T* ptr_;
};
int main() {
int* a = new int(1);
smartPtr<int> sp(a); //将a 指针委托给sp对象管理
smartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

上面的还不足以称为智能指针,因为

  1. 没有重载运算符 * 和 ->
  2. 如果使用了拷贝或者赋值操作,由于是浅拷贝,会有 double free 的错误。
1
2
3
4
5
int main() {
smartPtr<Date> sp(new Date);
smartPtr<Date> sp2(sp); // 发生浅拷贝,开辟的两个smartPtr对象指向同一个内存。
return 0; // 两个都调用析构,第二次析构出现问题。
}

auto_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory> // 智能指针定义在此
using namespace std;
class Date {
public:
Date(): year_(0), month_(0), day_(0) {}
~Date(){}
int year_;
int month_;
int day_;
};

int main() {
auto_ptr<Date> ap(new Date);
//拷贝构造
auto_ptr<Date> copy(ap);
ap->year_ = 2022;
}

可以发现报错了。原因是对于 auto_ptr 来说,赋值后之前的指针就被置空了。在拷贝或者赋值的过程中,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。虽然这种方法确实解决了浅拷贝的问题,但是我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。
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
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr = nullptr) : ptr_(ptr) {}

auto_ptr(auto_ptr<T>& ap) : _ptr(ap.ptr_) { // 拷贝构造
ap.ptr_ = nullptr; // 源指针管理权转移
}

auto_ptr<T>& operator = (auto_ptr<T>& ap) { // 赋值构造
if (this != *ap) {
delete ptr_;
ptr_ = ap.ptr_;
ap.ptr_ = nullptr;
}
return *this;
}

~auto_ptr() {
if (ptr_) {
delete ptr_;
}
}
T& operator *() { // 重载 * 运算符
return *ptr_;
}
T* operator ->() { // 重载 -> 运算符
return ptr_;
}
private:
T* ptr_;
};

unique_ptr

相比 auto_ptr,unique_ptr 得不到就毁掉,不允许别人拿到。禁用拷贝和赋值构造函数。只能采用移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr = nullptr) : ptr_(ptr) {}

unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;

~unique_ptr() {
if (ptr_) {
delete ptr_;
}
}
T& operator *() { // 重载 * 运算符
return *ptr_;
}
T* operator ->() { // 重载 -> 运算符
return ptr_;
}
private:
T* ptr_;
};

shared_ptr

shared_ptr 使用最为广泛,可以提供安全的拷贝构造。

原理:

对一个资源加一个计数器,让所有管理资源的 shared_ptr 共用这个计数器。如果发生拷贝,计数器 +1+1。等于 00 的时候,就析构。
再具体一点:

  1. shared_ptr 在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  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
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr =nullptr) : ptr_(ptr), pcount_(new int(1)) {}
//拷贝构造
shared_ptr(const T& sp) ptr_(sp.ptr_), pcount_(sp.pcount_) {
++(*pcount_);
}
// 赋值拷贝1
shared_ptr<T>& operator = (shared_ptr<T>& sp) {
if (ptr_ != sp.ptr_) { // 注意:如果对自己赋值可能会误析构资源。
if (--(*pcount_) == 0) { // 先析构自己,再赋值为别人的,以免内存泄漏
delete pcount_;
delete ptr_;
}
ptr_ = sp.ptr_;
pcount_ = sp.pcount_;
++(*pcount_);
}
return *this;
}
/** 赋值构造写法2
shared_ptr<T>& operator=(shared_ptr<T> sp) {
swap(_ptr, sp._ptr);
swap(_pcount, sp._pcount);
return *this;
}
**/
T& operator *() {
return *ptr_;
}
T* operator ->() {
return ptr_;
}

~shared_ptr() {
if (--(*pcount_) == 0 && ptr_) { // 只有引用计数为 0 才释放
delete pcount_;
delete ptr_;
}
}
private:
T* ptr_;
int* pcount_; // 所有指向同一块内存的share_ptr共享,所以申请在堆区
};

拷贝构造图解:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)),
_pmtx(new mutex) {}

void add_ref() {
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}

void release_ref() {
bool flag = false; // 是否释放锁的内存
_pmtx->lock();
if (--(*_pcount) == 0 && _ptr) {
delete _pcount;
delete _ptr;
flag = true;
cout << "释放资源:" << _ptr << endl;
}
_pmtx->unlock();
if (flag) {
delete _pmtx;
}
}

shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount),
_pmtx(sp._pmtx) {
add_ref();
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
if (--(*_pcount) == 0){
delete _pcount;
delete _ptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
add_ref();
}
return *this;
}
T& operator *() {
return *_ptr;
}

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

T* get() {
return _ptr;
}
int use_count() {
return *_pcount;
}

~shared_ptr() {
release_ref();
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx; // 此锁用于访问 _pcount时同步用
};

删除器

库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc 出来的话,就导致类型不是匹配的,无法析构。这个时候我们要自定义删除器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
using namespace std;
template<class T>
struct DeleteArray {
void operator()(T* ptr) { // 仿函数
cout << "delete functor called!" << endl;
delete[] ptr;
}
};

int main() {
DeleteArray<string> delfunctor; //使用仿函数定制
std::shared_ptr<string>s2(new string[10], delfunctor);
std::shared_ptr<string>s3((string*)malloc(sizeof(100)), [](string* ptr) {
cout << "call free!" << endl;
free(ptr);
}); //使用lamdba 定制
return 0;
}
1
2
3
@└────> # ./a.out 
call free!
delete functor called!

weak_ptr

有以下情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
using namespace std;
struct ListNode {
shared_ptr<ListNode> prev_;
shared_ptr<ListNode> next_;
int val_;
~ListNode() {
cout << "~ListNode called!" << endl;
}
};

int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->next_ = n2;
n2->prev_ = n1;
}

运行发现,析构函数没有被调用。
2
两边引用计数都是2,像是死锁一样,都不会析构。这时候就要用到 weak_ptr了。weak_ptr 是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的。我们只能使用 weak_ptr 或者 shared_ptr 去初始化它。
我们在会产生循环引用的位置,把shared_ptr 换成 weak_ptr。 weak_ptr 不是一个 RAII 智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个 weak_ptr,但是 weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。
上面代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
using namespace std;
struct ListNode {
weak_ptr<ListNode> prev_;
weak_ptr<ListNode> next_;
int val_;
~ListNode() {
cout << "~ListNode called!" << endl;
}
};

int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->next_ = n2;
n2->prev_ = n1;
}

实现代码参考:

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
template<class T>
class weak_ptr {
public:
weak_ptr() : _ptr(nullptr) {}
// 用 shared_ptr 初始化
weak_ptr(shared_ptr<T>& sp) :_ptr(sp.get()), _pcount(sp.use_count()) {}
weak_ptr(weak_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {}
weak_ptr& operator = (shared_ptr<T>& sp) {
_ptr = sp.get();
_pcount = sp.use_count();
return *this;
}
weak_ptr& operator = (weak_ptr<T>& sp) {
_ptr = sp._ptr;
_pcount = sp._pcount;
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
int use_count() {
return *_pcount;
}
private:
T* _ptr;
int* _pcount; // 只记录 share_ptr 引用个数,weak_ptr的观测不会增加它
};