前戲
先拋出兩個問題
- 如果
delete
一個指針,但是它真實的類型和指針類型不一樣會發生什么? - 是誰調用了析構函數?
下面這段代碼會發生什么有趣的事情?
// delete_diff_type.cpp #include <iostream> using namespace std; class Foo { public: Foo() { cout << "Foo()" << endl; } ~Foo() { cout << "~Foo()" << endl; } }; class FakeFoo { public: FakeFoo() { cout << "FakeFoo()" << endl; } ~FakeFoo() { cout << "~FakeFoo()" << endl; } }; int main(int argc, const char * argv[]) { void* vptr = new Foo(); delete vptr; // warning // FakeFoo* ffptr = static_cast<FakeFoo*>(new Foo()); // error FakeFoo* ffptr = reinterpret_cast<FakeFoo*>(new Foo()); delete ffptr; Foo* fptr = new Foo(); delete fptr; return 0; } 輸出: Foo() Foo() ~FakeFoo() Foo() ~Foo()
看一下匯編代碼可以看到main
函數主要做了下面這幾件事
; symbol stub for: operator new(unsigned long) ; Foo::Foo at delete_diff_type.cpp:8 ; symbol stub for: operator delete(void*) ; symbol stub for: operator new(unsigned long) ; Foo::Foo at delete_diff_type.cpp:8 ; FakeFoo::~FakeFoo at delete_diff_type.cpp:16 ; symbol stub for: operator delete(void*) ; symbol stub for: operator new(unsigned long) ; Foo::Foo at delete_diff_type.cpp:8 ; Foo::~Foo at delete_diff_type.cpp:9 ; symbol stub for: operator delete(void*)
從匯編中可以看出,構造造函數和析構函數是編譯器根據指針的類型生成的調用代碼。而且編譯器是不允許沒有繼承關系的指針之間進行轉換的,void*
是個例外,只要不作死用reinterpret_cast
把指針轉換成不相關的類型是不會有問題的。
所以上面兩個問題大概就有答案了。delete
語句會至少產生兩個動作,一個是調用指針對應類型的析構函數,然后去調用operator delete
釋放內存。所以如果delete
的指針和其指向的真實類型不一樣的時候,編譯器只會調用指針類型的析構函數,這也就為什么基類的析構函數需要聲明稱虛函數才能夠保證delete
基類指針的時候子類析構函數能夠被正確的調用。
operator delete
是都會被調用到的,所以指針指向的那塊內存是能夠“正常的”被釋放掉用。
std::shared_ptr<void>
的行為
那么這個跟std::shared_ptr<void>
有什么關系呢?
先看一段代碼
#include <iostream> using namespace std; class Foo { public: Foo() { cout << "Foo()" << endl; } ~Foo() { cout << "~Foo()" << endl; } }; int main(int argc, const char * argv[]) { shared_ptr<void> vptr = shared_ptr<Foo>(new Foo); return 0; } 輸出: Foo() ~Foo()
與第一段代碼中類似,不過把void*
換成了std::shared_ptr<void>
,那么shared_ptr<void>
為什么能夠調用到正確的析構函數呢?一定是shared_ptr
里面搞了什么鬼。
std::shared_ptr<void>
為啥能正常工作
那么就看看源代碼看看到底為啥這貨能夠工作,下面是libcxx中shared_ptr
的部分源碼,把關鍵的部分摳出來了。
/** C1. shared_ptr 構造函數 **/ template<class _Tp> template<class _Yp> shared_ptr<_Tp>::shared_ptr(_Yp* __p, typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type) : __ptr_(__p) { unique_ptr<_Yp> __hold(__p); typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT; typedef __shared_ptr_pointer<_Yp*, default_delete<_Yp>, _AllocT > _CntrlBlk; __cntrl_ = new _CntrlBlk(__p, default_delete<_Yp>(), _AllocT()); __hold.release(); __enable_weak_this(__p, __p); } /** C2. shared_ptr 拷貝構造 **/ template<class _Tp> inline shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl_) { if (__cntrl_) __cntrl_->__add_shared(); } /** C3. shared_ptr::__cntrl_ 類型 **/ __shared_weak_count* __cntrl_; /** C4. shared_ptr 析構函數 **/ template<class _Tp> shared_ptr<_Tp>::~shared_ptr() { if (__cntrl_) __cntrl_->__release_shared(); } /** C5. __shared_weak_count::__release_shared **/ bool __release_shared() _NOEXCEPT { if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -1) { __on_zero_shared(); return true; } return false; } /** C6. __shared_ptr_pointer::__release_shared **/ template <class _Tp, class _Dp, class _Alloc> /* 重點 !!! virtual !!! */ void __shared_ptr_pointer<_Tp, _Dp, _Alloc>::__on_zero_shared() _NOEXCEPT { // __data__ 是 內部工具類__compressed_pair // __data_.first().second()是 deleter // __data_.first().first() 是 shared_ptr<T> 中T類型的指針 __data_.first().second()(__data_.first().first()); __data_.first().second().~_Dp(); } /** C7. default deleter **/ template <class _Tp> struct _LIBCPP_TEMPLATE_VIS default_delete { // ... 此處省略若干行 void operator()(_Tp* __ptr) const _NOEXCEPT { delete __ptr; } };
不要慌,給你慢慢道來。
這個故事簡單得說是這樣的:
- 每一個shared_ptr
內部有一個control block
,里面會存放一個要維護的指針,一個計數,一個刪除器(deleter
),一個分配(allocator
)。這里我們要關心的是刪除器。顧名思義,它是用來刪除指針的。
- shared_ptr
中的 有一個__cntrl_
即control block
。字段的類型是__shared_weak_count
指針,這個類是一個非模板類。shared_ptr<T>
創建的control block
的類型是一個類模板template <class _Tp, class _Dp, class _Alloc> __shared_ptr_pointer
繼承自__shared_weak_count
。 見代碼C3。
- 當創建一個新的shared_ptr
的時候,構建一個新的control block
。見代碼C1。
- 當一個shared_ptr
A賦值或者拷貝構造給另一個shared_ptr
B的時候(當然是在類型能夠轉換的前提下),B會把A的__cntrl_
拷一份,同時將其引用加一。注意,這個時候拷貝的是指針,__cntrl_
還是指向最初創建的那個對象。見代碼C2。
- 當shared_ptr
被析構或者重置的時候會調用__cntrl_->__release_shared()
。見代碼C4。
- __cntrl_->__release_shared()
如果發現當前的計數為-1的時候,調用__on_zero_shared()
。見代碼C5。
- __on_zero_shared
是一個虛方法,那么它就會調用到最初創建的__shared_ptr_pointer
的實現。
- __shared_ptr_pointer
的實現中是有完整的類型和刪除器的信息的。見代碼C6。
- 默認的刪除器很簡單的執行了delete __ptr
操作,因為類型是已知的所以能夠正確的調用到析構函數。見代碼C7。
這種實現方式給shared_ptr
帶來額外好處
Effective C++ 條款07告訴我們“要為多態基類聲明 virtual 析構函數”。當然我認為建議依然有效,但是用了shared_ptr
以后帶來的一個額外好處就是即便你的析構函數忘記寫成virtual
也能幫你正確的調用析構函數。