天天看點

auto_ptr解析

  auto_ptr是目前C++标準庫中提供的一種智能指針,或許相對于boost庫提供的一系列眼花缭亂的智能指針, 或許相對于Loki中那個無所不包的智能指針,這個不怎麼智能的智能指針難免會黯然失色。誠然,auto_ptr有這樣那樣的不如人意,以至于程式員必須像使用”裸“指針那樣非常小心的使用它才能保證不出錯,以至于它甚至無法适用于同是标準庫中的那麼多的容器和一些算法,但即使如此,我們仍然不能否認這個小小的auto_ptr所蘊含的價值與理念。

  auto_ptr的出現,主要是為了解決“被異常抛出時發生資源洩漏”的問題。即如果我們讓資源在局部對象構造時配置設定,在局部對象析構時釋放。這樣即使在函數執行過程時發生異常退出,也會因為異常能保證局部對象被析構進而保證資源被釋放。auto_ptr就是基于這個理念而設計, 這最早出現在C++之父Bjarne Stroustrup的兩本巨著TC++PL和D&E中,其主題為"resource acquisition is initialization"(raii,資源擷取即初始化),然後又在Scott Meyer的<<More Effective C++>>中相關章節的推動下,被加入了C++标準庫。

  下面我就列出auto_ptr的源代碼,并詳細講解每一部分。因為标準庫中的代碼要考慮不同編譯器支援标準的不同而插入了不少預編譯判斷,而且命名可讀性不是很強(即使是侯捷老師推薦的SGI版本的stl,可讀性也不盡如人意), 這裡我用了Nicolai M. Josuttis(<<The C++ standard library>>作者)寫的一個auto_ptr的版本,并做了少許格式上的修改以易于分析閱讀。

1 構造函數與析構函數

auto_ptr在構造時擷取對某個對象的所有權(ownership),在析構時釋放該對象。我們可以這樣使用auto_ptr來提高代碼安全性:

int* p = new int(0);

auto_ptr<int> ap(p);

從此我們不必關心應該何時釋放p, 也不用擔心發生異常會有記憶體洩漏。

這裡我們有幾點要注意:

1) 因為auto_ptr析構的時候肯定會删除他所擁有的那個對象,所有我們就要注意了,一個蘿蔔一個坑,兩個auto_ptr不能同時擁有同一個對象。像這樣:

auto_ptr<int> ap1(p);

auto_ptr<int> ap2(p);

因為ap1與ap2都認為指針p是歸它管的,在析構時都試圖删除p, 兩次删除同一個對象的行為在C++标準中是未定義的。是以我們必須防止這樣使用auto_ptr.

2) 考慮下面這種用法:

int* pa = new int[10];

auto_ptr<int> ap(pa);

因為auto_ptr的析構函數中删除指針用的是delete,而不是delete [],是以我們不應該用auto_ptr來管理一個數組指針。

3) 構造函數的explicit關鍵詞有效阻止從一個“裸”指針隐式轉換成auto_ptr類型。

explicit構造函數隻能用于直接初始化,不能用于拷貝初始化(加上了“=”)。

4) 因為C++保證删除一個空指針是安全的, 是以我們沒有必要把析構函數寫成:

~auto_ptr() throw() 

{

 if(ap) delete ap;

}

2 拷貝構造與指派

與引用計數型智能指針不同的,auto_ptr要求其對“裸”指針的完全占有性。也就是說一個”裸“指針不能同時被兩個以上的auto_ptr所擁有。那麼,在拷貝構造或指派操作時,我們必須作特殊的處理來保證這個特性。auto_ptr的做法是“所有權轉移”,即拷貝或指派的源對象将失去對“裸”指針的所有權,是以,與一般拷貝構造函數,指派函數不同, auto_ptr的拷貝構造函數,指派函數的參數為引用而不是常引用(const reference).當然,一個auto_ptr也不能同時擁有兩個以上的“裸”指針,是以,拷貝或指派的目标對象将先釋放其原來所擁有的對象。

這裡的注意點是:

1) 因為一個auto_ptr被拷貝或被指派後, 其已經失去對原對象的所有權,這個時候,對這個auto_ptr的提領(dereference)操作是不安全的。如下:

auto_ptr<int> ap2 = ap1;

cout<<*ap1; //錯誤,此時ap1隻剩一個null指針在手了

這種情況較為隐蔽的情形出現在将auto_ptr作為函數參數按值傳遞,因為在函數調用過程中在函數的作用域中會産生一個局部對象來接收傳入的auto_ptr(拷貝構造),這樣,傳入的實參auto_ptr就失去了其對原對象的所有權,而該對象會在函數退出時被局部auto_ptr删除。如下:

void f(auto_ptr<int> ap){cout<<*ap;}

auto_ptr<int> ap1(new int(0));

f(ap1);

cout<<*ap1; //錯誤,經過f(ap1)函數調用,ap1已經不再擁有任何對象了。

因為這種情況太隐蔽,太容易出錯了, 是以auto_ptr作為函數參數按值傳遞是一定要避免的。或許大家會想到用auto_ptr的指針或引用作為函數參數或許可以,但是仔細想想,我們并不知道在函數中對傳入的auto_ptr做了什麼, 如果當中某些操作使其失去了對對象的所有權, 那麼這還是可能會導緻緻命的執行期錯誤。 也許,用const reference的形式來傳遞auto_ptr會是一個不錯的選擇。

2)我們可以看到拷貝構造函數與指派函數都提供了一個成員模闆在不覆寫“正統”版本的情況下實作auto_ptr的隐式轉換。如我們有以下兩個類

class base{};

class derived: public base{};

那麼下列代碼就可以通過,實作從auto_ptr<derived>到auto_ptr<base>的隐式轉換,因為derived*可以轉換成base*類型

auto_ptr<base> apbase = auto_ptr<derived>(new derived);

3) 因為auto_ptr不具有值語義(value semantic), 是以auto_ptr不能被用在stl标準容器中。

所謂值語義,是指符合以下條件的類型(假設有類A):

A a1;

A a2(a1);

A a3;

a3 = a1;

那麼

a2 == a1, a3 == a1

很明顯,auto_ptr不符合上述條件,而我們知道stl标準容器要用到大量的拷貝指派操作,并且假設其操作的類型必須符合以上條件。

3 提領操作(dereference)

提領操作有兩個操作, 一個是傳回其所擁有的對象的引用, 另一個是則實作了通過auto_ptr調用其所擁有的對象的成員。如:

struct A

 void f();

auto_ptr<A> apa(new A);

(*apa).f();

apa->f();

當然, 我們首先要確定這個智能指針确實擁有某個對象,否則,這個操作的行為即對空指針的提領是未定義的。

4 輔助函數

1) get用來顯式的傳回auto_ptr所擁有的對象指針。我們可以發現,标準庫提供的auto_ptr既不提供從“裸”指針到auto_ptr的隐式轉換(構造函數為explicit),也不提供從auto_ptr到“裸”指針的隐式轉換,從使用上來講可能不那麼的靈活, 考慮到其所帶來的安全性還是值得的。

2) release,用來轉移所有權

3) reset,用來接收所有權,如果接收所有權的auto_ptr如果已經擁有某對象, 必須先釋放該對象。

5 特殊轉換

這裡提供一個輔助類auto_ptr_ref來做特殊的轉換,按照标準的解釋, 這個類及下面4個函數的作用是:使我們得以拷貝和指派non-const auto_ptrs, 卻不能拷貝和指派const auto_ptrs. 我無法非常準确的了解這兩句話的意義,但根據我們觀察與試驗,應該可以這樣去了解:沒有這些代碼,我們本來就可以拷貝和指派non-const的auto_ptr和禁止拷貝和指派const的auto_ptr的功能, 隻是無法拷貝和指派臨時的auto_ptr(右值), 而這些輔助代碼提供某些轉換,使我們可以拷貝和指派臨時的auto_ptr,但并沒有使const的auto_ptr也能被拷貝和指派。如下:

auto_ptr<int> ap1 = auto_ptr<int>(new int(0));

auto_ptr<int>(new int(0))是一個臨時對象,一個右值,一般的拷貝構造函數當然能拷貝右值,因為其參數類别必須為一個const reference, 但是我們知道,auto_ptr的拷貝函數其參數類型為reference,是以,為了使這行代碼能通過,我們引入auto_ptr_ref來實作從右值向左值的轉換。其過程為:

1) ap1要通過拷貝 auto_ptr<int>(new int(0))來構造自己

2) auto_ptr<int>(new int(0))作為右值與現有的兩個拷貝構造函數參數類型都無法比對,也無法轉換成該種參數類型

3) 發現輔助的拷貝構造函數auto_ptr(auto_ptr_ref<T> rhs) throw()

4) 試圖将auto_ptr<int>(new int(0))轉換成auto_ptr_ref<T>

5) 發現類型轉換函數operator auto_ptr_ref<Y>() throw(), 轉換成功,進而拷貝成功。

進而通過一個間接類成功的實作了拷貝構造右值(臨時對象)

同時,這個輔助方法不會使const auto_ptr被拷貝, 原因是在第5步, 此類型轉換函數為non-const的,我們知道,const對象是無法調用non-const成員的, 是以轉換失敗。當然, 這裡有一個問題要注意, 假設你把這些輔助轉換的代碼注釋掉,該行代碼還是可能成功編譯,這是為什麼呢?debug一下, 我們可以發現隻調用了一次構造函數,而拷貝構造函數并沒有被調用,原因在于編譯器将代碼優化掉了。這種類型優化叫做returned value optimization,它可以有效防止一些無意義的臨時對象的構造。當然,前提是你的編譯器要支援returned value optimization。

  可見,auto_ptr短短百來行的代碼,還是包含了不少"玄機"的。 

以下代碼摘錄自More Effective C++

繼續閱讀