我在读《C++Primer(第五版)》智能指针的章节时,看到书中有如下内容
可见作者非常推荐使用make_shared来生成shared_ptr,那么我的疑问就来了,为什么make_shared是更好的方法?经过查阅相关资料和阅读源码,找出原因:make_shared只申请了一次内存,而普通的先new一个raw_ptr,再调用shared_ptr(raw_ptr)是进行了两次内存分配,所以一次分配要比两次分配效率高。
为了下文叙述方便,我们称呼下图中使用make_shared的构造方法为1,先new的方法为2。
shared_ptr<A> sha_a = make_shared<A>(); //方法1
A* a = new A;
shared_ptr<A> sha_b(a); //方法2
既然make_shared节省了内存申请次数,那么看来我们始终都应该使用maked_shared而不应该是方法2了?答案是No!因为make_shared里面有坑,有可能导致对象的内存无法被释放!
回答这个问题前,让我们稍微深究一下C++11的shared_ptr。首先我们必须要认识一个东西:control block!这是什么玩意?和shared_ptr有什么关系?
原来C++11的shared_ptr正是依靠control block记录了强引用和弱引用的数量,从而实现引用计数清零时进行内存释放。只要有shared_ptr的存在,就一定要有一块control block记录引用数量,而这个control block就是所谓的方法2中的“二次内存申请”,换言之,只要用了shared_ptr,就必须为control block开辟一块内存空间。只要有强引用和弱引用的存在,control block就永远不会被释放,记住这一点,下面make_shared的坑就和这点有关。
如果是这样的话,那么按照常理猜想,就应该实例是一块内存空间,control block是一块内存空间,control block记录了实例的地址,从而控制实例的销毁。Good!我们猜想的与方法2实际做法完全一致,实例内存与control block内存的关系如下图
图中黄色是control block内存,橘黄色R是实例内存,当强引用计数为0时,R内存被释放,当弱引用也为0时,control block被释放。OK,方法2一切按照预期发展,没问题。
现在来到make_shared的情况,make_shared之所以能够做到一次内存申请就达到同时为实例和control block分配内存的目标,是因为make_shared把二者合并为了一块内存,申请时一并申请了!memory.h里面make_shared的代码为
template<class _Ty,
class... _Types>
_NODISCARD inline shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
const auto _Rx = new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
return (_Ret);
}
这个_Ref_count_obj就是一块连续空间,定义如下
template<class _Ty>
class _Ref_count_obj
: public _Ref_count_base
{ // handle reference counting for object in control block, no allocator
public:
template<class... _Types>
explicit _Ref_count_obj(_Types&&... _Args)
: _Ref_count_base()
{ // construct from argument list
::new (static_cast<void *>(&_Storage)) _Ty(_STD forward<_Types>(_Args)...);
}
_Ty * _Getptr()
{ // get pointer
return (reinterpret_cast<_Ty *>(&_Storage));
}
private:
virtual void _Destroy() noexcept override
{ // destroy managed resource
_Getptr()->~_Ty();
}
virtual void _Delete_this() noexcept override
{ // destroy self
delete this;
}
aligned_union_t<1, _Ty> _Storage;
};
_Ref_count_obj里面有个成员_Storage,这个变量就是用来存储实例内存的,而_Ref_count_base就是control block,里面记录了强引用和弱引用的计数。
两个内存的逻辑关系如下图所示
从这张图也可以看出,control block和R被合并为了一块内存空间,哦,原来这样,怪不得申请一次就够了!But Wait,我们申请的是一个_Ref_count_obj,那我们释放了R后(也就是源码中的_Storage),怎么保证R的内存被真正归还了?答案是,R的内存逻辑上不存在了,但它其实并没有被归还,还和control block呆在一起,只是逻辑上触及不到而已。只有control block里面的弱引用也为0之后,control block被释放,R的内存才真正被一起归还。由此可见,即使强引用计数归0了,实例的内存也没有被释放掉(这个时候Object虽然调用了析构函数,但并不意味着内存被归还了),而这就是make_shared坑!
所以我们要记住一点,使用make_shared固然好,但请记住及时清理share_ptr和weak_ptr,只要还有一个引用,即使是弱引用,目标内存也是无法真正被释放的!
关于本文中的图片,来源于一个youtube视频,视频介绍了C++11的智能指针知识,值得一看,地址:C++ Smart Pointers