共享所有权
一个动态分配的对象可以在多个shared_ptr
之间共享,这是因为shared_ptr
支持 copy 操作:
原理介绍
shared_ptr
内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数和其它一些数据。由于这个控制块需要在多个shared_ptr
之间共享,所以它也是存在于 heap 中的。shared_ptr
对象本身是线程安全的,也就是说shared_ptr
的引用计数增加和减少的操作都是原子的。
通过unique_ptr
来构造shared_ptr
是可行的:
shared_ptr 的风险
你大概觉得使用智能指针就再也高枕无忧了,不再为内存泄露烦恼了。然而梦想总是美好的,使用shared_ptr
时,不可避免地会遇到循环引用的情况,这样容易导致内存泄露。循环引用就像下图所示,通过shared_ptr
创建的两个对象,同时它们的内部均包含shared_ptr
指向对方。
分析一下main
函数是如何退出的,一切就都明了:
main
函数退出之前,Father
和Son
对象的引用计数都是2
。son
指针销毁,这时Son
对象的引用计数是1
。father
指针销毁,这时Father
对象的引用计数是1
。- 由于
Father
对象和Son
对象的引用计数都是1
,这两个对象都不会被销毁,从而发生内存泄露。
为避免循环引用导致的内存泄露,就需要使用weak_ptr
,weak_ptr
并不拥有其指向的对象,也就是说,让weak_ptr
指向shared_ptr
所指向对象,对象的引用计数并不会增加:
使用weak_ptr
就能解决前面提到的循环引用的问题,方法很简单,只要让Son
或者Father
包含的shared_ptr
改成weak_ptr
就可以了。
同样,分析一下main
函数退出时发生了什么:
main
函数退出前,Son
对象的引用计数是2
,而Father
的引用计数是1
。son
指针销毁,Son
对象的引用计数变成1
。father
指针销毁,Father
对象的引用计数变成0
,导致Father
对象析构,Father
对象的析构会导致它包含的son_
指针被销毁,这时Son
对象的引用计数变成0
,所以Son
对象也会被析构。
然而,weak_ptr
并不是完美的,因为weak_ptr
不持有对象,所以不能通过weak_ptr
去访问对象的成员,例如:
你可能猜到了,既然shared_ptr
可以访问对象成员,那么是否可以通过weak_ptr
去构造shared_ptr
呢?事实就是这样,实际上weak_ptr
只是作为一个转换的桥梁(proxy),通过weak_ptr
得到shared_ptr
,有两种方式:
- 调用
weak_ptr
的lock()
方法,要是对象已被析构,那么lock()
返回一个空的shared_ptr
。 - 将
weak_ptr
传递给shared_ptr
的构造函数,要是对象已被析构,则抛出std::exception
异常。
既然weak_ptr
不持有对象,也就是说weak_ptr
指向的对象可能析构了,但weak_ptr
却不知道。所以需要判断weak_ptr
指向的对象是否还存在,有两种方式:
weak_ptr
的use_count()
方法,判断引用计数是否为0
。- 调用
weak_ptr
的expired()
方法,若对象已经被析构,则expired()
将返回true
。
转换过后,就可以通过shared_ptr
去访问对象了:
销毁操作
为shared_ptr
指定 deleter 的做法很简单:
与unique_ptr
不同,标准库并不提供shared_ptr<T[]>
,因此,使用shared_ptr
处理数组时需要显示指定删除行为,例如:
由于不存在shared_ptr<T[]>
,我们无法使用[]
来访问数组中的元素,实际上无法访问到数组中的元素。也就是说使用shared_ptr
来指向数组意义并不大。若想要数组在多个shared_ptr
之间共享,可以考虑使用shared_ptr<vector>
或shared_ptr<array>
。
更多陷阱
使用shared_ptr
时,注意不能直接通过同一个 raw pointer 指针来构造多个shared_ptr
:
很明显,每次通过 raw pointer 来构造shared_ptr
时候就会分配一个控制块,这时存在两个控制块,也就是说存在两个引用计数。这显然是错误的,因为当这两个shared_ptr
被销毁时,对象将会被delete
两次。
考虑到this
也是 raw pointer,所以一不小心就会用同一个this
去构造多个shared_ptr
了,就像这样:
每次调用addToGroup()
时都会创建一个控制块,所以这个对象会对应多个引用计数,最终这个对象就会被delete
多次,导致运行出错。解决这个问题很简单,只要让std::enable_shared_from_this<Student>
作为Student
的基类:
需要注意,调用shared_from_this()
之前需要确保对象被share_ptr
所持有,理解这一点是很容易的,因为只有当对象被shared_ptr
所持有,使用shared_from_this()
所返回的shared_ptr
才会指向同一个对象。
在调用shared_from_this()
之前需要确保对象被shared_ptr
所持有,要使得对象被shared_ptr
所持有,对象首先要初始化(调用构造函数),所以一定不能在Student
的构造函数中调用shared_from_this()
:
好了,那么问题来了:
- 要怎样才能防止用户以不正确的方式来创建
Student
对象呢? - 同时也要使得,我们可以使用不同的构造方式来创建
Student
对象。
可以将Student
的构造函数声明为private
的,因此,用户无法直接创建Student
对象。另一方面,增加create()
成员函数,在这个函数里面,我们使用 C++11 的 variadic templates 特性,调用Student
的构造函数来构造对象:
通过create()
保证了用户创建的对象总是被shared_ptr
所持有,可以将create()
想象成Student
构造函数的别名:
性能考虑
|
|
使用这种方式创建shared_ptr
时,需要执行两次new
操作,一次在 heap 上为string("hello")
分配内存,另一次在 heap 上为控制块分配内存。使用make_shared
来创建shared_ptr
会高效,因为make_shared
仅使用new
操作一次,它的做法是在 heap 上分配一块连续的内存用来容纳string("hello")
和控制块。同样,当shared_ptr
的被析构时,也只需一次delete
操作。
参考资料
- Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
- StackOverflow: what is the usefulness of enable_shared_from_this
- Safely using enable_shared_from_this
- The C++ Standard Library Second Edition
- StackOverflow: Is make_shared really more efficient than new?
- The C++ Programming Language 4th Edition