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; };
我們從這個示例出發(fā),一步步完善我們自己的智能指針。
模版化
這個類有個明顯的問題:只能適用于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
給回收掉了,所以后續(xù)對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++的規(guī)則,手動提供移動構造函數后,就會自動禁用拷貝構造函數。也就是我們能得到以下效果:
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&&
,這樣創(chuàng)建rhs
使用移動構造函數還是拷貝構造函數完全取決于unique_smart_ptr
的定義。因為unique_smart_ptr
當前只保留了移動構造函數,所以rhs
是通過移動構造函數創(chuàng)建的。
多個智能指針共享對象 - 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_; }
暫時不考慮多線程并發(fā)安全的問題,我們簡單在堆上創(chuàng)建一個int類型的計數器count_
。下面詳細展開各個函數的實現。
為了避免對count_
的重復刪除,我們保持:只有當ptr_ != nullptr
時,才對count_
進行賦值。
構造函數
同樣的,使用explicit
避免隱式轉換。除了賦值ptr_
, 還需要在堆上創(chuàng)建一個計數器。
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] use count of ptr1: 1
[使用拷貝構造函數將ptr1賦值給ptr2] use count of ptr1: 2
[使用移動構造函數將ptr2賦值給ptr3] use count of ptr1: 2
[ptr2和ptr3析構后] use count of ptr1: 1
總結
這一講我們從AutoIntPtr
出發(fā),先是將類進行模版化,使其能夠管理任何類型的指針對象,并給該類起了一個更抽象、更貼切的名稱——smart_ptr
。
接著圍繞著「如何正確釋放資源對象指針」的問題,一步步手擼了兩個智能指針 ——unique_smart_ptr
和shared_smart_ptr
。相信大家現在對智能指針有一個較為深入的理解了。
以上就是C++手擼智能指針的教程分享的詳細內容,更多關于C++智能指針的資料請關注腳本之家其它相關文章!
相關文章
全面了解#pragma once與 #ifndef的區(qū)別
下面小編就為大家?guī)硪黄媪私?pragma once與 #ifndef的區(qū)別。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08