C++:智慧指標
首先,為什麼需要智慧指標?
來看下面這段程式碼
bool doSomething()
{
// 如果時間執行失敗了就返回false
return false;
}
// 為了避免記憶體洩漏和檔案描述符洩漏,我們需要寫出以下這樣冗餘的程式碼
// 我們需要一種方法讓他自動的釋放掉
void Test1()
{
int* p1 = new int[2];
FILE* pf = fopen("test","r");
// 1.如果開啟檔案失敗,就需要釋放p1的空間
if(pf == NULL)
{
delete[] p1;
}
// 2.如果執行事件失敗,就需要釋放p1的空間,並且關閉pf檔案
if(!doSomething())
{
delete[] p1;
fclose(pf);
return;
}
// 3.如果丟擲了異常,我們捕獲異常以後,也需要釋放p1的空間,並且關閉檔案描述符
try
{
throw 1;
}
catch(int err)
{
delete[] p1;
fclose(pf);
return;
}
// 4.邏輯正常結束,也需要釋放空間和關閉檔案
delete [] p1;
fclose(pf);
return;
}
可以看到,在這段程式碼裡,我們需要考慮一些異常情況,並且要非常非常注意釋放資源。但是造成的問題就是,程式碼冗餘,反覆重複的釋放程式碼,降低了可讀性,並且反覆多次的釋放,如果有哪一個忘記寫了或是漏寫了,就會造成記憶體洩漏。
所以,這就體現出了智慧指標的好處。
什麼是智慧指標
一個智慧指標應具備以下的三點:
- RAII
- 像指標一樣的使用
- 拷貝構造與賦值
先來說第一點:RAII
後面兩點後面會說
RAII
資源分配即初始化,定義一個類來封裝資源的分配與釋放,在建構函式完成資源的分配與初始化,在解構函式完成資源的清理,可以保證資源的正確初始化和釋放。
再來看看都有哪些智慧指標
auto_ptr
該智慧指標是c98標準裡規定的,需要注意的是,這個智慧指標是不提倡使用的,某些情況下禁止使用的。所以只做瞭解。
上文種提到了智慧指標的三點,再次討論一下auto_ptr的後兩點:
1.像指標一樣使用,很容易就可以使用,過載*和->就可以實現
2.拷貝構造與賦值:拷貝構造預設的拷貝方式是淺拷貝,即把你的拷一份給我,對指標而言,就是你和我都指向同一空間,那麼就會出現很大的問題,析構時會析構兩次。auto_ptr對於該問題的解決方案是使用管理權轉移的思想
我們來模擬實現一個auto_ptr:
template <class T>
class Auto_Ptr{
public:
Auto_Ptr(T* ptr)
:_ptr(ptr)
{}
~Auto_Ptr()
{
if(_ptr!=NULL)
{
cout<<"delete _ptr"<<endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//以下做了管理權轉移
Auto_Ptr(Auto_Ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr=NULL;
}
//ap3=ap2;
T& operator=(Auto_Ptr<T>& ap)
{
if(this!=&ap)
{
if(_ptr!=NULL)
{
delete _ptr;
}
_ptr=ap._ptr;
ap._ptr=NULL;
}
return *this;
}
private:
T* _ptr;
};
scoped_ptr
以下智慧指標都是針對於Boots庫中的智慧指標。
與auot_ptr相似,scoped_ptr也具有以上三點,與auto_ptr不同的是對於拷貝構造與賦值實現思想不同。
scoped_ptr採用了防拷貝的思想,意思是,如果你要拷貝或是賦值但是我不允許這種行為。如果採取預設的拷貝構造與賦值,預設是淺拷貝,也就意味著我們必須實現一個,但是如何實現呢?scoped_ptr採取的方法是隻宣告不實現。並且將介面設定為私有的,也就說明你想實現也實現不了。
template <class T>
class Scoped_Ptr
{
public:
Scoped_Ptr(T* ptr)
:_ptr(ptr)
{}
~Scoped_Ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//防拷貝
//只宣告不實現
//定義為私有的,也不允許你在外面實現
Scoped_Ptr(const Scoped_Ptr<T>& sp);
Scoped_Ptr<T>& operator=(const Scoped_Ptr<T>& sp);
private:
T* _ptr;
};
shared_ptr
shared_ptr使用了一種我們比較熟悉的做法:引用計數
template<class T>
class Shared_Ptr
{
public:
Shared_Ptr(T* ptr)
:_ptr(ptr)
{
_pcount=new int(1);
}
~Shared_Ptr()
{
if(--(*_pcount)==0)
{
delete _ptr;
delete _pcount;
}
}
Shared_Ptr(const Shared_Ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
//sp1=sp2;
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
if(_ptr!=sp._ptr)
{
if(--(*_pcount)==0)
{
delete _pcount;
delete _ptr;
}
_ptr=sp._ptr;
_pcount=sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
這種做法乍一看是很完美,但是shared_ptr有一些問題:
1. 執行緒安全問題:由於對引用計數++不是原子的,所以這裡是有執行緒安全問題的
2. 迴圈引用問題:這個問題使用以上問題是不會有問題的,比如在以下場景:
struct ListNode
{
Shared_Ptr<ListNode> _next;
Shared_Ptr<ListNode> _prev;
ListNode()
:_next(NULL)
,_prev(NULL)
{}
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycle()
{
Shared_Ptr<ListNode> cur(new ListNode);
Shared_Ptr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
那麼該如何解決這個問題,我們不妨思考一下,造成的原因是由於引用計數,那麼我們就不要讓引用計數自增就好了。所以就需要另外一個智慧指標出場–weak_ptr
weak_ptr
這個智慧指標不含RAII,因為他的作用就是為了解決shared_ptr的迴圈引用問題的。
如何解決呢?看程式碼:
struct ListNode
{
Weak_Ptr<ListNode> _next;
Weak_Ptr<ListNode> _prev;
ListNode()
:_next(NULL)
,_prev(NULL)
{}
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycle()
{
Shared_Ptr<ListNode> cur(new ListNode);
Shared_Ptr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
將_next 與 _prev指標都定義為weak_ptr指標就可以避免出現迴圈引用問題了。
下面來總結一下以上提到的智慧指標:
智慧指標 | 特點 | 思想 | 使用 |
---|---|---|---|
auto_ptr | 帶有缺陷 | 管理權轉移 | 嚴禁使用 |
scoped_ptr | 簡單粗暴 | 防拷貝 | 鼓勵使用 |
shared_ptr | 相對複雜 | 引用計數 | 鼓勵使用,注意迴圈引用問題 |
weak_ptr |