故事的起因是這樣的。我們在代碼中發現了類似如下的代碼:

struct Base
{
~Base()
{

}
};

struct Derived : Base
{
~Derived()
{

}
};

int main()
{
Base* base = new Derived();
delete base ;

return 0;
}

這個真的是一個喜聞樂見的場景。我們也經常會拿來問面試的同學,當你delete base的時候,會發生什麼?

答案是,可能會造成內存泄露。其原因在於,基類的析構函數不是虛函數,因此只能調用到Base的析構函數~Base,而Derived的析構函數和它本身並不會被調用,造成了部分析構,從而導致內存泄露。解決方法很簡單,在~Base前加上virtual就可以了。

後來,為了秉承RAII思想,代碼被換成了下面這樣:

#include <memory>
struct Base
{
~Base()
{

}
};

struct Derived : Base
{
~Derived()
{

}
};

int main()
{
std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());
return 0;
}

這不是換湯不換嗎?當應用程序結束時,智能指針被析構,而base對象指向的是一個Base*類型的結構體指針,同樣會造成部分析構問題。

如果你這麼想,那就大錯特錯了。

事實上,如果給代碼打上斷點,你會清楚發現,編譯器是執行了Derived的析構函數。你可能會感到奇怪,我的base不是指向Base嗎,那為什麼會調用Derived的析構函數呢?

下面,我們就來簡單的聊一下shared_ptr的實現。

1. VS下的shared_ptr的實現

VS自帶的STL,shared_ptr繼承自_Ptr_base。_Ptr_base的成員有兩個,一個是引用計數_Rep,還有一個就是shared_ptr所指向的指針_Ptr

從簡單的說起,_Ptr很好理解,也就是傳給shared_ptr和構造函數。例如:

shared_ptr<P> p = make_shared<P>();

這樣,_Ptr的類型就為P*。智能指針重載的operator-> ,以及函數get,其實就是返回P*,這樣我們就可以像使用一個普通指針那樣來使用它了。

稍微麻煩一點的是這個成員_Rep,它的類型是_Ref_count_base,從名字上來看,它是用來管理智能指針的生命週期的。它不僅提供了引用計數的功能、創建對象功能,還有提供了一個非常重要的純虛函數Destroy。可想而知,當創建一個智能指針的時候,智能指針會創建一個_Ref_count_base的派生類實例,如_Ref_count_obj實例。在智能指針引用計數為0時,_Ref_count_base的Destroy被調用,託管的對象被刪除,同時它自己也被刪除。

在我們拷貝智能指針的時候,_Rep也被拷貝。而_Rep中的Destroy是刪除最初賦值時的對象,所以和新的智能指針類型無關。

2. 詳細流程

int main()
{
std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());
return 0;
}

我們重新梳理一下剛剛的代碼。看看這段代碼到底發生了什麼事情。

一. make_shared被調用,make_shared主要做了兩件事情:1) 創建一個引用計數實例_Rep,它派生於_Ref_count_base。2) 將新建的對象賦值給shared_ptr的_Ptr。

template<class _Ty,
class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);

shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}

以上是我截取出來的VS下的make_shared實現。_Ty的類型是Derived。那麼,_Ref_count_obj<_Ty>*_Rx其實就是_Ref_count_obj<Derived>*_Rx,它的Destroy方法其實調用的是~Derived。shared_ptr<_Ty> _Ret其實也就是shared_ptr<Derived> _Ret。_Rx也將承擔創建對象的責任,並且通過_Resetp0將創建出來的對象(類型是Derived*)和_Ref_count_base傳遞給shared_ptr 。此時引用計數為1。

二. std::shared_ptr移動構造函數被調用。

上段代碼中,return (_Ret);返回的是一個右值shared_ptr<Derived>,並且馬上傳遞給了std::shared_ptr<Base>,因此,下面版本的構造函數被調用:

template<class _Ty2,
class = typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,
void>::type>
shared_ptr(shared_ptr<_Ty2>&& _Right) _NOEXCEPT
: _Mybase(_STD move(_Right))
{ // construct shared_ptr object that takes resource from _Right
}

需要注意的是,這裡的_Ty2是Derived,而_Ty是Base。由於Derived*和Base*是可轉換的,因此符合enable_if的條件,此構造函數是可以被實例化出來的。這個構造函數其實什麼都沒有做,只是繼續將它傳遞給基類_Ptr_base,執行以下函數:

template<class _Ty2>
_Ptr_base(_Ptr_base<_Ty2>&& _Right)
: _Ptr(_Right._Ptr), _Rep(_Right._Rep)
{ // construct _Ptr_base object that takes resource from _Right
_Right._Ptr = 0;
_Right._Rep = 0;
}

看到這裡我們就明白了,其實std::shared_ptr<Base>(std::make_shared<Derived>())只是交換了智能指針的_Ptr和_Rep。我們並沒有在std::shared_ptr<Base>構造(或賦值)的時候創建一個新的_Rep,它沿用的是最開始創建的_Rep,所以它刪除的是指向派生類的對象。到這裡,引用計數還是為1.

三. 移動構造賦值給base。

std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());

等號的右半邊都執行完了,生成了個引用計數為1的智能指針,這個時候可以將它移動給base了。事實上編譯器可能都不會生成移動構造指令,而是直接在base上進行之前的操作。不管怎樣,你需要知道的是,base的引用計數為1,_Ptr為新建的Derived實例,迫於自己的Base類型,使用get()或者->的時候,只能拿出Base*類型,但是刪除的時候,調用的是~Derived。

最終,程序結束,引用計數為0,對象被釋放,~Derived被調用。

以上,便是這一行代碼所發生的幾乎全部的事情。希望對大家有所幫助,明白了它底層原理,能夠讓大家減少使用智能指針出錯的幾率。


補充:

有大佬說不建議以某個具體實現來理解代碼,我的看法是這樣的。

當遇到一個問題(或者bug)時,第一時間應該是翻閱文檔。尤其是STL、Qt這樣的庫,文檔一定會寫得非常詳細。尤其是STL,它是一套具體的規範,並沒有規定每一個細節,VS和GCC的實現是不一樣的。不過,它一定遵循文檔。

當仔細閱讀完文檔之後,如果有時間的話,可以思考如果是自己造輪子,應該如何實現。這個時候,就可以以某一個具體實現為參考,瞭解它所做的事情,這樣下來,會更加有助於加深印象。


推薦閱讀:
相關文章