共享所有权
一个动态分配的对象可以在多个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