天天看點

《深入了解Android》一3.2 智能指針

本節書摘來自華章出版社《深入了解android》一書中的第3章,第3.2節,作者孟德國 王耀龍 周金利 黎歡,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

智能指針(smart ptr)泛指一類原生指針的封裝,它的行為非常類似于原生指針。通過它,c++程式可以自動化地實作資源管理(比如對象的自動析構)以及很多包裹指針的衍生操作,如内容拷貝、引用計數、autolocker、lazy evaluation等。

 smart ptr使用廣泛,有衆多的原型及功能,幾乎在所有的大型工程中都可以看到它們的身影,本章由于篇幅及目标定位所限,不能一一列舉,有興趣的讀者可以參考stl、boost、android、chromium-base等開源代碼。

smart ptr可以實作多種行為,其經典應用是實作動态配置設定對象的記憶體自動回收。下面介紹smart ptr的工作原理。

在c++語言中,smart ptr對象作為棧上配置設定的(stack-allocated)自動變量(對象)存在,在代碼執行上下文退出其作用域時被自動析構(自動析構由編譯器插樁的析構函數調用以及abi中規定的退棧操作聯合實作)

smart ptr的析構函數中一般包含被引用對象的delete操作,進而間接實作了被引用對象的自動析構。

smart ptr自動回收資源這一功能在異常發生的情況下尤其有用,異常發生處之後的代碼可能不會執行。對于某些不合理的設計,如果誤将資源回收操作(如delete)放置在此範圍内,可能因delete未被執行而引起記憶體洩露。對于使用smart ptr的場景,異常棧展開的過程會析構棧上配置設定的變量,當然也就包括棧上配置設定的smart ptr對象,使smart ptr引用的資源一樣可以被正确回收。wtf 庫中smart ptr的行為正是實作被引用對象的引用計數及記憶體的自動回收。

下面展示smart ptr的一般實作結構:

【→smart pointer示例】

建立smart ptr對象的格式如下:

由上述代碼可知:

入口參數一般是堆上配置設定的對象,當然這本質上取決于析構函數的行為。

拷貝構造函數和拷貝指派運算符的實作,涉及被引用對象的所有權傳遞行為,其實作可以是将被引用對象的所有權直接傳遞給新對象或者右值對象,也可以是增加被引用對象的引用計數。

定義取值(operator *)和指針(operator ->)運算符的目的是使smart ptr在行為上類似于原生指針。

定義operator!()運算符是因為c/c++代碼存在對指針是否為空的簡潔判斷語句if(!ptr)。定義operator!()後,smart ptr可以應用在if(!smatrptr) 和if(!!smartptr)這樣的場景中。

operator ptrtype()是裸指針轉換操作,使得smart ptr在c++編譯系統提供的一次類型轉換的幫助下能夠用在需要裸指針的地方,同時也可以使用裸指針的值來參與邏輯運算,比如if(smartptr)。

smart ptr的實作和使用時需要注意的細節,将會在3.2.1節和3.2.2節中進行描述。

依據複制和指派時行為的不同,wtf提供了ownptr和refptr這兩類smart ptr,本章将以android 4.2的代碼為藍本來介紹它們的實作。

先來看一下ownptr的主要定義。

【→ownptr.h】

上面摘取的是ownptr的主要實作部分,下面來分析其實作和使用的要點。

關鍵點(1):與前面給出的smartptr主要的不同在于引入了removepointer類,本質上removepointer就是一種type_traits,用于萃取type屬性。這種type traits行為在stl中疊代器(iterator)的實作中經常使用,讀者應該不會陌生。

關鍵點(2)(3):統稱拷貝構造函數,但關鍵點(3)是嚴格意義上的定義,關鍵點(2)算作類型轉換構造函數,應用關鍵點(2)一般要求u和t可以互相轉換。在分析passownptr之前,可以暫時認為passownptr和ownptr完全等同(wtf設計passownptr主要作為ownptr的傳遞形态,也就是輔助完成複制及指派)。對于關鍵點(3),細心的讀者會發現ownptr的實作中隻提供了拷貝構造函數的定義,并未提供拷貝構造函數的實作,是以拷貝構造函數永遠不應該被調用,否則必然出現連結錯誤,也就是說ownptr是不允許拷貝構造的。

再看一下leakptr()的實作:

【→passownptr.h】

從上述leakptr的實作可以看出,通過這個函數passownptr會放棄對象的所有權,并将被引用對象的指針傳回。

關鍵點(7)(8):在具體分析關鍵點(7)(8)前,我們先分析一下ownptr的拷貝指派運算符,其實ownptr類沒有提供拷貝指派運算符的聲明和實作,是以它采用預設的拷貝指派運算,故其行為就是bitwise copy,這種淺拷貝是非常危險的,使用不當極易引起被引用對象的重複删除,進而觸發異常。其實在最新的wtf代碼中,ownptr的拷貝指派運算符在編譯器不支援右值引用的情況下,被聲明為private,即不允許使用:

android 4.2中ownptr的實作還是比較危險的,希望讀者在使用時注意。

在這裡,我們看到了ownptr的一個非常重要的特征—對象所有權的傳遞,源passownptr的m_ptr被指派0,新ownptr的m_ptr指向了源對象。ownptr借助passownptr實作對象所有權的傳遞。ownptr的這一特征使得将ownptr作為函數參數和傳回值時,一般要傳遞引用,有意要将對象所有權轉交到函數中或者函數外的情況除外。從上述代碼中我們也可以看到拷貝指派運算符的傳回值是ownptr&。

關鍵點(9):一個比較考究的設計。在前面的【→smart pointer示例】中筆者展示了傳回raw ptr的類型轉換操作,傳回raw ptr的設計可以應用于更多的場合,但是若使用者對被引用對象的生存期把握不精準,可能引起空指針問題。相比而言,關鍵點(9)傳回指向類内部成員的指針,因内部增加了對m_ptr的判斷而更加安全。

wtf為了可以在函數的參數和傳回值中使用ownptr,而設計了passownptr。passownptr的實作和行為本質上跟ownptr一緻,可以了解為ownptr的傳遞形态,讀者可自行分析。

refptr和ownptr的不同之處在于refptr要操作對象的引用計數,這就限制了refptr可以引用的對象的範圍—包含ref()和deref()方法的類的對象才可以由refptr引用。

那麼每一個将被refptr引用的類都要手工的重複的去添加ref()和deref()函數嗎?答案是沒有必要,因為wtf提供了refcounted類模闆。以下代碼列出了refcounted類的定義:

refcounted類的定義分成兩部分:refcountedbase類和refcounted類,其目的是使模闆類refcounted中的代碼盡可能少,因為refcounted類模闆會被大量繼承和使用,refcounted類盡可能少的代碼可以避免模闆類大量執行個體化而引起代碼過分膨脹。有了refcounted模闆,那麼任何一個類class想要被refptr引用,隻需要繼承自refcounted。關鍵點(1)處代碼顯式将this指針轉成其子類對象指針,目的是:

删除子類對象,因為refcounted模闆的析構函數沒有聲明為virtual函數,是以顯式轉化為子類指針是必需的。

有了前面對smartptr和ownptr的分析,對于refptr隻需分析一下拷貝指派運算符。

【→refptr.h】

該段代碼的核心在refifnotnull和derefifnotnull,也就是增加和減少引用計數,其實作比較簡單,讀者可自行分析。

本節讨論一下被smart ptr索引的對象的線程安全性問題。

ownptr索引的對象顯然是線程不安全的,而refptr索引的對象若繼承自refcounted,則引用計數連同整個被索引的對象都不是線程安全的。對于繼承自threadsaferefconunted (threadsaferefcounted.h)的類的對象,它隻保證引用計數的線程安全性,并不保證對refptr所引用的整個對象讀寫的線程安全性。refptr和ownptr一樣是線程不安全的,涉及多線程寫通路它們索引對象的情況要注意加鎖。

細心的讀者可能還注意到crossthreadrefconted(crossthreadrefconted.h)類,該類在android 4.2版的webkit中使用非常少,雖然它也有ref和deref函數,但它卻不能也不應該被當作基類來繼承,因為其構造和析構函數是private的,并且其deref函數首先調用了delete this,然後其析構函數又調用了delete m_data,這種語義隻能用來直接索引某個對象,也就是—template refptr >。

繼續閱讀