智能指針是爲了解決C++裏防止程序員因爲忘記釋放資源而造成內存泄漏的問題
RAII
RAII(Resource Acquisition Is Initialization)是一種利用對象生命週期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。
在對象構造時獲取資源,接着控制對資源的訪問使之在對象的生命週期內始終保持有效,最後在對象析構的時候釋放資源。藉此, 我們實際上把管理一份資源的責任託管給了一個對象。這種做法有兩大好處:
- 不需要顯式地釋放資源。
- 採用這種方式,對象所需的資源在其生命期內始終保持有效。
智能指針
一、原理:
- RAII特性
- 重載operator*和opertaor->,具有像指針一樣的行爲
理解:利用一個行爲很像指針的對象幫你保管資源,同時當出了對象作用域時,會自動幫你釋放,智能指針不是一個真正的指針:而是一個對象,可以使用操控指針的語句來操控這個對象。並且因爲智能指針是棧空間上類的對象。所以當程序運行結束後,會自動調用其析構函數自動釋放。這樣就不會擔心在申請資源和釋放資源中程序拋異常了。
二、分類:
指針名 | 優點 | 缺點 |
---|---|---|
auto_ptr | 實現簡單 | 不安全,進行復制後會有指針懸空現象 |
unique_ptr | 簡單粗暴且安全 | 無法拷貝,僅能進行所有權轉移 |
shared_ptr | 安全,並且能拷貝 | 爲了解決循環引用問題,必須藉助weak_ptr實現完整功能 |
注:C++庫中的智能指針都定義在#include <memory>這個頭文件中
三、分析:
- auto_ptr
1.實現原理:將資源管理權進行轉移,即將一個指針所指向的空間交給另一個指針。
2.缺陷:管理權一直在變,但只允許有一個指針指向該資源,兩個指針就無法指向同一塊資源。這樣的話當調用拷貝構造和賦值運算符重載的時候,將原來所管理的資源轉移給當前對象後,就斷開了原ptr與其所管理資源的聯繫,導致再通過原對象訪問資源 時就會出現問題。
3.代碼實現:
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
AutoPtr(AutoPtr<T>& ap) //拷貝構造
:_ptr(ap._ptr) //將ap中資源轉移到當前對象中
{
ap._ptr = nullptr; //令ap與其所管理資源斷開聯繫
}
AutoPtr<T>& operator=(AutoPtr<T>& ap) //賦值運算符重載
{
if (&this != &ap) //檢測是否給自己賦值
{
if (_ptr) //釋放當前對象中的資源
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
- unique_ptr (C++11中提供)
1.實現原理:簡單粗暴的防拷貝,即不然拷貝和賦值
2.特點:讓自己的東西無法被別人獲取
3.代碼實現:
template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~UniquePtr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//C++98中防拷貝的方式:聲明成私有 並且 不進行實現
//UniquePtr(UniquePtr<T> const &);
//UniquePtr& operator=(UniquePtr<T>const &);
//C++11中防拷貝的方式:採用delete來禁止默認創建的函數
UniquePtr(UniquePtr<T>const &) = delete;
UniquePtr& operator=(UniquePtr<T>const &) = delete;
private:
T* _ptr;
};
- shared_ptr(C++11中提供)
1.實現原理:通過引用計數的方式來實現多個shared_ptr對象之間 共享資源
- 在shared_ptr內部,給每個資源都維護着一份計數,用來記錄該份資源被幾個對象共享。
- 在對象被銷燬時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一。
- 如果引用計數是0,就說明自己是最後一個使用該資源的對象,必須釋放該資源;
- 如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針了。
注:count的類型是int*,且它是由多個對象共有的,在對count++或--時需要加上互斥鎖,這樣才能保證線程安全。
shared_ptr的線程安全問題:
- 智能指針對象中引用計數是多個智能指針對象共享的,兩個線程中智能指針的引用計數同時++或--,這個操作不是原子的,引用計數原來是1,++了兩次,可能還是2,這樣引用計數就錯亂了。會導致資源未釋放或者程序崩潰的問題。所以在智能指針中引用計數++、--是需要加鎖的,也就是說引用計數的操作是 線程安全的。
- 智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。
2.缺點:雙向鏈表中可能會出現循環引用問題,導致資源空間不能完全釋放掉。
解決方案:在引用計數的場景下,把節點中的_prev和_next改成weak_ptr就可以了。
3.代碼實現:
#include <thread>
#include <mutex>
template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
:_ptr(ptr)
,_pCount(new int(1))
,_pMutex(new mutex)
{
// 如果是一個空指針對象,則引用計數給0
if (_ptr == nullptr)
*_pCount = 0;
}
SharedPtr()
{
Release();
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMutex(sp._pMutex)
{
if (_ptr) //指針對象不爲空纔可進行加引用計數
AddCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp) //sp1 = sp2;
{
if (_ptr != sp._ptr) //if(this != &sp)
{
Release(); //釋放舊資源
//共享管理新對象的資源
_ptr = sp._ptr;
_pCount = sp._pCount;
_pMutex = sp._pMutex;
//並進行加引用計數
if (_ptr)
AddCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int UseCount()
{
return *_pCount;
}
T* Get()
{
return _ptr;
}
int AddCount() //引用計數加1
{
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
return *_pCount;
}
int SubCount() //引用計數減1
{
_pMutex->lock();
--(*_pCount);
_pMutex->unlock();
return *_pCount;
}
private:
void Release()
{
if (_ptr && SubCount() == 0) //判斷引用計數減1後是否爲0,爲0則釋放資源
{
delete _ptr;
delete _pCount;
}
}
private:
T* _ptr; //指向管理資源的指針
int* _pCount; //引用計數
mutex* _pMutex; //互斥鎖
};