c++手擼智能指針的教程分享
前言
大家好,今天是【重學c++】的第三講,書接上回,第二講《02 脫離指針陷阱:深入淺出 c++ 智能指針》介紹了c++智能指針的一些使用方法和基本原理。今天,我們自己動手,從0到1實現一下自己的unique_ptr和shared_ptr。
回顧
智能指針的基本原理是基于raii設計理論,自動回收內存資源,從根本上避免內存泄漏。在第一講《01 c++ 如何進行內存資源管理?》介紹raii的時候,就已經給了一個用于封裝int類型指針,實現自動回收資源的代碼實例:
class?autointptr?{ public: ????autointptr(int*?p?=?nullptr)?:?ptr(p)?{} ????~autointptr()?{?delete?ptr;?} ????int&?operator*()?const?{?return?*ptr;?} ????int*?operator->()?const?{?return?ptr;?} private: ????int*?ptr; };
我們從這個示例出發,一步步完善我們自己的智能指針。
模版化
這個類有個明顯的問題:只能適用于int類指針。所以我們第一步要做的,就是把它改造成一個類模版,讓這個類適用于任何類型的指針資源。code show time
template?<typename?t> class?smart_ptr?{ public: ?explicit?smart_ptr(t*?ptr?=?nullptr):?ptr_(ptr)?{} ?~smart_ptr()?{ ??delete?ptr_; ?} ?t&?operator*()?const?{?return?*ptr_;?} ?t*?operator->()?const?{?return?ptr_;?} private: ?t*?ptr_; }
我給我們的智能指針類用了一個更抽象,更切合的類名:smart_ptr。
和autointptr相比,我們把smart_ptr設計成一個類模版,原來代碼中的int改成模版參數t,非常簡單。使用時也只要把autointptr(new int(9))改成smart_ptr<int>(new int(9))即可。
另外,有一點值得注意,smart_ptr的構造函數使用了explicit,explicit關鍵字主要用于防止隱式的類型轉換。代碼中,如果原生指針隱式地轉換為智能指針類型可能會導致一些潛在的問題。至于會有什么問題,你那聰明的小腦瓜看完下面的代碼肯定能理解了:
void?foo(smart_ptr<int>?int_ptr)?{ ????//?... } int?main()?{ ????int*?raw_ptr?=?new?int(42); ????foo(raw_ptr);??//?隱式轉換為?smart_ptr<int> ????std::cout?<<?*raw_ptr?<<?std::endl;???//?error:?raw_ptr已經被回收了 ????//?... }
假設我們沒有為smart_ptr構造函數加上explicit,原生指針raw_ptr在傳給foo函數后,會被隱形轉換為smart_ptr<int>,foo函數調用結束后,棲構入參的smart_ptr<int>時會把raw_ptr給回收掉了,所以后續對raw_ptr的調用都會失敗。
拷貝還是移動
當前我們沒有為smart_ptr自定義拷貝構造函數/移動構造函數,c++會為smart_ptr生成默認的拷貝/移動構造函數。默認的拷貝/移動構造函數邏輯很簡單:把每個成員變量拷貝/移動到目標對象中。
按當前smart_ptr的實現,我們假設有以下代碼:
smart_ptr<int>?ptr1{new?int(10)}; smart_ptr<int>?ptr2?=?ptr1;
這段代碼在編譯時不會出錯,問題在運行時才會暴露出來:第二行將ptr1管理的指針復制給了ptr2,所以會重復釋放內存,導致程序奔潰。
為了避免同一塊內存被重復釋放。解決辦法也很簡單:
- 獨占資源所有權,每時每刻一個內存對象(資源)只能有一個smart_ptr占有它。
- 一個內存對象(資源)只有在最后一個擁有它的smart_ptr析構時才會進行資源回收。
獨占所有權 - unique_smart_ptr
獨占資源的所有權,并不是指禁用掉smart_ptr的拷貝/移動函數(當然這也是一種簡單的避免重復釋放內存的方法)。而是smart_ptr在拷貝時,代表資源對象的指針不是復制到另外一個smart_ptr,而是"移動"到新smart_ptr。移動后,原來的smart_ptr.ptr_==nullptr, 這樣就完成了資源所有權的轉移。這也是c++unique_ptr的基本行為。我們在這里先把它命名為unique_smart_ptr,代碼完整實現如下:
template?<typename?t> class?unique_smart_ptr?{ public: ?explicit?unique_smart_ptr(t*?ptr?=?nullptr):?ptr_(ptr)?{} ?~unique_smart_ptr()?{ ??delete?ptr_; ?} ?//?1.?自定義移動構造函數 ?unique_smart_ptr(unique_smart_ptr&&?other)?{ ??//?1.1?把other.ptr_?賦值到this->ptr_ ??ptr_?=?other.ptr_; ??//?1.2?把other.ptr_指為nullptr,other不再擁有資源指針 ??other.ptr_?=?nullptr; ?} ?//?2.?自定義賦值行為 ?unique_smart_ptr&?operator?=?(unique_smart_ptr?rhs)?{ ??//?2.1?交換rhs.ptr_和this->ptr_ ??std::swap(rhs.ptr_,?this->ptr_); ??return?*this; ?} t&?operator*()?const?{?return?*ptr_;?} t*?operator->()?const?{?return?ptr_;?} private: ?t*?ptr_; };
自定義移動構造函數。在移動構造函數中,我們先是接管了other.ptr_指向的資源對象,然后把other的ptr_置為nullptr,這樣在other析構時就不會錯誤釋放資源內存。
同時,根據c++的規則,手動提供移動構造函數后,就會自動禁用拷貝構造函數。也就是我們能得到以下效果:
unique_smart_ptr<int>?ptr1{new?int(10)}; unique_smart_ptr<int>?ptr2?=?ptr1;?//?error unique_smart_ptr<int>?ptr3?=?std::move(ptr1);?//?ok unique_smart_ptr<int>?ptr4{ptr1}?//?error unique_smart_ptr<int>?ptr5{std::move(ptr1)}?//?ok
自定義賦值函數。在賦值函數中,我們使用std::swap交換了rhs.ptr_和this->ptr_,注意,這里不能簡單的將rhs.ptr_設置為nullptr,因為this->ptr_可能有指向一個堆對象,該對象需要轉給rhs,在賦值函數調用結束,rhs析構時順便釋放掉。避免內存泄漏。
注意賦值函數的入參rhs的類型是unique_smart_ptr而不是unique_smart_ptr&&,這樣創建rhs使用移動構造函數還是拷貝構造函數完全取決于unique_smart_ptr的定義。因為unique_smart_ptr當前只保留了移動構造函數,所以rhs是通過移動構造函數創建的。
多個智能指針共享對象 - shared_smart_ptr
學過第二講的shared_ptr, 我們知道它是利用計數引用的方式,實現了多個智能指針共享同一個對象。當最后一個持有對象的智能指針析構時,計數器減為0,這個時候才會回收資源對象。
我們先給出shared_smart_ptr的類定義
template?<typename?t> class?shared_smart_ptr?{ public: ?//?構造函數 ?explicit?shared_smart_ptr(t*?ptr?=?nullptr) ?//?析構函數 ?~shared_smart_ptr() ?//?移動構造函數 ?shared_smart_ptr(shared_smart_ptr&&?other) ?//?拷貝構造函數 ?shared_smart_ptr(const?shared_smart_ptr&?other) ?//?賦值函數 ?shared_smart_ptr&?operator?=?(shared_smart_ptr?rhs) ?//?返回當前引用次數 ?int?use_count()?const?{?return?*count_;?} ?t&?operator*()?const?{?return?*ptr_;?} ?t*?operator->()?const?{?return?ptr_;?} private: ?t*?ptr_; ?int*?count_; }
暫時不考慮多線程并發安全的問題,我們簡單在堆上創建一個int類型的計數器count_。下面詳細展開各個函數的實現。
為了避免對count_的重復刪除,我們保持:只有當ptr_ != nullptr時,才對count_進行賦值。
構造函數
同樣的,使用explicit避免隱式轉換。除了賦值ptr_, 還需要在堆上創建一個計數器。
explicit?shared_smart_ptr(t*?ptr?=?nullptr){ ?ptr_?=?ptr; ?if?(ptr_)?{ ??count_?=?new?int(1); ?} }
析構函數
在析構函數中,需要根據計數器的引用數判斷是否需要回收對象。
~shared_smart_ptr()?{ ?//?ptr_為nullptr,不需要做任何處理 ?if?(ptr_)?{ ??return; ?} ?//?計數器減一 ?--(*count_); ?//?計數器減為0,回收對象 ?if?(*count_?==?0)?{ ??delete?ptr_; ??delete?count_; ??return; ?} }
移動構造函數
添加對count_的處理
shared_smart_ptr(shared_smart_ptr&&?other)?{ ?ptr_?=?other.ptr_; ?count_?=?other.count_; ?other.ptr_?=?nullptr; ?other.count_?=?nullptr; }
賦值構造函數
添加交換count_
shared_smart_ptr&?operator?=?(shared_smart_ptr?rhs)?{ ?std::swap(rhs.ptr_,?this->ptr_); ?std::swap(rhs.count_,?this->count_); ?return?*this; }
拷貝構造函數
對于shared_smart_ptr,我們需要手動支持拷貝構造函數。主要處理邏輯是賦值ptr_和增加計數器的引用數。
shared_smart_ptr(const?shared_smart_ptr&?other)?{ ?ptr_?=?other.ptr_; ?count_?=?other.count_; ?if?(ptr_)?{ ??(*count_)++; ?} }
這樣,我們就實現了一個自己的共享智能指針,貼一下完整代碼
template?<typename?t> class?shared_smart_ptr?{ public: ?explicit?shared_smart_ptr(t*?ptr?=?nullptr){ ??ptr_?=?ptr; ??if?(ptr_)?{ ???count_?=?new?int(1); ??} ?} ?~shared_smart_ptr()?{ ??//?ptr_為nullptr,不需要做任何處理 ??if?(ptr_?==?nullptr)?{ ???return; ??} ??//?計數器減一 ??--(*count_); ??//?計數器減為0,回收對象 ??if?(*count_?==?0)?{ ???delete?ptr_; ???delete?count_; ??} ?} ?shared_smart_ptr(shared_smart_ptr&&?other)?{ ??ptr_?=?other.ptr_; ??count_?=?other.count_; ??other.ptr_?=?nullptr; ??other.count_?=?nullptr; ?} ?shared_smart_ptr(const?shared_smart_ptr&?other)?{ ??ptr_?=?other.ptr_; ??count_?=?other.count_; ??if?(ptr_)?{ ???(*count_)++; ??} ?} ?shared_smart_ptr&?operator?=?(shared_smart_ptr?rhs)?{ ??std::swap(rhs.ptr_,?this->ptr_); ??std::swap(rhs.count_,?this->count_); ??return?*this; ?} ?int?use_count()?const?{?return?*count_;?}; ?t&?operator*()?const?{?return?*ptr_;?}; ?t*?operator->()?const?{?return?ptr_;?}; private: ?t*?ptr_; ?int*?count_; };
使用下面代碼進行驗證:
int?main(int?argc,?const?char**?argv)?{ ?shared_smart_ptr<int>?ptr1(new?int(1)); ?std::cout?<<?"[初始化ptr1]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl; ?{ ??//?賦值使用拷貝構造函數 ??shared_smart_ptr<int>?ptr2?=?ptr1; ??std::cout?<<?"[使用拷貝構造函數將ptr1賦值給ptr2]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl; ??//?賦值使用移動構造函數 ??shared_smart_ptr<int>?ptr3?=?std::move(ptr2); ??std::cout?<<?"[使用移動構造函數將ptr2賦值給ptr3]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl; ?} ?std::cout?<<?"[ptr2和ptr3析構后]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl; }
運行結果:
[初始化ptr1]usecountofptr1:1
[使用拷貝構造函數將ptr1賦值給ptr2]usecountofptr1:2
[使用移動構造函數將ptr2賦值給ptr3]usecountofptr1:2
[ptr2和ptr3析構后]usecountofptr1:1
總結
這一講我們從autointptr出發,先是將類進行模版化,使其能夠管理任何類型的指針對象,并給該類起了一個更抽象、更貼切的名稱——smart_ptr。
接著圍繞著「如何正確釋放資源對象指針」的問題,一步步手擼了兩個智能指針 ——unique_smart_ptr和shared_smart_ptr。相信大家現在對智能指針有一個較為深入的理解了。
以上就是c++手擼智能指針的教程分享的詳細內容,更多關于c++智能指針的資料請關注碩編程其它相關文章!