C++知識點 – 智能指針
一、智能指針的使用及原理
1.使用場景
對于上面的場景,p1和p2在new申請空間后,div函數(shù)如果出現(xiàn)了除0錯誤,那么程序就會拋出異常,跳到接受異常的程序段繼續(xù)執(zhí)行,p1和p2申請的空間就沒有被正常釋放,造成了內(nèi)存泄漏;
這種場景我們就可以使用智能指針來解決空間的釋放問題。
2.RAII
RAII(Resource Acquisition Is Initialization)獲取到資源立即初始化,是一種利用對象生命周期來控制程序資源的技術(shù);
在對象構(gòu)造時獲取資源,接著控制對資源的訪問使其在對象的生命周期內(nèi)都有效,最后在對象析構(gòu)時釋放資源,我們實際上把管理一份資源的責(zé)任托管給了一個對象,好處在于:
(1)不需要顯式地釋放資源;
(2)采用這種方式,對象所需的資源在其生命周期內(nèi)始終有效;
3.智能指針的設(shè)計思想
(1)利用RAII的思想設(shè)計delete資源的類;
(2)像指針一樣的行為;
因此,智能指針實際上是一個對象,這個對象重載了operator->和operator*,具有像指針一樣的行為;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
上面的代碼就是一個簡易的智能指針,能夠?qū)崿F(xiàn)資源的釋放,以及像指針一樣的行為;
double Div(int a, int b)
{
if (b == 0)
{
throw invalid_argument("除0錯誤");
}
return (double)a / b;
}
void test1()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << Div(2, 0) << endl;
}
int main()
{
try
{
test1();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
使用智能指針對上面的代碼進(jìn)行重寫,讓智能指針托管開辟的新資源,當(dāng)sp1和sp2析構(gòu)時,其析構(gòu)函數(shù)會delete其管理的資源;
4.智能指針的拷貝問題
編譯器自動生成的拷貝構(gòu)造函數(shù)對于內(nèi)置類型會完成淺拷貝,這里的拷貝需要的就是淺拷貝,因為深拷貝會將管理的資源在其他地方拷貝一份,違背了功能需求;
但是淺拷貝又帶來了delete多次造成程序崩潰的問題,因此c++庫中設(shè)計了幾種智能指針來解決拷貝問題;
二、auto_ptr
這是c++98版本中提供的智能指針,auto_ptr對于拷貝的處理是管理權(quán)轉(zhuǎn)移;
下面是它的模擬實現(xiàn):
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
//管理權(quán)轉(zhuǎn)移
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//檢測是否為自己給自己賦值
if (this != &ap)
{
//釋放當(dāng)前對象中的資源
if (_ptr)
{
delete _ptr;
}
//轉(zhuǎn)移ap中的資源到當(dāng)前對象
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
auto_ptr對于拷貝的處理方式是管理權(quán)轉(zhuǎn)移,對于拷貝構(gòu)造,auto_ptr將資源的管理權(quán)轉(zhuǎn)移給新的對象,原來的對象的指針置空;
對于賦值重載,auto_ptr清空等號左邊的對象的資源,然后將等號右邊的對象管理的資源轉(zhuǎn)交給左邊的對象,右邊對象的指針置空;
auto_ptr對于智能指針拷貝的處理不是很好,所以很多項目都會禁止使用auto_ptr;
三、unique_ptr
c++11中開始提供unique_ptr,對拷貝的處理方式就是禁止拷貝;
下面是它的模擬實現(xiàn):
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;//只聲明不實現(xiàn)
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
unique_ptr將拷貝構(gòu)造和賦值重載都進(jìn)行了只聲明不實現(xiàn)的操作,這樣類中就不會生成默認(rèn)的拷貝構(gòu)造和賦值重載了,也就禁止了unique_ptr對象間的拷貝;
四、shared_ptr
shared_ptr支持拷貝,原理是通過引用計數(shù)的方式來實現(xiàn)多個shared_ptr對象之間共享資源:
(1)每個資源都維護(hù)著一份計數(shù),用來記錄該份資源被幾個對象共享;
(2)在對象被銷毀時,對象引用計數(shù)減一;
(3)如果引用計數(shù)是0,就說明自己是最后一個被銷毀的對象,就必須釋放該資源;
(4)如果引用計數(shù)不是0,就不能釋放該資源;
1.模擬實現(xiàn)
如何實現(xiàn)多個對象共享一個引用計數(shù)呢?不能將引用計數(shù)設(shè)為static成員變量,因為這樣所有的資源對象都是用一個引用計數(shù);可以將引用計數(shù)的成員便變量設(shè)置成一個指針,只在對象構(gòu)造的時候new一個引用計數(shù),在拷貝和賦值的時候,只增加引用計數(shù);
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1)) // 只有在對象調(diào)用構(gòu)造函數(shù)的時候,才會新建一個管理資源的引用計數(shù)
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) // 拷貝構(gòu)造的時候不新建引用計數(shù),只拷貝資源指針和計數(shù)指針,并且計數(shù)++
{
*(_pCount)++;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
--(*_pCount); //指針指向新的資源,原來指向的資源計數(shù)需要--
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
private:
T* _ptr;
//引用計數(shù)
int* _pCount;
};
上面代碼中的賦值重載是有問題的:
在這種情況下,sp1,2,3全部都賦值sp5,原先sp1,2,3指向的資源的引用計數(shù)就減為了0,但是上面的賦值重載函數(shù)并不能將原先的資源釋放掉,就造成了內(nèi)存泄露;
同時,還需要解決自己給自己賦值的問題(這里還要注意指向同一份資源之間的對象相互賦值);
void Release()
{
if (--(*_pCount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr) //判斷指向的資源是不是同一塊資源
{
return *this;
}
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
整體模擬實現(xiàn)如下:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1)) // 只有在對象調(diào)用構(gòu)造函數(shù)的時候,才會新建一個管理資源的引用計數(shù)
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) // 拷貝構(gòu)造的時候不新建引用計數(shù),只拷貝資源指針和計數(shù)指針,并且計數(shù)++
{
*(_pCount)++;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
void Release()
{
if (--(*_pCount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr) //判斷指向的資源是不是同一塊資源
{
return *this;
}
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
int use_count()
{
return *_pCount;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
//引用計數(shù)
int* _pCount;
};
2.shared_ptr的循環(huán)引用
如上圖所示,這種情況下,左右兩個節(jié)點都由shared_ptr n1、n2管理,節(jié)點中的指針同樣也是shared_ptr,左邊節(jié)點的next指向右邊節(jié)點,右邊節(jié)點的prev指向左邊節(jié)點,也就是next管著右邊節(jié)點的內(nèi)存塊,prev管著左邊節(jié)點的內(nèi)存塊,那么左右兩個節(jié)點的引用計數(shù)都是2;
如果僅僅析構(gòu)n1和n2,那么左右兩節(jié)點的引用計數(shù)都會減為1,不會觸發(fā)資源的回收,因為還有next和prev在指向著資源;只有在左邊節(jié)點被釋放,調(diào)用了析構(gòu)函數(shù)時,next才會被釋放,右邊節(jié)點的引用計數(shù)才能減到0,才能夠釋放,顯然無法直接釋放左邊的節(jié)點;
為了解決shared_ptr的循環(huán)引用問題,c++11引入了weak_ptr;
五、weak_ptr
weak_ptr不是常規(guī)的智能指針,沒有RAII,不直接管理資源,weak_ptr主要用shared_ptr構(gòu)造,用來解決shared_ptr的循環(huán)引用問題;
weak_ptr訪問資源時,不增加引用計數(shù),將節(jié)點成員的指針next和prev設(shè)置成weak_ptr就不存在循環(huán)引用的問題了;
模擬實現(xiàn)
template<class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
: _ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
六、定制刪除器
如果自定義類型使用new [ ]申請空間,使用delete釋放可能會出錯;
delete[]是在開空間時多開4byte的空間在頭部,儲存new的對象的個數(shù),在delete[]的時候就知道要調(diào)用多少次析構(gòu)函數(shù)了;
shared_ptr和unique_ptr都支持定制刪除器;
shared_ptr需要在調(diào)用構(gòu)造函數(shù)初始化時傳一個仿函數(shù)對象或者lambda表達(dá)式;
unique_ptr需要傳模板參數(shù)為仿函數(shù);
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete:" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "delete:" << ptr << endl;
free(ptr);
}
};
int main()
{
//調(diào)用構(gòu)造函數(shù)時傳仿函數(shù)對象
std::shared_ptr<int> n1(new int[5], DeleteArray<int>());
std::shared_ptr<int> n2((int*)malloc(5 * sizeof(int)), Free<int>());
//調(diào)用構(gòu)造函數(shù)時傳lambda表達(dá)式
std::shared_ptr<int> n3(new int[5], [](int* ptr) {delete[] ptr; });
//unique_ptr在模板參數(shù)中傳仿函數(shù)
std::unique_ptr<int, DeleteArray<int>> n4(new int[5]);
return 0;
}
七、內(nèi)存泄漏
1.什么是內(nèi)存泄漏
內(nèi)存泄漏是指因為疏忽或錯誤導(dǎo)致程序未能釋放已經(jīng)不再使用的內(nèi)存的情況;內(nèi)存泄漏不是內(nèi)存在物理上消失,而是因為設(shè)計錯誤而失去了對內(nèi)存的控制(指針丟失);
如果內(nèi)存還在,進(jìn)程正常結(jié)束,內(nèi)存也會釋放;
2.內(nèi)存泄露的危害
長期內(nèi)存泄漏,將導(dǎo)致程序相應(yīng)越來越慢,直到卡死;文章來源:http://www.zghlxwxcb.cn/news/detail-437503.html
3.如何避免內(nèi)存泄漏
(1)養(yǎng)成良好的工程編碼規(guī)范,申請的內(nèi)存記著去釋放;
(2)使用RAII的思想或者智能指針來管理資源;
(3)使用內(nèi)存泄漏檢測工具;文章來源地址http://www.zghlxwxcb.cn/news/detail-437503.html
到了這里,關(guān)于C++知識點 -- 智能指針的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!