09.C++ 智能指针

本节分为五部分:

  1. 自己实现智能指针
  2. 不带引用计数的智能指针:auto_ptrscoped_ptrunique_ptr
  3. 带引用计数的智能指针:shared_ptrweak_ptr
  4. 多线程访问共享对象问题
  5. 自定义删除器

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在 C++ 中,动态内存的管理是用一对运算符完成的:new 和 delete。

  • new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针;
  • delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现以下几种问题:

  1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题)。
  2. 尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
  3. 统一资源释放多次,导致释放野指针,程序崩溃。
  4. 代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间 return 掉了,导致释放资源的代码未被执行到。
  5. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

主要体现在用户可以不关注资源的释放,因为智能指针会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异常,资源在到期的情况下,一定会进行释放。

C++ 11 库中,提供了 带引用计数的智能指针和不带引用计数的智能指针,本章节主要以原理和应用场景。

1. 自己实现智能指针

智能指针的基本原理:

利用栈上的对象做出作用域会自动析构 的特点,把资源释放代码全部放在析构函数中执行,就达到了所谓的智能指针。

  1. 使用裸指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main()
{
int* p = new int;
/*其它的代码...*/
/*
如果这里忘记写 delete,或者上面的代码段中程序 return 掉了,
没有执行到这里,都会导致这里没有释放内存,内存泄漏
*/
delete p;

return 0;
}
  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
#include <iostream>
using namespace std;

template<class T>
class CSmartPtr
{
public:
// 构造函数
CSmartPtr(T* ptr = nullptr)
: mptr(ptr)
{
// ....
}
// 析构函数
~CSmartPtr()
{
delete mptr;
}

private:
T* mptr;
};

int main()
{
CSmartPtr<int> ptr(new int);
/*其它的代码...*/

/*
由于 ptr 是栈上的智能指针对象,不管是函数正常执行完,
还是运行过程中出现异常,栈上的对象都会自动调用析构函数,
在析构函数中进行了 delete 操作,保证释放资源
*/

return 0;
}

上面代码实现了较为简单的智能指针,主要用到两点:

(1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源。
(2)利用栈上的对象出作用域自动析构这一特点,在智能指针的析构函数中保证释放资源。

所以,智能指针一般都是定义在栈上的。

面试题:能不能在堆上定义智能指针?

1
CSmartPtr* p = new CSmartPtr(new int);

这里定义的 p 虽然是智能指针类型,但它实质上还是一个裸指针,因此 p 还是需要进行手动 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
#include <iostream>
using namespace std;

template<class T>
class CSmartPtr
{
public:
// 构造函数
CSmartPtr(T* ptr = nullptr)
: mptr(ptr)
{
// ....
}
// 析构函数
~CSmartPtr()
{
delete mptr;
}

// 重载函数
// 1. *
T& operator*() { return *mptr; }
const T& operator*() const { return *mptr; }
// 2. ->
T& operator->() { return *mptr; }
const T& operator->() const { return *mptr; }

private:
T* mptr;
};

int main()
{
CSmartPtr<int> ptr(new int);

*ptr = 20;
cout << "ptr = " << *ptr << endl;

return 0;
}

image.png

上面的这个智能指针,使用起来就和普通的裸指针非常相似了,但是它还存在很大的问题,看下面的代码:

1
2
3
4
5
6
int main()
{
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
return 0;
}

这个 main 函数运行,代码直接崩溃,问题出在默认的拷贝构造函数做的浅拷贝,两个智能指针都持有一个 new int 资源,ptr2 仙溪沟释放了资源,到了 ptr1 析构的时候,就变成了 delete 野指针,造成程序的崩溃。所以这里引出来智能指针需要解决的两件事情:

  1. 怎么解决智能指针的浅拷贝问题
  2. 多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次,造成代码运行不可预期的严重后果

查看 C++ 库中的智能指针如何解决的问题。

2. 不带引用计数的智能指针

C++ 库中提供的不带引用计数的智能指针主要包括:auto_ptr,scoped_ptr,unique_ptr,下面一一进行介绍。

1. 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
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef _Ty element_type;

explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
: _Myptr(_Ptr)
{ // construct from object pointer
}

/*
这里是 auto_ptr 的拷贝构造函数,
_Right.release() 函数中,把 _Right 的 _Myptr
赋为 nullptr,也就是换成当前 auto_ptr 持有资源地址
*/
auto_ptr(auto_ptr& _Right) noexcept
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}

_Ty * release() noexcept
{ // return wrapped pointer and give up ownership
_Ty * _Tmp = _Myptr;
_Myptr = nullptr;
return (_Tmp);
}
private:
_Ty * _Myptr; // the wrapped object pointer
};

image.png

auto_ptr 的源码可以看到,只有最后一个 auto_ptr 智能指针持有资源,原来的 auto_ptr 都被赋 nullptr 了,考虑如下代码:

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

int main()
{
auto_ptr<int> p1(new int);
/*
经过拷贝构造,p2 指向了 new int 资源,
p1 现在为 nullptr 了,如果使用 p1,相当于
访问空指针了,很危险
*/
auto_ptr<int> p2(p1);
*p1 = 10;

cout << "p1 = " << *p1 << endl;

return 0;
}

image.png

上面的程序,如果用户不了解 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
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<auto_ptr<int>> vec;

vec.push_back(auto_ptr<int>(new int(10)));
vec.push_back(auto_ptr<int>(new int(20)));
vec.push_back(auto_ptr<int>(new int(30)));

// 输出
cout << "vec[0] = " << *vec[0] << endl;

// 拷贝
vector<auto_ptr<int>> vec2 = vec;
/*
这里由于上面做了 vector 容器的拷贝,相当于容器中
的每一个元素都进行了拷贝构造,原来 vec 中的智能指针
全部为 nullptr了,再次访问就成访问空指针了,程序崩溃
*/

cout << "vec[0] = " << *vec[0] << endl;

return 0;
}

image.png

所以不要在容器中使用 auto_ptrC++ 建议最好不要使用 auto_ptr,除非应用场景非常简单。

【总结】:auto_ptr 智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的 auto_ptr 都置为 nullptr,只让最后一个 auto_ptr 持有资源。

2. scoped_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
template<class T> class scoped_ptr // noncopyable
{
private:
T * px;

/*
私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
*/
scoped_ptr(scoped_ptr const &);
scoped_ptr & operator=(scoped_ptr const &);

typedef scoped_ptr<T> this_type;

/*
私有化逻辑比较运算符重载函数,不支持scoped_ptr的智能指针
对象的比较操作
*/
void operator==( scoped_ptr const& ) const;
void operator!=( scoped_ptr const& ) const;

public:
typedef T element_type;
explicit scoped_ptr( T * p = 0 ): px( p ) // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}

#ifndef BOOST_NO_AUTO_PTR
/*
支持从 auto_ptr 构造一个 scoped_ptr 智能指针对象,
但是 auto_ptr 因为调用 release() 函数,导致其内部指
针为 nullptr
*/
explicit scoped_ptr( std::auto_ptr<T> p ) BOOST_NOEXCEPT : px( p.release() )
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}

#endif
/*析构函数,释放智能指针持有的资源*/
~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}
};

相当于:

1
2
scoped_ptr(scoped_ptr const &) = delete;
scoped_ptr & operator=(scoped_ptr const &) = delete;

scoped_ptr 的源码中可以看到,该智能指针私有化了拷贝构造函数和
operator= 重载赋值函数,因此从根本上杜绝了智能指针浅拷贝的发生,所以 scoped_ptr 也是不能用在容器当中的,如果容器互相进行拷贝或者赋值,就会引起 scoped_ptr 对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误

auto_ptrscoped_ptr 这一点上的区别,有些资料上用所有权的概念来描述,道理是相同的,auto_ptr 可以任意转移资源的所有权,而 scoped_ptr 不会转移所有权(因为拷贝构造和赋值被禁止了)。

3. unique_ptr 源码

要深入了解 unique_ptr,需要先了解 C++ 的右值引用原理。

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
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{ // non-copyable pointer to an object
public:
typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
typedef typename _Mybase::pointer pointer;
typedef _Ty element_type;
typedef _Dx deleter_type;

/*提供了右值引用的拷贝构造函数*/
unique_ptr(unique_ptr&& _Right) noexcept
: _Mybase(_Right.release(),
_STD forward<_Dx>(_Right.get_deleter()))
{ // construct by moving _Right
}

/*提供了右值引用的operator=赋值重载函数*/
unique_ptr& operator=(unique_ptr&& _Right) noexcept
{ // assign by moving _Right
if (this != _STD addressof(_Right))
{ // different, do the move
reset(_Right.release());
this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
}
return (*this);
}

/*
交换两个unique_ptr智能指针对象的底层指针
和删除器
*/
void swap(unique_ptr& _Right) noexcept
{ // swap elements
_Swap_adl(this->_Myptr(), _Right._Myptr());
_Swap_adl(this->get_deleter(), _Right.get_deleter());
}

/*通过自定义删除器释放资源*/
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get());
}
}

/*unique_ptr提供->运算符的重载函数*/
_NODISCARD pointer operator->() const noexcept
{ // return pointer to class object
return (this->_Myptr());
}

/*返回智能指针对象底层管理的指针*/
_NODISCARD pointer get() const noexcept
{ // return pointer to object
return (this->_Myptr());
}

/*提供bool类型的重载,使unique_ptr对象可以
直接使用在逻辑语句当中,比如if,for,while等*/
explicit operator bool() const noexcept
{ // test for non-null pointer
return (get() != pointer());
}

/*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
pointer release() noexcept
{ // yield ownership of pointer
pointer _Ans = get();
this->_Myptr() = pointer();
return (_Ans);
}

/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
void reset(pointer _Ptr = pointer()) noexcept
{ // establish new pointer
pointer _Old = get();
this->_Myptr() = _Ptr;
if (_Old != pointer())
{
this->get_deleter()(_Old);
}
}
/*
删除了unique_ptr的拷贝构造和operator=赋值函数,
因此不能做unique_ptr智能指针对象的拷贝构造和
赋值,防止浅拷贝的发生
*/
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};

从源码中可以看出,unique_ptr 有一点和 scoped_ptr 做的一样,就是去掉了拷贝构造函数和 operator= 赋值重载函数,静止了用于对 unique_ptr 进行显示的拷贝和赋值,放置智能指针浅拷贝问题的发生。

但是 unique_ptr 提供了带右值引用参数的拷贝构造和赋值,也就是说,unique_ptr 智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生 unique_ptr 临时对象的地方,如把 unique_ptr 作为函数的返回值时,示例代码如下:

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

int main()
{
unique_ptr<int> ptr1(new int);
// 转换为 优质引用
unique_ptr<int> ptr2 = std::move(ptr1);
ptr2 = std::move(ptr1);

return 0;
}

示例2:

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

unique_ptr<int> test_uniqueptr()
{
unique_ptr<int> ptr1(new int);
return ptr1;
}

int main()
{
/*
此处调用 test_uniqueptr 函数,在 return ptr1 代码处,
调用右值引用的拷贝构造函数,由 ptr1 拷贝构造 ptr
*/
unique_ptr<int> ptr = test_uniqueptr();

return 0;
}

unique_ptr 还提供了 reset 重置资源,swap 交换资源等函数,也经常会使用到。

可以看到,unique_ptr 从名字就可以看出来,最终也是只能有一个该智能指针引用资源,因此建议在使用不带引用计数的智能指针时,可以优先选择 unique_ptr 智能指针

3. 带引用计数的智能指针

带引用计数的智能指针可以实现多个智能指针管理同一个资源。 通过给每个被管理的资源匹配一个引用计数来实现。当新增一个智能指针指向该资源时,引用计数 +1,当减少一个智能指向该资源是,引用计数 -1,知道引用计数为 0 时,资源被释放掉。由最后一个智能指针的析构函数来处理资源的释放问题,这就是引用计数的概念。

  • 带引用计数:多个智能指针可以管理同一个资源
  • 带引用计数:给每一个对象资源,匹配一个引用计数

智能指针 -> 引用资源的时候 -> 引用计数 +1

智能指针 -> 不适用资源的时候 -> 引用计数 -1 -> != 0 不释放资源,为 0 则释放资源

库里面的 shared_ptrweak_ptr 引用计数的加减是线程安全的,因为用 atomic 定义了引用计数。

计数实现:

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 T>
class RefCnt
{
public:
// 构造函数初始化
RefCnt(T* ptr = nullptr)
: mptr(ptr)
{
if (mptr != nullptr)
{
mcount = 1;
}
}

// 实现增加和减少引用方法
void addRef() { mcount++; }
int delRef() { return --mcount; }

private:
T* mptr;
int mcount;
};

智能指针类:

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
// 实现智能指针
template<class T>
class CSmartPtr
{
public:
// 构造函数 - 初始化
CSmartPtr(T* ptr = nullptr)
: mptr(ptr)
{
// 调用引用计数
mpRefCnt = new RefCnt<T>(mptr);
}
/*CSmartPtr(const Csmartptr<T> &src) { mptr = new T(*src .mptr) ; }*/
// 析构函数 - 释放内存
~CSmartPtr()
{
if (mpRefCnt->delRef() == 0)
{
delete mptr;
mptr = nullptr;
}
}

// 重载函数
T& operator*() { return *mptr; }
T* operator->() { return mptr; }

// 拷贝构造函数
CSmartPtr(CSmartPtr<T>& src)
: mptr(src.mptr)
, mpRefCnt(src.mpRefCnt)
{
if (mptr != nullptr)
{
mpRefCnt->addRef();
}
}

// 赋值重载函数
CSmartPtr<T>& operator=(const CSmartPtr<T>& src)
{
// 判断
if (this == &src)
{
return *this;
}

// 判断计时器
if (mpRefCnt->delRef() == 0)
{
delete mptr;
}

// 重新赋值
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
}

private:
// 指向资源的指针
T* mptr;
// 指向该资源引用计数对象的指针
RefCnt<T>* mpRefCnt;
};

main 函数测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
// 普通初始化
CSmartPtr<int> ptr1(new int);
// 拷贝构造函数初始化
CSmartPtr<int> ptr2(ptr1);
// 初始化
CSmartPtr<int> ptr3;
// 复制构造函数
ptr3 = ptr2;

// 赋值
*ptr1 = 20;

cout << *ptr2 << " " << *ptr3 << endl;

return 0;
}

image.png

1. shared_ptr 实现

内部大概实现:每次复制,多一个共享同处资源的 shared_ptr 时,计数 +1。每次释放 shared_ptr 时,计数 -1。
当 shared 计数为 0 时,则证明所有指向同一处资源的 shared_ptr 们全都释放了,则随即释放该资源(还会释放 new 出来的 SharedPtrControlBlock)。

1
2
3
4
5
6
7
8
9
10
11
12
//shared计数放在这个结构体里面,实际上结构体里还应该有另一个weak计数。下文介绍weak_ptr时会解释。
struct SharedPtrControlBlock
{  
int shared_count;
};
//大概长这个样子(简化版)
template<class T>
class shared_ptr
{  
T* ptr;  
SharedPtrControlBlock* count;
};

shared_ptr 是强智能指针,可以改变资源的引用计数

循环引用问题,造成 new 出来的资源无法释放,资源泄露

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
#include <iostream>
using namespace std;

class B;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr<B> ptrb;
};

class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr<A> ptra;
};

int main()
{
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());

pa->ptrb = pb;
pb->ptra = pa;

cout << pa.use_count() << endl; //打印引用计数
cout << pb.use_count() << endl;
}

image.png

image.png

  1. 首先,初始化时:
1
2
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());

shared_ptr 创建后,栈上的指针指向了对应的对象上,时期计数 +1。

  1. 其次,指向
1
2
pa->ptrb = pb;
pb->ptra = pa;

指针互相指向,其计数器又 +1。

这就是交叉引用问题。!!!!

  1. 最后,析构,无法释放对象中互相引用的内存。

image.png

如下表所示是 shared_ptr 特有的操作:

image.png

如何解决循环引用问题?

  • 定义对象的时候,用强智能指针;引用对象的地方,使用弱智能指针
  • weak_ptr 之所以可以打破循环引用,是因为:将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数

2. weak_ptr

weak_ptr 是为了辅助 shared_ptr 的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。

(只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以 weak_ptr 是一种弱引用型指针)

内部大概实现:

  • 计数区域(SharedPtrControlBlock)结构体引进新的 int 变量 weak_count,来作为弱引用计数。
  • 每个 weak_ptr 都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针(和 shared_ptr 一样的成员)。
  • weak_ptr 可以由一个 shared_ptr 或者另一个 weak_ptr 构造。
  • weak_ptr 的构造和析构不会引起 shared_count 的增加或减少,只会引起 weak_count 的增加或减少。

被管理资源的释放只取决于 shared 计数,当 shared 计数为0,才会释放被管理资源,也就是说 weak_ptr 不控制资源的生命周期

但是计数区域的释放却取决于 shared 计数和 weak 计数,当两者均为 0 时,才会释放计数区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//shared引用计数和weak引用计数
//之前的计数区域实际最终应该长这个样子
struct SharedPtrControlBlock
{  
int shared_count;  
int weak_count;
};
//大概长这个样子(简化版)
template<class T>
class weak_ptr
{  
T* ptr;  
SharedPtrControlBlock* count;
};

弱智能指针 weak_ptr 区别于 shared_ptr 之处在于:

  1. weak_ptr 不会改变资源的引用计数,只是一个观察者的角色,通过观察 shared_ptr 来判定资源是否存在
  2. weak_ptr 持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  3. weak_ptr 没有提供常用的指针操作,无法直接访问资源,需要先通过 lock方法提升为 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
#include <iostream>
#include <memory>
using namespace std;

class B; // 前置声明类B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr<A> _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
// 定义对象时,用强智能指针
shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
ptra->_ptrb = ptrb;
// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
ptrb->_ptra = ptra;

cout << ptra.use_count() << endl; // 打印结果:1
cout << ptrb.use_count() << endl; // 打印结果:1

/*
出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
*/
return 0;
}

weak_ptr 是弱智能指针,不会改变资源的引用计数

weak_ptr -> (观察)shared_ptr ->(管理) 资源(内存)

注意:

weak_ptr 只是一个观察者,它并不能直接操纵资源,没有重载 ->* 运算符,所以不能 -> 这样输出,要用 lock 返回 shared_ptr 类型才能用 ->

image.png

4. 多线程访问共享对象问题

强弱智能指针解决的另一问题:多线程访问共享对象的线程安全问题

有一个用 C++ 写的开源网络库,muduo 库,作者陈硕

该源码中对于智能指针的应用非常优秀,其中借助 shared_ptrweak_ptr 解决了这样一个问题,多线程访问共享对象的线程安全问题。

解释如下:线程 A 和线程 B 访问一个共享的对象,如果线程 A 正在析构这个对象的时候,线程 B 又要调用该共享对象的成员方法,此时可能线程 A 已经把对象析构完了,线程 B 再去访问该对象,就会发生不可预期的错误。

代码:

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
#include <iostream>
#include <thread>
using namespace std;

class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test()
:_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int* volatile _ptr;
};

void threadProc(Test* p)
{
// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
*/
p->show();
}

int main()
{
// 在堆上定义共享对象
Test* p = new Test();
// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
std::thread t1(threadProc, p);

// 在main线程中析构Test共享对象
delete p;
// 等待子线程运行结束
t1.join();

return 0;
}

运行上面的代码,发现在 main 主线程已经 delete 析构 Test 对象以后,子线程 threadProc 再去访问 Test 对象的 show 方法,无法打印出 *_ptr 的值 20。可以用 shared_ptrweak_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
55
56
57
#include <iostream>
#include <thread>
#include <memory>
using namespace std;

class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int* volatile _ptr;
};

void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
// 睡眠两秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
*/
shared_ptr<Test> ps = pw.lock();
if (ps != nullptr)
{
ps->show();
}
}

int main()
{
// 在堆上定义共享对象
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 等待子线程运行结束
t1.join();
return 0;
}

运行上面的代码,show 方法可以打印出 20,因为 main 线程调用了 t1.join() 方法等待子线程结束,此时 pw 通过 lock 提升为 ps 成功,见上面代码示例。

如果设置 t1 为分离线程,让 main 主线程结束,p 智能指针析构,进而把 Test 对象析构,此时 show 方法已经不会被调用,因为在 threadProc 方法中,pw 提升到 ps 时,lock 方法判定 Test 对象已经析构,提升失败!main 函数代码可以如下修改测试:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
// 在堆上定义共享对象
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 设置子线程分离
t1.detach();
return 0;
}

该 main 函数运行后,最终的 threadProc 中,show 方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用

5. 自定义删除器

在用智能指针管理的资源是堆内存,当智能指针出作用域的时候,在其析构函数中会 delete 释放堆内存资源,但是除了堆内存资源,智能指针还可以管理其它资源,比如:打开的文件,此时对于文件指针的关闭,就不能用 delete 了,这时需要自定义智能指针释放资源的方式,先看看 unique_ptr 智能指针的析构函数代码,如下:

1
2
3
4
5
6
7
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
}
}

unique_ptr 的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:

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

class FileDeleter
{
public:
// 删除器负责删除资源的函数
void operator()(FILE* pf)
{
fclose(pf);
}
};

int main()
{
// 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));

return 0;
}

当然这种方式需要定义额外的函数对象类型,不推荐,可以用 C++11 提供的函数对象 function 和 lambda 表达式更好的处理自定义删除器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
// 自定义智能指针删除器,关闭文件资源
unique_ptr<FILE, function<void(FILE*)>>
filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});

// 自定义智能指针删除器,释放数组资源
unique_ptr<int, function<void(int*)>>
arrayPtr(new int[100], [](int *ptr)->void {delete[]ptr; });

return 0;
}

想进一步了解智能指针,可以查看智能指针的源码实现,或者看 muduo 网络库的源码。


09.C++ 智能指针
http://example.com/2023/08/17/03.C++进阶部分/09.C++ 智能指针/
Author
Yakumo
Posted on
August 17, 2023
Licensed under