轉自: auto_ptr與shared_ptr --- auto_ptr (1)
auto_ptr與shared_ptr --- shared_ptr (2)
建議移步之~~
這篇文章試圖說明如何使用auto_ptr和shared_ptr,從而使得動態分配對象的使用和管理更安全,方便。除了一般的使用說明外,更主要是說明它們之間的異同 —— 滿足需求的不同和開銷上的差異。
文章的多數知識都來源於:
1. Exceptional C++(Herb)Item 37 auto_ptr
2. Exceptional C++ Style(Herb)和C++ Coding Standard(Herb,Andrei)其中一些關於使用shared_ptr的論述
3. GC++和VC++ 8.0 auto_ptr的源代碼
4. Boost庫shared_ptr的源碼和文檔
auto_ptr和shared_ptr都是智能指針的一種實現,所謂智能指針,多數情況下都是指這樣的一些對象:
1. 內部有一個動態分配對象的指針,擁有該對象的使用權和所有權(獨占或共享)。
2. 重載*和->操作,行為上跟所擁有的對象的指針一致。
3. 當自身的生命期結束的時候,會做一些跟擁有對象相關的清理動作。
auto_ptr
auto_ptr是現在標准庫里面一個輕量級的智能指針的實現,存在於頭文件 memory中,之所以說它是輕量級,是因為它只有一個成員變量(擁有對象的指針),相關的調用開銷也非常小。
下面的代碼來自於VC++ 8.0里面的源碼:
里面有個auto_ptr_ref的數據結構,我們可以把它忽略,這個只是內部使用的代理結構,用於一些隱式的const變化,我們客戶端代碼通常不會直接使用到它。
我們可以看到除了構造函數,拷貝構造函數,賦值函數,析構函數和兩個重載操作符(*,->)外,還有get,release和reset三個函數,它們的作用分別是:
1. get,獲得內部對象的指針
2. release,放棄內部對象的所有權,將內部指針置為空
3. reset,銷毀內部對象並接受新的對象的所有權(如果使用缺省參數的話,也就是沒有任何對象的所有權)
下面的例程來自Exceptional C++,Item 37:
//
Example 2: Using an auto_ptr
//
void g()
{
T* pt1 = new T;
// right now, we own the allocated object
// pass ownership to an auto_ptr
auto_ptr<T> pt2( pt1 );
// use the auto_ptr the same way
// we'd use a simple pointer
*pt2 = 12; // same as "*pt1 = 12;"
pt2->SomeFunc(); // same as "pt1->SomeFunc();"
// use get() to see the pointer value
assert( pt1 == pt2.get() );
// use release() to take back ownership
T* pt3 = pt2.release();
// delete the object ourselves, since now
// no auto_ptr owns it any more
delete pt3;
}
//
pt2 doesn't own any pointer, and so won't
//
try to delete it... OK, no double delete

//
Example 3: Using reset()
//
void h()
{
auto_ptr<T> pt( new T(1) );
pt.reset( new T(2) );
// deletes the first T that was
// allocated with "new T(1)"
}
//
finally, pt goes out of scope and
//
the second T is also deleted
從上面的例子來看,auto_ptr的使用很簡單,通過構造函數擁有一個動態分配對象的所有權,然后就可以被當作對象指針來使用,當auto_ptr對象被銷毀的時候,它也會自動銷毀自己擁有所有權的對象(嗯,標准的RAAI做法),release可以用來手動放棄所有權,reset可用於手動銷毀內部對象。
但實際上,auto_ptr是一個相當容易被誤用並且在實際中常常被誤用的類。原因是由於它的對象所有權占用的特性和它非平凡的拷貝行為。
auto_ptr的對象所有權是獨占性的!
這決定了不可能有兩個auto_ptr對象同時擁有同一動態對象的所有權,從而也導致了auto_ptr的拷貝行為是非對等的,其中伴隨着對象所有權的轉移。
我們仔細觀察auto_ptr的源碼就會發現拷貝構造和賦值操作符所接受的參數類型都是非const的引用類型(
auto_ptr
<_Ty>& ),而不是我們一般應該使用的const引用類型,查看源碼我們會發現:
auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
template<
class _Other>
auto_ptr<_Ty>&
operator=(auto_ptr<_Other>& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
拷貝過程中被拷貝的對象(_Right)都會被調用release來放棄所包括的動態對象的所有權,動態對象的所有權被轉移了,新的auto_ptr獨占了動態對象的所有權。也就是說被拷貝對象在拷貝過程中會被修改,拷貝物與被拷貝物之間是非等價的。這意味着如下的代碼是錯誤的(例程來自 Exceptional C++ Item 37):
//
Example 6: Never try to do work through
//
a non-owning auto_ptr
//
void f()
{
auto_ptr<T> pt1( new T );
auto_ptr<T> pt2;
pt2 = pt1; // now pt2 owns the pointer, and
// pt1 does not
pt1->DoSomething();
// error: following a null pointer
}
同時也不要將auto_ptr放進標准庫的容器中,否則在標准庫容器無准備的拷貝行為中(標准庫容器需要的拷貝行為是等價的),會導致難以發覺的錯誤。(請參考Exceptional C++ Item 37獲得更多信息)
auto_ptr特殊的拷貝行為使得使用它來遠距離傳遞動態對象變成了一件十分危險的行為,在傳遞的過程中,一不小心就會留下一些實際為空但程序本身卻缺少這樣認知的auto_ptr對象。
簡單的說來,auto_ptr適合用來管理生命周期比較短或者不會被遠距離傳遞的動態對象,使用auto_ptr來管理動態分配對象,最好是局限於某個函數內部或者是某個類的內部。也就是說,動態對象的產生,使用和銷毀的全過程是處於一個小的受控的范圍,而不會在其中加入一些適應未來時態的擴展。
shared_ptr
shared_ptr是Boost庫所提供的一個智能指針的實現,正如其名字所蘊意的一樣:
An important goal of shared_ptr is to provide a standard shared-ownership pointer.
shared_ptr的一個重要目的就是為了提供一個標准的共享所有權的智能指針。
—— Boost庫文檔
沒錯,shared_ptr就是為了解決auto_ptr在對象所有權上的局限性(auto_ptr是獨占的),在使用引用計數的機制上提供了可以共享所有權的智能指針,當然這不會沒有任何額外的代價……
首先一個shared_ptr對象除了包括一個所擁有對象的指針(px)外,還必須包括一個引用計數代理對象(
shared_count)的指針(pn)。而這個引用計數代理對象包括一個真正的多態的引用計數對象(
sp_counted_base)的指針(_pi),真正的引用計數對象在使用VC編譯器的情況下包括一個虛表,一個虛表指針,和兩個計數器。
下圖中result是一個shared_ptr對象,我們可以清楚看到它展開后所包含的數據:

假設我們有多個(5個以上)shared_ptr共享一個動態對象,那么每個shared_ptr的開銷比起只使用原生指針的開銷大概在3,4倍左右(這還是理想狀況,忽略了動態分配帶來的俑余開銷)。如果只有一個shared_ptr獨占動態對象,空間上開銷更是高度十數倍!而auto_ptr的開銷只是使用原生指針的兩倍。
時間上的開銷主要在初始化和拷貝操作上,*和->操作符重載的開銷跟auto_ptr是一樣的。
當然開銷並不是我們不使用shared_ptr的理由,永遠不要進行不成熟的優化,直到性能分析器告訴你這一點,這是Hurb提出的明智的建議。以上的說明只是為了讓你了解強大的功能背后總是伴隨着更多的開銷,shared_ptr應該被使用,但是也不要過於濫用,特別是在一些auto_ptr更擅長的地方。
下面是shared_ptr的類型定義:
template<
class T>
class shared_ptr
{

public:

typedef T element_type;

shared_ptr(); // never throws
template<class Y> explicit shared_ptr(Y * p);
template<class Y, class D> shared_ptr(Y * p, D d);
~shared_ptr(); // never throws

shared_ptr(shared_ptr const & r); // never throws
template<class Y> shared_ptr(shared_ptr<Y> const & r); // never throws
template<class Y> explicit shared_ptr(weak_ptr<Y> const & r);
template<class Y> explicit shared_ptr(std::auto_ptr<Y> & r);

shared_ptr & operator=(shared_ptr const & r); // never throws 
template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r); // never throws
template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & r);

void reset(); // never throws
template<class Y> void reset(Y * p);
template<class Y, class D> void reset(Y * p, D d);

T & operator*() const; // never throws
T * operator->() const; // never throws
T * get() const; // never throws

bool unique() const; // never throws
long use_count() const; // never throws

operator unspecified-bool-type() const; // never throws

void swap(shared_ptr & b); // never throws
};
大多數成員函數都跟auto_ptr類似,但是沒有了release(請看注釋),reset用來放棄所擁有對象的所有權或擁有對象的變更,會引起原有對象的引用計數的減少。
Note:
Boost文檔里面的QA說明了為什么不提供release函數
Q. Why doesn't shared_ptr provide a release() function?
A. shared_ptr cannot give away ownership unless it's unique() because the other copy will still destroy
the object.
Consider:
shared_ptr<
int> a(
new
int);
shared_ptr<
int> b(a);
//
a.use_count() == b.use_count() == 2
int * p = a.release();
//
Who owns p now? b will still call delete on it in its destructor.
Furthermore, the pointer returned by release() would be difficult to deallocate reliably,
as the source shared_ptr could have been created with a custom deleter.
use_count返回引用計數的個數,unique擁於確認是否是獨占所有權(use_count為1),swap用於交換兩個shared_ptr對象(即交換所擁有的對象),有一個bool類型轉換操作符使得shared_ptr可用於需要的bool類型的語境下,比如我們通常用if(pointer)來判斷某個指針是否為空。
Boost庫里面有很多shared_ptr的使用例程,文檔里面也列舉了許許多多shared_ptr的用途,其中最有用也最常用的莫過於傳遞動態分配對象,有了引用計數機制,我們現在可以安全地將動態分配的對象包裹在shared_ptr里面跨越模塊,跨越線程的邊界傳遞。shared_ptr為我們自動管理對象的生命周期,嗯,C++也可以體會到Java里面使用引用的美妙之處了。
另外,還記得Effective C++里面(或者其它的C++書籍),Scott Meyer告訴你的:在一個由多個模塊組成的系統里面,一個模塊不用試圖自己去釋放另外一個模塊分配的資源,而應該遵循誰分配誰釋放的原則。正確的原則但是有時難免有時讓人忽略(過於繁瑣),將資源包裝在shared_ptr里面傳遞,而shared_ptr保證了在資源不再被擁有的時候,產生資源的模塊的delete語句會被調用。
shared_ptr是可以拷貝和賦值的,拷貝行為也是等價的,並且可以被比較,這意味這它可被放入標准庫的一般容器(vector,list)和關聯容器中(map)。
shared_ptr可以用來容納多態對象,比如所下面的例子:
class Base
{
}
class Derived :
public Base
{
}
shared_ptr<Base> sp_base(
new Derived);
甚至shared_ptr也具備多態的行為:
Derived* pd =
new Derived;
shared_ptr<Derived> sp_derived(pd);
shared_ptr<Base> sp_base2(sp_derived);
上面的語句是合法的,shared_ptr會完成所需的類型轉換,當shared_ptr的模版參數Base的確是Derived的基類的時候。
最后是一個小小的提醒,無論是使用auto_ptr還是shared_ptr,都永遠不要寫這樣的代碼:
A* pa =
new A;
xxx_ptr<A> ptr_a_1(pa);
xxx_ptr<A> ptr_a_2(pa);
很明顯,在ptr_a_1和ptr_a_2生命周期結束的時候都會去刪除pa,pa被刪除了兩次,這肯定會引起你程序的崩潰,當然,這個誤用的例子比較明顯,但是在某種情況下,可能會一不小心就寫出如下的代碼(嗯,我承認我的確做過這樣的事情):
void DoSomething(xxx_ptr<A>)
{
//do something
}
class A
{
doSomething()
{
xxx_ptr<A> ptr_a(this);
DoSomething(ptr_a);
}
};
int main()
{
A a;
a.doSomething();
//continue do something with a, but it was already destory
}
在函數a.doSomething()里面發生了什么事情,它為了調用DoSomething所以不得不把自己包裝成一個xxx_ptr,但是忘記在函數結束的時候,xxx ptr_a被銷毀的同時也銷毀了自己,程序或者立刻崩潰或者在下面的某個時間點上崩潰!
所以你在使用智能指針做為函數參數的時候請小心這樣的誤用,有時候使用智能指針作為函數參數不一定是一個好注意。比如請遵循下面的建議,請不要在屬於類型A的接口的一部分的非成員函數或者跟A有緊密聯系的輔助函數里面使用xxx_ptr<A>作為函數的參數類型。
