前言:智能指針某些情況下能夠避免內存泄露,這裏我做了一次重複製造輪子,目的是爲了深入理解智能指針的實現原理。這裏只實現了auto_point,根據原理我們不難推出其他智能指針的實現方式。
一、實現:
#pragma once
//基於auto_ptr的實現,不拋出任何異常
template<class T>
class auto_pnt{
private:
T* m_pnt;
public:
typedef T element_type;
//避免無意的隱式類型轉換
explicit auto_pnt(element_type* p = 0) throw():m_pnt(p){}
auto_pnt (auto_pnt& a) throw():m_pnt(a.release()){}
template<class T1>
auto_pnt(auto_pnt<T1>& a):m_pnt(a->release()) throw(){}
//複製操作符,注意銷燬當前指針指向的對象,並將a指針置爲0,防止多次delete的異常
auto_pnt& operator=(auto_pnt& a) throw()
{
reset(a->release());
return *this;
}
template<class T1>
auto_pnt& operator=(auto_pnt<T1>& a) throw()
{
reset(a->release());
return *this;
}
~auto_pnt(){delete m_pnt;}
//區指針指向的對象
element_type& operator*() const throw()
{
return *m_pnt;
}
//注意指針爲空的情況
element_type* operator->() const throw()
{
return m_pnt;
}
//返回當前指針的值
element_type* get() const throw()
{
return m_pnt;
}
//返回該指針值作爲臨時變量,並將當前指針置0
element_type* release() throw()
{
element_type* tmp = m_pnt;
m_pnt = 0;
return tmp;
}
//將指針指向p,並銷燬當前指向的對象
void reset(element_type* p = 0) throw()
{
if (p != m_pnt)
{
delete m_pnt;
m_pnt = p;
}
}
};
二、測試
class Item{
public:
Item(){ cout << "Item Constructor." << endl;}
~Item(){ cout << "Item Deconstructor." << endl;}
void Print(char* str = NULL){
if(str == NULL)
cout << "hello world."<< endl;
else
cout << str << endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
char str[] = "hi, how are you.";
{
auto_pnt<Item> pt(new Item());
pt->Print(str);
auto_pnt<Item> pt2 = pt;
pt2->Print();
}
system("pause");
return 0;
}
三、與其他智能指針的一些比較
STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr。模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。
1、auto_ptr的缺點
看下面的賦值語句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocaticn = ps;
注意:如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因爲程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。
要避免這種問題,方法有多種:
定義陚值運算符,使之執行深複製。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未採用此方案。建立所有權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱爲引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1,。當減爲0時才調用delete。這是shared_ptr採用的策略。當然,同樣的策略也適用於複製構造函數。
2、摒棄auto_ptr的原因:避免潛在的內存崩潰問題
下面舉個例子來說明。#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main() {
auto_ptr<string> films[5] =
{
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership. 將所有權從films[2]轉讓給pwin,此時films[2]不再引用該字符串從而變成空指針
cout << "The nominees for best avian baseballl film are\n";
for(int i = 0; i < 5; ++i)
cout << *films[i] << endl;
cout << "The winner is " << *pwin << endl;
cin.get();
return 0;
}
運行下發現程序崩潰了,原因在上面註釋已經說的很清楚,films[2]已經是空指針了,下面輸出訪問空指針當然會崩潰了。但這裏如果把auto_ptr換成shared_ptr或unique_ptr後,程序就不會崩潰,原因如下:
使用shared_ptr時運行正常,因爲shared_ptr採用引用計數,pwin和films[2]都指向同一塊內存,在釋放空間時因爲事先要判斷引用計數值的大小因此不會出現多次刪除一個對象的錯誤。
使用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程序不會等到運行階段崩潰,而在編譯器因下述代碼行出現錯誤:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
3、如何選擇智能指針
在掌握了這幾種智能指針後,應使用哪種智能指針呢?
(1)如果程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的情況包括:
有一個指針數組,並使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
兩個對象包含都指向第三個對象的指針;
STL容器包含指針。
很多STL算法都支持複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行爲不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
(2)如果程序不需要多個指向同一個對象的指針,則可使用unique_ptr。如果函數使用new分配內存,並返還指向該內存的指針,將其返回類型聲明爲unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr複製或賦給另一個算法(如sort())。
例如,可在程序中石油類似於下面的代碼段。
unique_ptr<int> make_int(int n)
{
return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
cout << *a << ' ';
}
int main()
{
...
vector<unique_ptr<int> > vp(size);
for(int i = 0; i < vp.size(); i++)
vp[i] = make_int(rand() % 1000); // 拷貝臨時unique_ptr
vp.push_back(make_int(rand() % 1000)); <span style="white-space:pre"> </span> // ok參數是臨時的
for_each(vp.begin(), vp.end(), show); // 使用宏定義的for_each
}
其中push_back調用沒有問題,因爲它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞對象,for_each()將非法,因爲這將導致使用一個來自vp的非臨時unique_ptr初始化pi,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。
在unique_ptr爲右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的代碼中,make_int()的返回類型爲unique_ptr<int>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok
shared_ptr<int> spp(pup); //不允許, pup 左值
shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板shared_ptr包含一個顯式構造函數,可用於將右值unique_ptr轉換爲shared_ptr。shared_ptr將接管原來歸unique_ptr所有的對象。
在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。
最後,要想完全弄透徹看一兩篇博客是不可能的,這裏我也是參考別人博客加自己理解,進行整理的。對於unique_ptr、shared_ptr和weak_ptr等智能指針我並沒有重複製造輪子,SGI的STL源碼閱讀也是蠻不方便的,加之篇幅已經過長,對於沒耐心的可能很難讀完,這裏只是一個拋磚引玉,希望能幫到讀者。