緣起
最近在補一些基礎知識,恰好涉及到了智能指針std::weak_ptr在解決std::shared_ptr時候循環引用的問題,如下:
classA{ public: std::weak_ptrb_ptr; }; classB{ public: std::weak_ptra_ptr; }; autoa=std::make_shared(); autob=std::make_shared(); a->b_ptr=b; b->a_ptr=a;
就問了下,通常的用法是將A或者B中間的某一個變量聲明為std::weak_ptr,如果兩者都聲明為std::weak_ptr會有什么問題?
咱們先不論這個問題本身,在隨后的討論中,風神突然貼了段代碼:
#include#include #include usingnamespacestd; structA{ charbuffer[1024*1024*1024];//1GB weak_ptrnext; }; intmain(){ while(true){ autoa0=make_shared(); autoa1=make_shared(); autoa2=make_shared(); a0->next=a1; a1->next=a2; a2->next=a0; //thisweak_ptrleak: newweak_ptr{a0}; this_thread::sleep_for(chrono::seconds(3)); } return0; }
說實話,當初看了這個代碼第一眼,是存在內存泄漏的(new一個weak_ptr沒有釋放),而沒有理解風神這段代碼真正的含義,于是在本地把這段代碼編譯運行了下,我的乖乖,內存占用如圖:
emm,雖然存在內存泄漏,但也不至于這么大,于是網上進行了搜索,直至我看到了下面這段話:
make_shared 只分配一次內存, 這看起來很好. 減少了內存分配的開銷. 問題來了, weak_ptr 會保持控制塊(強引用, 以及弱引用的信息)的生命周期, 而因此連帶著保持了對象分配的內存, 只有最后一個 weak_ptr 離開作用域時, 內存才會被釋放. 原本強引用減為 0 時就可以釋放的內存, 現在變為了強引用, 若引用都減為 0 時才能釋放, 意外的延遲了內存釋放的時間. 這對于內存要求高的場景來說, 是一個需要注意的問題.
如果介意上面new那點泄漏的話,不妨修改代碼如下:
#include#include #include usingnamespacestd; structA{ charbuffer[1024*1024*1024];//1GB weak_ptrnext; }; intmain(){ std::weak_ptrwptr; { autosptr=make_shared(); wptr=sptr; } this_thread::sleep_for(chrono::seconds(30)); return0; }
也就是說,對于std::shared_ptr ptr(new Obj),形如下圖:
而對于std::make_shared,形如下圖:
好了,理由上面已經說明白了,不再贅述了,如果你想繼續分析的話,請看下文,否則~~
原因
雖然上節給出了原因,不過還是好奇心驅使,想從源碼角度去了解下,于是打開了好久沒看的gcc源碼。
std::make_shared
首先看下它的定義:
templateinlineshared_ptr<_Tp>make_shared(_Args&&...__args){ typedeftypenamestd::remove_cv<_Tp>::type_Tp_nc; returnstd::allocate_shared<_Tp>(std::allocator<_Tp_nc>(), std::forward<_Args>(__args)...); }
這個函數函數體只有一個std::allocate_shared,接著看它的定義:
templateinlineshared_ptr<_Tp> allocate_shared(const_Alloc&__a,_Args&&...__args){ returnshared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a}, std::forward<_Args>(__args)...); }
創建了一個shared_ptr對象,看下其對應的構造函數:
templateshared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args) :__shared_ptr<_Tp>(__tag,std::forward<_Args>(__args)...){}
接著看__shared_ptr這個類對應的構造函數:
template__shared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args) :_M_ptr(),_M_refcount(_M_ptr,__tag,std::forward<_Args>(__args)...) {_M_enable_shared_from_this_with(_M_ptr);}
其中,_M_refcount的類型為__shared_count,也就是說我們通常所說的引用計數就是由其來管理。
因為調用make_shared函數,所以這里的_M_ptr指針也就是相當于一個空指針,然后繼續看下_M_refcount(請注意_M_ptr作為參數傳入)定義:
template__shared_count(_Tp*&__p,_Sp_alloc_shared_tag<_Alloc>__a,_Args&&...__args){ typedef_Sp_counted_ptr_inplace<_Tp,?_Alloc,?_Lp>_Sp_cp_type;//L1 typename_Sp_cp_type::__allocator_type__a2(__a._M_a);//L2 auto__guard=std::__allocate_guarded(__a2); _Sp_cp_type*__mem=__guard.get();//L3 auto__pi=::new(__mem)_Sp_cp_type(__a._M_a,std::forward<_Args>(__args)...);//L4 __guard=nullptr; _M_pi=__pi; __p=__pi->_M_ptr();//L5 }
這塊代碼當時看了很多遍,一直不明白在沒有顯示分配對象內存的情況下,是如何使用placement new的,直至今天上午,靈光一閃,突然明白了,且聽慢慢道來。
首先看下L1行,其聲明了模板類_Sp_counted_ptr_inplace的別名為_Sp_cp_type,其定義如下:
templateclass_Sp_counted_ptr_inplacefinal:public_Sp_counted_base<_Lp> { class_Impl:_Sp_ebo_helper<0,?_Alloc> { typedef_Sp_ebo_helper<0,?_Alloc>_A_base; public: explicit_Impl(_Alloc__a)noexcept:_A_base(__a){} _Alloc&_M_alloc()noexcept{return_A_base::_S_get(*this);} __gnu_cxx::__aligned_buffer<_Tp>_M_storage; }; public: using__allocator_type=__alloc_rebind<_Alloc,?_Sp_counted_ptr_inplace>; //Allocparameterisnotareferencesodoesn'taliasanythingin__args template _Sp_counted_ptr_inplace(_Alloc__a,_Args&&...__args) :_M_impl(__a) { //_GLIBCXX_RESOLVE_LIB_DEFECTS //2070.allocate_sharedshoulduseallocator_traits::construct allocator_traits<_Alloc>::construct(__a,_M_ptr(), std::forward<_Args>(__args)...);//mightthrow } ~_Sp_counted_ptr_inplace()noexcept{} virtualvoid _M_dispose()noexcept { allocator_traits<_Alloc>::destroy(_M_impl._M_alloc(),_M_ptr()); } //Overridebecausetheallocatorneedstoknowthedynamictype virtualvoid _M_destroy()noexcept { __allocator_type__a(_M_impl._M_alloc()); __allocated_ptr<__allocator_type>__guard_ptr{__a,this}; this->~_Sp_counted_ptr_inplace(); } private: friendclass__shared_count<_Lp>;//Tobeabletocall_M_ptr(). _Tp*_M_ptr()noexcept{return_M_impl._M_storage._M_ptr();} _Impl_M_impl; };
這個類繼承于_Sp_counted_base,這個類定義不再次列出,需要注意的是其中有兩個變量:
_Atomic_word_M_use_count;//#shared _Atomic_word_M_weak_count;//#weak+(#shared!=0)
第一個為強引用技術,也就是shared對象引用計數,另外一個為弱因為計數。
繼續看這個類,里面定義了一個class _Impl,其中我們創建的對象類型就在這個類里面定義,即**__gnu_cxx::__aligned_buffer<_Tp> _M_storage;**
接著看L2,這行定義了一個對象__a2,其對象類型為using __allocator_type = __alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>;,這行的意思是重新封裝rebind_alloc<_Sp_counted_ptr_inplace>
繼續看L3,在這一行中會創建一塊內存,這塊內存中按照順序為創建對象、強引用計數、弱引用計數等(也就是說分配一大塊內存,這塊內存中 包含對象、強、弱引用計數所需內存等),在創建這塊內存的時候,強、弱引用計數已經被初始化
最后是L3,這塊調用了placement new來創建,其中調用了對象的構造函數:
template_Sp_counted_ptr_inplace(_Alloc__a,_Args&&...__args) :_M_impl(__a) { //_GLIBCXX_RESOLVE_LIB_DEFECTS //2070.allocate_sharedshoulduseallocator_traits::construct allocator_traits<_Alloc>::construct(__a,_M_ptr(), std::forward<_Args>(__args)...);//mightthrow }
至此,整個std::make_shared流量已經完整的梳理完畢,最后返回一個shared_ptr對象。
好了,下面繼續看下令人迷惑的,存在大內存不分配的這行代碼:
newweak_ptr{a0};
其對應的構造函數如下:
template> __weak_ptr(const__shared_ptr<_Yp,?_Lp>&__r)noexcept :_M_ptr(__r._M_ptr),_M_refcount(__r._M_refcount) {}
其中_M_refcount的類型為__weak_count,而\__r._M_refcount即常說的強引用計數類型為__shared_count,其繼承于接著往下看:
__weak_count(const__shared_count<_Lp>&__r)noexcept :_M_pi(__r._M_pi) { if(_M_pi!=nullptr) _M_pi->_M_weak_add_ref(); }
emm,弱引用計數加1,也就是說此時_M_weak_count為1。
接著,退出作用域,此時有std::make_shared創建的對象開始釋放,因此其內部的成員變量r._M_refcount也跟著釋放:
~__shared_count()noexcept { if(_M_pi!=nullptr) _M_pi->_M_release(); }
接著往下看_M_release()實現:
template<> inlinevoid _Sp_counted_base<_S_single>::_M_release()noexcept { if(--_M_use_count==0) { _M_dispose(); if(--_M_weak_count==0) _M_destroy(); } }
此時,因為shared_ptr對象的引用計數本來就為1(沒有其他地方使用),所以if語句成立,執行_M_dispose()函數,在分析這個函數之前,先看下前面提到的代碼:
__shared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args) :_M_ptr(),_M_refcount(_M_ptr,__tag,std::forward<_Args>(__args)...) {_M_enable_shared_from_this_with(_M_ptr);}
因為是使用std::make_shared()進行創建的,所以_M_ptr為空,此時傳入_M_refcount的第一個參數也為空。接著看_M_dispose()定義:
template<> inlinevoid _Sp_counted_ptr::_M_dispose()noexcept{} template<> inlinevoid _Sp_counted_ptr ::_M_dispose()noexcept{} template<> inlinevoid _Sp_counted_ptr ::_M_dispose()noexcept{}
因為傳入的指針為nullptr,因此調用了_Sp_counted_ptr的特化版本,因此_M_dispose()這個函數什么都沒做。因為_M_pi->_M_weak_add_ref();這個操作,此時這個計數經過減1之后不為0,因此沒有沒有執行_M_destroy()操作,因此之前申請的大塊內存沒有被釋放,下面是_M_destroy()實現:
virtualvoid _M_destroy()noexcept { __allocator_type__a(_M_impl._M_alloc()); __allocated_ptr<__allocator_type>__guard_ptr{__a,this}; this->~_Sp_counted_ptr_inplace(); }
也就是說真正調用了這個函數,內存才會被分配,示例代碼中,顯然不會,這就是造成內存一直不被釋放的原因。
總結
下面解釋下我當時閱讀這塊代碼最難理解的部分,下面是make_shared執行過程:
下面是析構過程:
整體看下來,比較重要的一個類就是_Sp_counted_base 不僅充當引用計數功能,還充當內存管理功能。從上面的分析可以看到,_Sp_counted_base負責釋放用戶申請的申請的內存,即
?當 _M_use_count 遞減為 0 時,調用 _M_dispose() 釋放 *this 管理的資源?當 _M_weak_count 遞減為 0 時,調用 _M_destroy() 釋放 *this 對象
審核編輯:劉清
-
STD
+關注
關注
0文章
36瀏覽量
14358 -
變量
+關注
關注
0文章
613瀏覽量
28360 -
內存泄漏
+關注
關注
0文章
39瀏覽量
9215
原文標題:一次詭異的內存泄漏
文章出處:【微信號:CPP開發者,微信公眾號:CPP開發者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論