C++手?jǐn)]智能指針的教程分享
前言
大家好,今天是【重學(xué)C++】的第三講,書接上回,第二講《02 脫離指針陷阱:深入淺出 C++ 智能指針》介紹了C++智能指針的一些使用方法和基本原理。今天,我們自己動手,從0到1實(shí)現(xiàn)一下自己的unique_ptr和shared_ptr。
回顧
智能指針的基本原理是基于RAII設(shè)計理論,自動回收內(nèi)存資源,從根本上避免內(nèi)存泄漏。在第一講《01 C++ 如何進(jìn)行內(nèi)存資源管理?》介紹RAII的時候,就已經(jīng)給了一個用于封裝int類型指針,實(shí)現(xiàn)自動回收資源的代碼實(shí)例:
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設(shè)計成一個類模版,原來代碼中的int改成模版參數(shù)T,非常簡單。使用時也只要把AutoIntPtr(new int(9)) 改成smart_ptr<int>(new int(9))即可。
另外,有一點(diǎn)值得注意,smart_ptr的構(gòu)造函數(shù)使用了explicit, explicit關(guān)鍵字主要用于防止隱式的類型轉(zhuǎn)換。代碼中,如果原生指針隱式地轉(zhuǎn)換為智能指針類型可能會導(dǎo)致一些潛在的問題。至于會有什么問題,你那聰明的小腦瓜看完下面的代碼肯定能理解了:
void?foo(smart_ptr<int>?int_ptr)?{
????//?...
}
int?main()?{
????int*?raw_ptr?=?new?int(42);
????foo(raw_ptr);??//?隱式轉(zhuǎn)換為?smart_ptr<int>
????std::cout?<<?*raw_ptr?<<?std::endl;???//?error:?raw_ptr已經(jīng)被回收了
????//?...
}假設(shè)我們沒有為smart_ptr構(gòu)造函數(shù)加上explicit,原生指針raw_ptr在傳給foo函數(shù)后,會被隱形轉(zhuǎn)換為smart_ptr<int>, foo函數(shù)調(diào)用結(jié)束后,棲構(gòu)入?yún)⒌?code>smart_ptr<int>時會把raw_ptr給回收掉了,所以后續(xù)對raw_ptr的調(diào)用都會失敗。
拷貝還是移動
當(dāng)前我們沒有為smart_ptr自定義拷貝構(gòu)造函數(shù)/移動構(gòu)造函數(shù),C++會為smart_ptr生成默認(rèn)的拷貝/移動構(gòu)造函數(shù)。默認(rèn)的拷貝/移動構(gòu)造函數(shù)邏輯很簡單:把每個成員變量拷貝/移動到目標(biāo)對象中。
按當(dāng)前smart_ptr的實(shí)現(xiàn),我們假設(shè)有以下代碼:
smart_ptr<int>?ptr1{new?int(10)};
smart_ptr<int>?ptr2?=?ptr1;這段代碼在編譯時不會出錯,問題在運(yùn)行時才會暴露出來:第二行將ptr1管理的指針復(fù)制給了ptr2,所以會重復(fù)釋放內(nèi)存,導(dǎo)致程序奔潰。
為了避免同一塊內(nèi)存被重復(fù)釋放。解決辦法也很簡單:
- 獨(dú)占資源所有權(quán),每時每刻一個內(nèi)存對象(資源)只能有一個
smart_ptr占有它。 - 一個內(nèi)存對象(資源)只有在最后一個擁有它的
smart_ptr析構(gòu)時才會進(jìn)行資源回收。
獨(dú)占所有權(quán) - unique_smart_ptr
獨(dú)占資源的所有權(quán),并不是指禁用掉smart_ptr的拷貝/移動函數(shù)(當(dāng)然這也是一種簡單的避免重復(fù)釋放內(nèi)存的方法)。而是smart_ptr在拷貝時,代表資源對象的指針不是復(fù)制到另外一個smart_ptr,而是"移動"到新smart_ptr。移動后,原來的smart_ptr.ptr_ == nullptr, 這樣就完成了資源所有權(quán)的轉(zhuǎn)移。這也是C++ unique_ptr的基本行為。我們在這里先把它命名為unique_smart_ptr,代碼完整實(shí)現(xiàn)如下:
template?<typename?T>
class?unique_smart_ptr?{
public:
?explicit?unique_smart_ptr(T*?ptr?=?nullptr):?ptr_(ptr)?{}
?~unique_smart_ptr()?{
??delete?ptr_;
?}
?//?1.?自定義移動構(gòu)造函數(shù)
?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_;
};自定義移動構(gòu)造函數(shù)。在移動構(gòu)造函數(shù)中,我們先是接管了other.ptr_指向的資源對象,然后把other的ptr_置為nullptr,這樣在other析構(gòu)時就不會錯誤釋放資源內(nèi)存。
同時,根據(jù)C++的規(guī)則,手動提供移動構(gòu)造函數(shù)后,就會自動禁用拷貝構(gòu)造函數(shù)。也就是我們能得到以下效果:
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自定義賦值函數(shù)。在賦值函數(shù)中,我們使用std::swap交換了 rhs.ptr_和this->ptr_,注意,這里不能簡單的將rhs.ptr_設(shè)置為nullptr,因為this->ptr_可能有指向一個堆對象,該對象需要轉(zhuǎn)給rhs,在賦值函數(shù)調(diào)用結(jié)束,rhs析構(gòu)時順便釋放掉。避免內(nèi)存泄漏。
注意賦值函數(shù)的入?yún)?code>rhs的類型是unique_smart_ptr而不是unique_smart_ptr&&,這樣創(chuàng)建rhs使用移動構(gòu)造函數(shù)還是拷貝構(gòu)造函數(shù)完全取決于unique_smart_ptr的定義。因為unique_smart_ptr當(dāng)前只保留了移動構(gòu)造函數(shù),所以rhs是通過移動構(gòu)造函數(shù)創(chuàng)建的。

多個智能指針共享對象 - shared_smart_ptr
學(xué)過第二講的shared_ptr, 我們知道它是利用計數(shù)引用的方式,實(shí)現(xiàn)了多個智能指針共享同一個對象。當(dāng)最后一個持有對象的智能指針析構(gòu)時,計數(shù)器減為0,這個時候才會回收資源對象。

我們先給出shared_smart_ptr的類定義
template?<typename?T>
class?shared_smart_ptr?{
public:
?//?構(gòu)造函數(shù)
?explicit?shared_smart_ptr(T*?ptr?=?nullptr)
?//?析構(gòu)函數(shù)
?~shared_smart_ptr()
?//?移動構(gòu)造函數(shù)
?shared_smart_ptr(shared_smart_ptr&&?other)
?//?拷貝構(gòu)造函數(shù)
?shared_smart_ptr(const?shared_smart_ptr&?other)
?//?賦值函數(shù)
?shared_smart_ptr&?operator?=?(shared_smart_ptr?rhs)
?//?返回當(dāng)前引用次數(shù)
?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類型的計數(shù)器count_。下面詳細(xì)展開各個函數(shù)的實(shí)現(xiàn)。
為了避免對count_的重復(fù)刪除,我們保持:只有當(dāng)ptr_ != nullptr時,才對count_進(jìn)行賦值。
構(gòu)造函數(shù)
同樣的,使用explicit避免隱式轉(zhuǎn)換。除了賦值ptr_, 還需要在堆上創(chuàng)建一個計數(shù)器。
explicit?shared_smart_ptr(T*?ptr?=?nullptr){
?ptr_?=?ptr;
?if?(ptr_)?{
??count_?=?new?int(1);
?}
}析構(gòu)函數(shù)
在析構(gòu)函數(shù)中,需要根據(jù)計數(shù)器的引用數(shù)判斷是否需要回收對象。
~shared_smart_ptr()?{
?//?ptr_為nullptr,不需要做任何處理
?if?(ptr_)?{
??return;
?}
?//?計數(shù)器減一
?--(*count_);
?//?計數(shù)器減為0,回收對象
?if?(*count_?==?0)?{
??delete?ptr_;
??delete?count_;
??return;
?}
}移動構(gòu)造函數(shù)
添加對count_的處理
shared_smart_ptr(shared_smart_ptr&&?other)?{
?ptr_?=?other.ptr_;
?count_?=?other.count_;
?other.ptr_?=?nullptr;
?other.count_?=?nullptr;
}賦值構(gòu)造函數(shù)
添加交換count_
shared_smart_ptr&?operator?=?(shared_smart_ptr?rhs)?{
?std::swap(rhs.ptr_,?this->ptr_);
?std::swap(rhs.count_,?this->count_);
?return?*this;
}拷貝構(gòu)造函數(shù)
對于shared_smart_ptr,我們需要手動支持拷貝構(gòu)造函數(shù)。主要處理邏輯是賦值ptr_和增加計數(shù)器的引用數(shù)。
shared_smart_ptr(const?shared_smart_ptr&?other)?{
?ptr_?=?other.ptr_;
?count_?=?other.count_;
?if?(ptr_)?{
??(*count_)++;
?}
}這樣,我們就實(shí)現(xiàn)了一個自己的共享智能指針,貼一下完整代碼
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;
??}
??//?計數(shù)器減一
??--(*count_);
??//?計數(shù)器減為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_;
};使用下面代碼進(jìn)行驗證:
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;
?{
??//?賦值使用拷貝構(gòu)造函數(shù)
??shared_smart_ptr<int>?ptr2?=?ptr1;
??std::cout?<<?"[使用拷貝構(gòu)造函數(shù)將ptr1賦值給ptr2]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl;
??//?賦值使用移動構(gòu)造函數(shù)
??shared_smart_ptr<int>?ptr3?=?std::move(ptr2);
??std::cout?<<?"[使用移動構(gòu)造函數(shù)將ptr2賦值給ptr3]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl;
?}
?std::cout?<<?"[ptr2和ptr3析構(gòu)后]?use?count?of?ptr1:?"?<<?ptr1.use_count()?<<?std::endl;
}運(yùn)行結(jié)果:
[初始化ptr1] use count of ptr1: 1
[使用拷貝構(gòu)造函數(shù)將ptr1賦值給ptr2] use count of ptr1: 2
[使用移動構(gòu)造函數(shù)將ptr2賦值給ptr3] use count of ptr1: 2
[ptr2和ptr3析構(gòu)后] use count of ptr1: 1
總結(jié)
這一講我們從AutoIntPtr出發(fā),先是將類進(jìn)行模版化,使其能夠管理任何類型的指針對象,并給該類起了一個更抽象、更貼切的名稱——smart_ptr。
接著圍繞著「如何正確釋放資源對象指針」的問題,一步步手?jǐn)]了兩個智能指針 ——unique_smart_ptr和shared_smart_ptr。相信大家現(xiàn)在對智能指針有一個較為深入的理解了。
以上就是C++手?jǐn)]智能指針的教程分享的詳細(xì)內(nèi)容,更多關(guān)于C++智能指針的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C++中的內(nèi)存同步模式(memory order)
這篇文章主要介紹了C++中的內(nèi)存同步模式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
全面了解#pragma once與 #ifndef的區(qū)別
下面小編就為大家?guī)硪黄媪私?pragma once與 #ifndef的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08
一些語言的按行讀取文件的代碼實(shí)現(xiàn)小結(jié)
這篇文章主要介紹了一些語言的按行讀取文件的代碼實(shí)現(xiàn)小結(jié),這里羅列了Java和C語言和C++以及PHP的實(shí)現(xiàn)需要的朋友可以參考下2015-08-08
C++中關(guān)鍵字const的詳細(xì)說明和使用介紹(最全)
const在C/C++中是十分重要的,如果單純理解為“常量”那么你的格局就小了,今天在這里給大家介紹一下const在C++中具體詳細(xì)的用法,需要的朋友可以參考下2025-03-03
Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Matlab有一定幫助,需要的可以參考一下2022-09-09

