維基百科上面對於「智慧指標」是這樣描述的:
智慧指標(英語:Smart pointer)是一種抽象的資料型別。在程式設計中,它通常是經由型別模板(class template)來實做,藉由模板(template)來達成泛型,通常藉由型別(class)的解構函式來達成自動釋放指標所指向的儲存器或物件。
簡單的來講,智慧指標是一種看上去類似指標的資料型別,只不過它更加智慧,懂的完成記憶體洩露,垃圾回收等一系列看上去很智慧的工作。如你所看到的那樣,藉助 C++ RAII(Resource acquisition is initialization) 特性,在型別(class)的解構函式時來完成自動釋放指標所指向物件的目的。
1、什麼是智慧指標?
先看看一個最簡單的例子 auto_ptr:
template <class T> class auto_ptr
{
T* ptr;
public:
explicit auto_ptr(T* p = 0) : ptr(p) {}
~auto_ptr() {delete ptr;}
T& operator*() {return *ptr;}
T* operator->() {return ptr;}
// ...
};
首先它擁有指標最基本的 2 個特性:deferencing(operator *) 和 indirection(operator ->). 於是下面的程式碼
void foo()
{
MyClass* p(new MyClass);
p->DoSomething();
delete p;
}
可以寫成:
void foo()
{
auto_ptr<MyClass> p(new MyClass);
p->DoSomething();
}
這樣我們新申請的 MyClass 可以完全由智慧指標 p 接管,p 知道何時去釋放這塊記憶體,而不需要程式設計師去操心。
2、為什麼要用智慧指標?
使用智慧指標的好處是顯而易見的,正如上面所舉例,可以有效的防止因為程式設計師粗心而引發的記憶體洩露問題。當然,智慧指標所能達到的效果還遠不止於此,它可以使你的程式更加安全、高效。當上面的 void foo() 函式出現異常的時候,我們不得不修改程式成為下面的樣子:
void foo()
{
MyClass* p;
try {
p = new MyClass;
p->DoSomething();
delete p;
}
catch (...) {
delete p;
throw;
}
}
可以想象,當程式邏輯越來越複雜的時候,傳統的程式碼將會變得更加臃腫不堪。從美觀的角度來說,這樣的程式碼或許缺少點藝術性在裡面,那麼還是用智慧指標吧,程式碼依然如此簡潔、優雅。
再看看下面這個場景:
MyClass* p(new MyClass);
MyClass* q = p;
delete p;
p->DoSomething(); // p is now dangling
p = NULL; // p is no longer dangling
q->DoSomething(); // q is still dangling
當出現訪問異常的時候,可能要耗費程式設計師很多精力去排查這類問題,因為 delete p 之後 p 可能依然指向某塊記憶體(懸掛的)但是卻是無效的指標。下面看看 auto_ptr 處理 operator = 的做法:
template <class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
if (this != &rhs) {
delete ptr;
ptr = rhs.ptr;
rhs.ptr = NULL;
}
return *this;
}
可以看出,auto_ptr 把 q 指向 p 指向的記憶體,並且 p 指標賦值為 null 了。不同型別的智慧指標針對類似問題解決的方案是不同的:
a. copied_ptr: q 指向的記憶體是 p 指向記憶體的一個拷貝。
b. owned_ptr: 讓 p 和 q 指向同一塊記憶體,只不過把 clean up 的責任轉交給了 q。
c. counted_ptr: 維護一個所申請記憶體塊的計數 count,當 q = p 時 count 加 1,當 count 為 0 時釋放記憶體。
d. linked_ptr: 所有的智慧指標組成一個雙向連結串列,但是所有的指標都是指向同一塊記憶體,當出現 q = p 時把 q 加入到這個雙向連結串列中。
e. cow_ptr: Copy-On-Write 機制,本質上是 counted_ptr or linked_ptr,僅當有意圖要寫記憶體時才為 q 重新開闢新的記憶體。
const X& operator*() const throw() {return *itsPtr;}
const X* operator->() const throw() {return itsPtr.get();}
const X* get() const throw() {return itsPtr.get();}
X& operator*() {copy(); return *itsPtr;}
X* operator->() {copy(); return itsPtr.get();}
X* get() {copy(); return itsPtr.get();}
private:
counted_ptr<X> itsPtr;
void copy() // create a new copy of itsPtr
{
if (!itsPtr.unique()) {
X* old_p = itsPtr.get();
itsPtr = counted_ptr<X>(new X(*old_p));
}
}
上面的程式碼展示了 Copy-On-Write 機制產生的時機,這也解釋了為什麼智慧指標會比普通指標更加高效的原因。同樣的手法在 string 類中也出現過:
string s("Hello"); string t = s; // t and s shared the same 'hello' t += " there!"; // now a new buffer allocated for t
3、選擇哪種智慧指標?
關於 counted_ptr 有 2 種不同的實現方法,intrusive(侵入式)和 non-intrusive(非侵入式):
關於 linked_ptr,在多執行緒環境下容易引起死鎖問題:
下面給出了一個總結,什麼時候應該應用什麼樣的智慧指標:
Local variables auto_ptr
Class members Copied pointer
STL Containers Garbage collected pointer (e.g. reference counting/linking)
Explicit ownership transfer Owned pointer
Big objects Copy on write
「參考資料」
http://en.wikipedia.org/wiki/Smart_pointer
http://ootips.org/yonat/4dev/smart-pointers.html