如果不熟悉C++带引用计数的智能指针shared_ptr和weak_ptr,请参考我的另一篇介绍智能指针的博客: https://blog.csdn.net/QIANGWEIYUAN/article/details/88562935
鉴于同学们提问的一些有关智能指针的问题,这篇文章主要介绍C++11提供的智能指针相关的enable_shared_from_this和shared_from_this机制
一、问题代码
先给出两个智能指针的应用场景代码,都是有问题的,仔细思考一下问题的原因。
1. 错误代码一
#include <iostream>
using namespace std;
// 智能指针测试类
class A
{
public:
A():mptr(new int)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
delete mptr;
mptr = nullptr;
}
private:
int *mptr;
};
int main()
{
A *p = new A(); // 裸指针指向堆上的对象
shared_ptr<A> ptr1(p);// 用shared_ptr智能指针管理指针p指向的对象
shared_ptr<A> ptr2(p);// 用shared_ptr智能指针管理指针p指向的对象
// 下面两次打印都是1,因此同一个new A()被析构两次,逻辑错误
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代码打印如下:
A()
1
1
~A()
~A()
main函数中,虽然用了两个智能指针shared_ptr,但是它们管理的都是同一个资源,资源的引用计数应该是2,为什么打印出来是1呢? 导致出main函数把A对象析构了两次,不正确!如果你有这样的疑问,说明对于shared_ptr的底层原理还没有完全搞清楚。
2. 错误代码二
#include <iostream>
using namespace std;
// 智能指针测试类
class A
{
public:
A():mptr(new int)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
delete mptr;
mptr = nullptr;
}
// A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针。
shared_ptr<A> getSharedPtr()
{
/*注意:不能直接返回this,在多线程环境下,根本无法获知this指针指向
的对象的生存状态,通过shared_ptr和weak_ptr可以解决多线程访问共享
对象的线程安全问题,参考我的另一篇介绍智能指针的博客*/
return shared_ptr<A>(this); // 显式调用普通构造,然后右值拷贝构造ptr2
}
private:
int *mptr;
};
int main()
{
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
/* 按原先的想法,上面两个智能指针管理的是同一个A对象资源,但是这里打印都是1
导致出main函数A对象析构两次,析构逻辑有问题*/
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代码运行打印如下:
A()
1
1
~A()
~A()
代码同样有错误,A对象被析构了两次,而且看似两个shared_ptr指向了同一个A对象资源,但是资源计数并没有记录成2,还是1,不正确。
二、shared_ptr原理分析
如果你能够理解上面代码的问题所在,那么直接跳到下一节看上面错误代码的解决方案;如果不明白问题的所在,通过下面的源码介绍,仔细理解shared_ptr的实现原理。
源码上shared_ptr的定义如下:
template<class _Ty>
class shared_ptr : public _Ptr_base<_Ty>
shared_ptr是从_Ptr_base继承而来的,作为派生类,shared_ptr本身没有提供任何成员变量,但是它从基类_Ptr_base继承来了如下成员变量(只罗列部分源码):
template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
protected:
void _Decref()
{ // decrement reference count
if (_Rep)
{
_Rep->_Decref();
}
}
void _Decwref()
{ // decrement weak reference count
if (_Rep)
{
_Rep->_Decwref();
}
}
private:
// _Ptr_base的两个成员变量,这里只罗列了_Ptr_base的部分代码
element_type * _Ptr{nullptr}; // 指向资源的指针
_Ref_count_base * _Rep{nullptr}; // 指向资源引用计数的指针
};
_Ref_count_base 记录资源的类是怎么定义的呢,如下(只罗列部分源码):
class __declspec(novtable) _Ref_count_base
{ // common code for reference counting
private:
/* _Uses记录了资源的引用计数,也就是引用资源的shared_ptr
的个数;_Weaks记录了weak_ptr的个数,相当于资源观察者的
个数,都是定义成基于CAS操作的原子类型,增减引用计数时时
线程安全的操作
*/
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;
}
也就是说,当我们定义一个shared_ptr< int > ptr(new int)的智能指针对象时,该智能指针对象本身的内存是8个字节,如下图所示:
那么把智能指针管理的外部资源以及引用计数资源都画出来的话,就是如下图的展示:
有的同学研究智能指针,研究的比较深入,最终发现,我们在课堂上实现的那个带引用计数的智能指针,和C++11库中提供的shared_ptr在资源计数管理方式上还是有区别的,不能混为一谈。
当你做这样的代码操作时:
shared_ptr<int> ptr1(new int);
shared_ptr<int> ptr2(ptr1); // 拷贝构造
cout<<ptr1.use_count()<<endl; // 2
cout<<ptr2.use_count()<<endl; // 2
这段代码没有任何问题,ptr1和ptr2管理了同一个资源,引用计数打印出来的都是2,出函数作用域依次析构,最终new int资源只释放一次,逻辑正确! 这是因为shared_ptr ptr2(ptr1)调用了shared_ptr的拷贝构造函数,部分源码如下:
shared_ptr(const shared_ptr& _Other) noexcept { // construct shared_ptr object that owns same resource as _Other
this->_Copy_construct_from(_Other);
}
template <class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other) noexcept {
// implement shared_ptr's (converting) copy ctor
_Other._Incref();
_Ptr = _Other._Ptr; // 指向资源的指针
_Rep = _Other._Rep; // // 指向引用计数对象的指针
}
可以看到,shared_ptr的拷贝构造函数中,新构造对象的成员_Ptr 、_Rep用已存在对象进行赋值,只是做了资源的引用计数的改变,没有额外分配其它资源,如下图所示:
但是当你做如下代码操作时:
int *p = new int;
shared_ptr<int> ptr1(p); // 普通构造
shared_ptr<int> ptr2(p);
cout<<ptr1.use_count()<<endl; // 1
cout<<ptr2.use_count()<<endl; // 1
这段代码就有问题了,因为shared_ptr ptr1( p )和shared_ptr ptr2( p ) 都调用了shared_ptr的构造函数,在它的构造函数中,都重新开辟了引用计数的资源,导致ptr1和ptr2都记录了一次new int的引用计数,都是1,析构的时候它俩都去释放内存资源,导致释放逻辑错误,如下图所示:
上面两个代码段,分别是shared_ptr的构造函数和拷贝构造函数做的事情,导致虽然都是指向同一个new int资源,但是对于引用计数对象的管理方式,这两个函数是不一样的,构造函数是新分配引用计数对象,拷贝构造函数只做引用计数增减。
相信说到这里,大家知道最开始的两个代码清单上的代码为什么出错了吧,因为每次调用的都是shared_ptr的构造函数,虽然大家管理的资源都是一样的,_Ptr都是指向同一个堆内存,但是_Rep却指向了不同的引用计数对象,并且都记录引用计数是1,出作用域都去析构,导致问题发生!
三、问题解决
1. 错误代码一修改
那么清单1的代码修改很简单,就是在产生同一资源的多个shared_ptr的时候,通过拷贝构造函数或者赋值operator=函数进行,不要重新构造,避免产生多个引用计数对象,代码修改如下:
int main()
{
A *p = new A(); // 裸指针指向堆上的对象
shared_ptr<A> ptr1(p);// 用shared_ptr智能指针管理指针p指向的对象
shared_ptr<A> ptr2(ptr1);// 用ptr1拷贝构造ptr2
// 下面两次打印都是2,最终随着ptr1和ptr2析构,资源只释放一次,正确!
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
2. 错误代码二修改
enable_shared_from_this和shared_from_this
那么错误代码二怎么修改呢?注意我们有时候想在类里面提供一些方法,返回当前对象的一个shared_ptr强智能指针,做参数传递使用(多线程编程中经常会用到)
首先肯定不能像上面代码清单2那样写return shared_ptr< A > ( this ) ,这会调用shared_ptr智能指针的构造函数,对this指针指向的对象,又建立了一份引用计数对象,加上main函数中的shared_ptr< A > ptr1(new A())已经对这个A对象建立的引用计数对象,又成了两个引用计数对象,对同一个资源都记录了引用计数,为1,最终两次析构对象释放内存,错误!
那如果一个类要提供一个函数接口,返回一个指向当前对象的shared_ptr智能指针怎么办?方法就是继承enable_shared_from_this类,然后通过调用从基类继承来的shared_from_this()方法返回指向同一个资源对象的智能指针shared_ptr
修改如下:
#include <iostream>
using namespace std;
// 智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A>
{
public:
A() :mptr(new int)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
delete mptr;
mptr = nullptr;
}
// A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
shared_ptr<A> getSharedPtr()
{
/*通过调用基类的shared_from_this方法得到一个指向当前对象的
智能指针*/
return shared_from_this();
}
private:
int *mptr;
};
一个类继承enable_shared_from_this会怎么样?看看enable_shared_from_this基类的成员变量有什么,如下:
template<class _Ty>
class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
using _Esft_type = enable_shared_from_this;
_NODISCARD shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
// 成员变量是一个指向资源的弱智能指针
mutable weak_ptr<_Ty> _Wptr;
};
也就是说,如果一个类继承了enable_shared_from_this,那么它产生的对象就会从基类enable_shared_from_this继承一个成员变量_Wptr,当定义第一个智能指针对象的时候shared_ptr< A > ptr1(new A()),调用shared_ptr的普通构造函数shared_ptr(_Ux* _Px)
,就会初始化A对象的成员变量_Wptr,作为观察A对象资源的一个弱智能指针观察者
explicit shared_ptr(_Ux* _Px) { // construct shared_ptr object that owns _Px
if constexpr (is_array_v<_Ty>) {
_Setpd(_Px, default_delete<_Ux[]>{});
} else {
_Temporary_owner<_Ux> _Owner(_Px);
_Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<_Ux>(_Owner._Ptr));
_Owner._Ptr = nullptr;
}
}
template <class _Ux>
void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept { // take ownership of _Px
this->_Ptr = _Px;
this->_Rep = _Rx;
if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
if (_Px && _Px->_Wptr.expired()) {
_Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
}
}
}
以上源代码就是在shared_ptr的普通构造函数中给weak_ptr _Wptr赋值,观察A对象资源
shared_from_this方法会使用_Wptr构造一个shared_ptr对象,这个构造过程并没有创建新的引用计数对象,只是将引用计数+1,然后让新的shared_ptr指向资源。使用weak_ptr构造shared_ptr源代码如下:
template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
explicit shared_ptr(const weak_ptr<_Ty2>& _Other) { // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) {
_Throw_bad_weak_ptr();
}
}
template <class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other) noexcept {
// implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
if (_Other._Rep && _Other._Rep->_Incref_nz()) {
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
return true;
}
return false;
}
然后代码如下调用shared_ptr< A > ptr2 = ptr1->getSharedPtr(),getSharedPtr函数内部调用shared_from_this()函数返回指向该对象的智能指针,这个函数怎么实现的呢,看源码:
class enable_shared_from_this {
public:
shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
private:
mutable weak_ptr<_Ty> _Wptr;
}
shared_ptr< _Ty >(_Wptr),说明通过当前enable_shared_from_this对象的成员变量_Wptr构造一个shared_ptr出来,看看shared_ptr相应的构造函数:
shared_ptr(const weak_ptr<_Ty2>& _Other)
{ // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 从弱智能指针提升一个强智能指针
{
_THROW(bad_weak_ptr{});
}
}
_Construct_from_weak方法先增加对资源引用的shared_ptr的数量(_Ref_count_base::_Uses),然后用weak_ptr给新对象shared_ptr的指向资源的指针以及指向引用计数对象的指针做初始化,_Construct_from_weak方法的实现如下:
template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{ // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通过判断资源的引用计数是否还在,判定对象的存活状态,对象存活,提升成功;
// 对象析构,提升失败!之前的博客内容讲过这些知识,可以去参考!
// _Incref_nz就是增加对资源引用的shared_ptr的数量_Ref_count_base::_Uses
if (_Other._Rep && _Other._Rep->_Incref_nz())
{
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
return (true);
}
return (false);
}
综上所述,所有过程都没有再使用shared_ptr的普通构造函数,没有在产生额外的引用计数对象,不会存在把一个内存资源,进行多次计数的过程;更关键的是,通过weak_ptr到shared_ptr的提升,还可以在多线程环境中判断对象是否存活或者已经析构释放,在多线程环境中是很安全的,通过this裸指针进行构造shared_ptr,不仅仅资源会多次释放,而且在多线程环境中也不确定this指向的对象是否还存活。
最终代码二修改如下:
#include <iostream>
using namespace std;
// 智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A>
{
public:
A() :mptr(new int)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
delete mptr;
mptr = nullptr;
}
// A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
shared_ptr<A> getSharedPtr()
{
/*通过调用基类的shared_from_this方法得到一个指向当前对象的
智能指针*/
return shared_from_this();
}
private:
int *mptr;
};
int main()
{
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
// 引用计数打印为2
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代码打印如下:
A()
2
2
~A()
打印完全正确,A对象构造一次,析构一次,引用计数为2
shared_from_this怎么解决使用普通构造方法开辟多个引用计数对象,导致计数错误,析构崩溃的问题?
A对象继承了enable_shared_from_this后,会有一个弱智能指针变量_Wptr,一旦使用new A调用普通构造方法生成shared_ptr对象时,shared_ptr的普通构造会调用_Set_ptr_rep_and_enable_shared方法,会使用正在构造的shared_ptr对象初始化A对象的_Wptr成员,调用调用shared_from_this方法时,就会使用当前A对象的_Wptr成员调用shared_ptr特定的构造方法,而不会重新开辟引用计数对象
注意下面写法是错误的
A* p1 = new A();
shared_ptr<Test> p2 = p1->getSharedPtr();
由于没有调用到shared_ptr的普通构造函数,所以enable_shared_from_this类的私有成员_Wptr并没有进行初始化,当我们使用shared_from_this时,会使用_Wptr构造shared_ptr,即执行如下函数:
shared_ptr(const weak_ptr<_Ty2>& _Other)
{ // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 从弱智能指针提升一个强智能指针
{
_THROW(bad_weak_ptr{});
}
}
由于_Wptr没有初始化,_Construct_from_weak会返回false,就会抛出异常