複製鏈接
請複製以下鏈接發送給好友

智能指針

鎖定
當類中有指針成員時,一般有兩種方式來管理指針成員:一是採用值型的方式管理,每個類對象都保留一份指針指向的對象的拷貝;另一種更優雅的方式是使用智能指針,從而實現指針指向的對象的共享。
智能指針(smart pointer)的一種通用實現技術是使用引用計數(reference count)。智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象的指針指向同一對象。
中文名
智能指針
外文名
smart pointer
經典策略
引入輔助類、使用句柄類
含    義
存儲指向動態分配對象指針的類
工作機制
類似C++的內置指針

智能指針原理

每次創建類的新對象時,初始化指針並將引用計數置為1;當對象作為另一對象的副本而創建時,拷貝構造函數拷貝指針並增加與之相應的引用計數;對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數為減至0,則刪除對象),並增加右操作數所指對象的引用計數;調用析構函數時,析構函數減少引用計數(如果引用計數減至0,則刪除基礎對象)。
實現引用計數有兩種經典策略:一是引入輔助類,二是使用句柄類。下面分別介紹這些內容

智能指針問題描述

假設有一個名為TestPtr的類,裏面有一個指針成員,簡化為如下代碼
classTestPtr
{
public:
TestPtr(int*p):ptr(p){}
~TestPtr(){deleteptr;}
//otheroperations
private:
int*ptr;
//otherdata
};
在這種情況下,類TestPtr對象的任何拷貝、賦值操作都會使多個TestPtr對象共享相同的指針。但在一個對象發生析構時,指針指向的對象將被釋放,從而可能引起懸垂指針
現在我們使用引用計數來解決這個問題,一個新的問題是引用計數放在哪裏。顯然,不能放在TestPtr類中,因為多個對象共享指針時無法同步更新引用計數。
方案一
這裏給出的解決方案是,定義一個單獨的具體類(RefPtr)來封裝指針和相應的引用計數。由於這個類只是用於對類TestPtr中的成員指針ptr進行了封裝,無其它用途,所以把引用計數類RefPtr的所有成員均定義為private,並把類TestPtr聲明為它的友元類,使TestPtr類可以訪問RefPtr類。示例代碼如下:
class RefPtr
{
friend class TestPtr;
int* ptr;
size_t count;
RefPtr(int* p):ptr(p),count(1){}
~RefPtr(){
delete ptr;
}
};
class TestPtr
{
public:
TestPtr(int* p):ptr(newRefPtr(p)){}
TestPtr(const TestPtr& src):ptr(src.ptr){
++ptr->count;
}
TestPtr &operator=(const TestPtr& rhs){
//self-assigningisalsoright
++rhs.ptr->count;
if(--ptr->count==0)
delete ptr;
ptr=rhs.ptr;
return *this;
}
~TestPtr(){
if(--ptr->count==0)
delete ptr;
}
private:
RefPtr *ptr;
};
當希望每個TestPtr對象中的指針所指向的內容改變而不影響其它對象的指針所指向的內容時,可以在發生修改時,創建新的對象,並修改相應的引用計數。這種技術的一個實例就是寫時拷貝(Copy-On-Write)。
這種方案的缺點是每個含有指針的類的實現代碼中都要自己控制引用計數,比較繁瑣。特別是當有多個這類指針時,維護引用計數比較困難。
方案二
為了避免上面方案中每個使用指針的類自己去控制引用計數,可以用一個類把指針封裝起來。封裝好後,這個類對象可以出現在用户類使用指針的任何地方,表現為一個指針的行為。我們可以像指針一樣使用它,而不用擔心普通成員指針所帶來的問題,我們把這樣的類叫句柄類。在封裝句柄類時,需要申請一個動態分配的引用計數空間,指針與引用計數分開存儲。實現示例如下
#include<iostream>
#include<stdexcept>
using namespace std;
#define TEST_SMARTPTR
class Stub
{
public:
void print(){
cout<<"Stub:print"<<endl;
}
~Stub(){
cout<<"Stub:Destructor"<<endl;
}
};
template<typename T>
class SmartPtr
{
public:
SmartPtr(T*p=0):ptr(p),pUse(newsize_t(1)){}
SmartPtr(constSmartPtr&src):ptr(src.ptr),pUse(src.pUse){
++*pUse;
}
SmartPtr&operator=(constSmartPtr&rhs){
//self-assigningisalsoright
++*rhs.pUse;
decrUse();
ptr=rhs.ptr;
pUse=rhs.pUse;
return *this;
}
T* operator->(){
if(ptr)
return ptr;
throw std::runtime_error("accessthroughNULLpointer");
}
const T* operator->()const{
if(ptr)
return ptr;
throw std::runtime_error("accessthroughNULLpointer");
}
T &operator*(){
if(ptr)
return *ptr;
throw std::runtime_error("dereferenceofNULLpointer");
}
const T &operator*()const{
if(ptr)
return *ptr;
throw std::runtime_error("dereferenceofNULLpointer");
}
~SmartPtr(){
decrUse();
#ifdef TEST_SMARTPTR
std::cout<<"SmartPtr:Destructor"<<std::endl;//fortesting
#endif
}
private:
void decrUse(){
if(--*pUse==0){
delete ptr;
delete pUse;
}
}
T* ptr;
size_t* pUse;
};
int main()
{
try{
SmartPtr<Stub>t;
t->print();
}catch(constexception&err){
cout<<err.what()<<endl;
}
SmartPtr<Stub>t1(newStub);
SmartPtr<Stub>t2(t1);
SmartPtr<Stub>t3(newStub);
t3=t2;
t1->print();
(*t3).print();
return 0;
}

智能指針詳細信息

智能指針是存儲指向動態分配(堆)對象指針的類。除了能夠在適當的時間自動刪除指向的對象外,他們的工作機制很像C++的內置指針。智能指針在面對異常的時候格外有用,因為他們能夠確保正確的銷燬動態分配的對象。他們也可以用於跟蹤被多用户共享的動態分配對象。
事實上,智能指針能夠做的還有很多事情,例如處理線程安全,提供寫時複製,確保協議,並且提供遠程交互服務。有能夠為這些ESP (Extremely Smart Pointers)創建一般智能指針的方法,但是並沒有涵蓋進來。
智能指針的大部分使用是用於生存期控制,階段控制。它們使用operator->和operator*來生成原始指針,這樣智能指針看上去就像一個普通指針。
這樣的一個類來自標準庫:std::auto_ptr。它是為解決資源所有權問題設計的,但是缺少對引用數和數組的支持。並且,std::auto_ptr在被複制的時候會傳輸所有權。在大多數情況下,你需要更多的和/或者是不同的功能。這時就需要加入smart_ptr類。
smart_ptr 類
在Boost中的智能指針有:
。scoped_ptr,用於處理單個對象的唯一所有權;與std::auto_ptr不同的是,scoped_ptr可以被複制。
。scoped_array,與scoped_ptr類似,但是用來處理數組
。shared_ptr,允許共享對象所有權
。shared_array,允許共享數組所有權
scoped_ptr
scoped_ptr智能指針與std::auto_ptr不同,因為它是不傳遞所有權的。事實上它明確禁止任何想要這樣做的企圖!這在你需要確保指針任何時候只有一個擁有者時的任何一種情境下都是非常重要的。如果不去使用scoped_ptr,你可能傾向於使用std::auto_ptr,讓我們先看看下面的代碼:
auto_ptrMyOwnString?
(new string("This is mine to keep!"));
auto_ptrNoItsMine?(MyOwnString?);
cout << *MyOwnString << endl; // Boom
這段代碼顯然將不能編譯通過,因為字符串的所有權被傳給了NoItsMine。這不是std::auto_ptr的設計缺陷—而是一個特性。儘管如此,當你需要MyOwnString達到上面的代碼預期的工作效果的話,你可以使用scoped_ptr:
scoped_ptr MyOwnString?
(new string("This is mine to keep for real!"));
// Compiler error - there is no copy constructor.
scoped_ptr TryingToTakeItAnyway?
(MyOwnString?);
scoped_ptr通過從boost::noncopyable繼承來完成這個行為(可以查看Boost.utility庫)。不可複製類聲明覆制構造函數並將賦值操作符聲明為private類型。
scoped_array
scoped_array與scoped_ptr顯然是意義等價的,但是是用來處理數組的。在這一點標準庫並沒有考慮—除非你當然可以使用std::vector,在大多數情況下這樣做是可以的。
用法和scoped_ptr類似:
typedef tuples::tupleint> ArrayTuple?;
scoped_array MyArray?(new ArrayTuple?[10]);
tuples::get<0>(MyArray?[5]) ="The library Tuples is also part of Boost";
tuple是元素的集合—例如兩倍,三倍,和四倍。Tuple的典型用法是從函數返回多個值。Boost Tuple庫可以被認為是標準庫兩倍的擴展,目前它與近10個tuple元素一起工作。支持tuple流,比較,賦值,卸包等等。
當scoped_array越界的時候,delete[]將被正確的調用。這就避免了一個常見錯誤,即是調用錯誤的操作符delete。
shared_ptr
這裏有一個你在標準庫中找不到的—引用數智能指針。大部分人都應當有過使用智能指針的經歷,並且已經有很多關於引用數的文章。最重要的一個細節是引用數是如何被執行的—插入,意思是説你將引用計數的功能添加給類,或者是非插入,意思是説你不這樣做。Boost shared_ptr是非插入類型的,這個實現使用一個從堆中分配來的引用計數器。關於提供參數化策略使得對任何情況都極為適合的討論很多了,但是最終討論的結果是決定反對聚焦於可用性。可是不要指望討論的結果能夠結束。
shared_ptr完成了你所希望的工作:他負責在不使用實例時刪除由它指向的對象(pointee),並且它可以自由的共享它指向的對象(pointee)。
void PrintIfString?(constany&Any){
if(cons tshared_ptr* s=
any_cast>(&Any)){
cout<<**s<<endl;
}
}
int main(int argc,char* argv[])
{
std::vectorStuff;
shared_ptrSharedString1?
(new string("Shareme.Bytheway,Boost.anyisanotherusefulBoostlibrary"));
shared_ptrSharedString2?
(SharedString1?);
shared_ptrSharedInt1?
(newint(42));
shared_ptrSharedInt2?
(SharedInt1?);
Stuff.push_back(SharedString1?);
Stuff.push_back(SharedString2?);
Stuff.push_back(SharedInt1?);
Stuff.push_back(SharedInt2?);
//Printthestrings
for_each(Stuff.begin(),Stuff.end(),
PrintIfString?);
Stuff.clear();
//Thepointeesoftheshared_ptr's
//willbereleasedonleavingscope
//shared_ptr的pointee離開這個範圍後將被釋放
return0;
}

any庫提供了存儲所有東西的方法[2]HYPERLINK "file:///C:Documents%20and%20SettingsAdministrator桌面My%20Documents新建 CUJhtml20.04karlsson%22%20l"[4]。在包含類型中需要的是它們是可拷貝構造的(CopyConstructible),析構函數這裏絕對不能引發,他們應當是可賦值的。我們如何存儲和傳遞“所有事物”?無區別類型(讀作void*)可以涉及到所有的事物,但這將意味着將類型安全(與知識)拋之腦後。any庫提供類型安全。所有滿足any需求的類型都能夠被賦值,但是解開的時候需要知道解開類型。any_cast是解開由any保存着的值的鑰匙,any_cast與dynamic_cast的工作機制是類似的—指針類型的類型轉換通過返回一個空指針成功或者失敗,因此賦值類型的類型轉換拋出一個異常(bad_any_cast)而失敗。
shared_array
shared_array與shared_ptr作用是相同的,只是它是用於處理數組的。
shared_array MyStrings?( new Base[20] );
深入shared_ptr實現
創建一個簡單的智能指針是非常容易的。但是創建一個能夠在大多數編譯器下通過的智能指針就有些難度了。而創建同時又考慮異常安全就更為困難了。Boost::shared_ptr這些全都做到了,下面便是它如何做到這一切的。(請注意:所有的include,斷開編譯器處理,以及這個實現的部分內容被省略掉了,但你可以在Boost.smart_ptr當中找到它們)。
首先,類的定義:很顯然,智能指針是(幾乎總是)模板。
template classshared_ptr{
公共接口是:
explicitshared_ptr(T* p =0) : px(p) {// fix: prevent leak if new throwstry { pn = new long(1); }catch (...) { checked_delete(p); throw; }}
現在看來,在構造函數當中兩件事情是容易被忽略的。構造函數是explicit的,就像大多數的構造函數一樣可以帶有一個參數。另外一個值得注意的是引用數的堆分配是由一個try-catch塊保護的。如果沒有這個,你得到的將是一個有缺陷的智能指針,如果引用數沒有能夠成功分配,它將不能正常完成它自己的工作。
~shared_ptr() { dispose(); }
析構函數執行另外一個重要任務:如果引用數下降到零,它應當能夠安全的刪除指向的對象(pointee)。析構函數將這個重要任務委託給了另外一個方法:dispose。
void dispose() { if (—*pn == 0)
{ checked_delete(px); delete pn; } }
正如你所看到的,引用數(pn)在減少。如果它減少到零,checked_delete在所指對象 (px)上被調用,而後引用數(pn)也被刪除了。
那麼,checked_delete執行什麼功能呢?這個便捷的函數(你可以在Boost.utility中找到)確保指針代表的是一個完整的類型。在你的智能指針類當中有這個麼?
這是第一個賦值運算符
templateshared_ptr& operator=(const shared_ptr& r) {return *this;}
這是成員模版,如果不是這樣,有兩種情況:
1. 如果沒有參數化複製構造函數,類型賦值Base = Derived無效。
2. 如果有參數化複製構造函數,類型賦值將生效,但同時創建了一個不必要的臨時smart_ptr。
這再一次的展示給你為什麼不應當加入你自己的智能指針的一個非常好的原因—這些都不是很明顯的問題。
賦值運算符的實際工作是由share函數完成的:
void share(T* rpx,long* rpn){
if(pn=rpn){//Q:whynotpx=rpx?
//A:failswhenboth==0
++*rpn;//donebeforedispose()incase
//rpntransitivelydependenton
//*this(bugreportedbyKenJohnson)
dispose();
px=rpx;
pn=rpn;
}
}

需要注意的是自我賦值(更準確地説是自我共享)是通過比較引用數完成的,而不是通過指針。為什麼這樣呢?因為它們兩者都可以是零,但不一定是一樣的。
template shared_ptr
(const shared_ptr& r) : px(r.px) { // never throws
++*(pn =r.pn);
}
這個版本是一個模版化的拷貝構造和函數。可以看看上面的討論來了解為什麼要這樣做。
賦值運算符以及賦值構造函數在這裏同樣也有一個非模版化的版本:
shared_ptr(const shared_ptr& r) :
// never throws
px(r.px) { ++*(pn =r.pn); }
shared_ptr& operator=
(const shared_ptr& r) {
share(r.px,r.pn);
return *this;
}
reset函數就像他的名字那樣,重新設置所指對象(pointee)。在將要離開作用域的時候,如果你需要銷燬所指對象(pointee)它將非常方便的幫你完成,或者簡單的使緩存中的值失效。
void reset(T*p=0){
//fix:self-assignmentsafe
if(px==p)return;
if(—*pn==0)
{checked_delete(px);}
else{//allocatenewreference
//counter
//fix:preventleakifnewthrows
try{pn=new long;}
catch(...){
//undoeffectof—*pnaboveto
//meeteffectsguarantee
++*pn;
checked_delete(p);
throw;
}//catch
}//allocatenewreferencecounter
*pn=1;
px=p;
}//reset

這裏仍然請注意避免潛在的內存泄漏問題和保持異常安全的處理手段。
這樣你就有了使得智能指針發揮其“智能”的運算符:
// never throws
T& operator*() const { return *px; }
// never throws
T* operator->() const { return px; }
// never throws
T* get() const { return px; }
這僅僅是一個註釋:有的智能指針實現從類型轉換運算符到T*的轉換。這不是一個好主意,這樣做常會使你因此受到傷害。雖然get在這裏看上去很不舒服,但它阻止了編譯器同你玩遊戲。
我記得是Andrei Alexandrescu説的:“如果你的智能指針工作起來和啞指針沒什麼兩樣,那它就是啞指針。”簡直是太對了。
這裏有一些非常好的函數,我們就拿它們來作為本文的結束吧。
long use_count() const
{ return *pn; } // never throws
bool unique() const
{ return *pn == 1; } // never throws
函數的名字已經説明了它的功能了,對麼?
關於Boost.smart_ptr還有很多應當説明的(比如std::swap和std::less的特化,與std::auto_ptr榜定在一起確保兼容性以及便捷性的成員,等等),由於篇幅限制不能再繼續介紹了。詳細內容請參考Boost distribution ()的smart_ptr.hpp。即使沒有那些其它的內容,你不認為他的確是一個非常智能的指針麼?