CBase剖析

Symbian 的 CBase類內幕-六個本質的問題 (轉載)

每個人都知道Symbian中的C-class,是繼承自CBase類,CBase在Symbian中被廣泛的使用,因爲它表示這樣的一個類將被創建在堆上,每個Symbian程序員都知道如何調用CBase子類的NewL()或者NewLC()(也可能是new(ELeave))來構造一個對象,但是隻有很少人真正的去看CBase類裏面有趣的特徵。 如果你可以回答下面的問題,那麼你可以忽略這篇文章,因爲可以看出你在Symbian上是一個狂熱的程序員。如果你不確定部分答案,我建議你仔細閱讀一下這篇文章,因爲CBase類在Symbian OS裏面是基本的東西並且知道這個類裏面的一些特徵是非常有意思的。問題是:


1、爲什麼清除棧有3個PushL()版本,包括PushL(CBase *aPtr)?
2、爲什麼CBase有一個公共的虛析構函數
3、CBase的子類對象是如何被初始化爲0的?
4、爲什麼CBase的子類被初始化爲0?
5、爲什麼不建議使用new[]來初始化CBase的子類?
6、爲什麼CBase的 拷貝構造函數和賦值(操作符)函數是私有的?

讓我們一個一個的來看問題吧。

爲什麼清除棧有3個PushL()版本,包括PushL(CBase *aPtr)?


這是一個有意思的問題,在CleanupStack裏面有3個PushL()版本,分別是PushL(TAny *aPtr), PushL(CBase *aPtr) 和 PushL(TCleanupItem anItem), 我們來看看清除棧是如何工作的。一般我們是這樣使用的:

CTest* test = CTest::NewL();        // CTest 是一個繼承自 CBase的類
CleanupStack::PushL( test );
test->FunL();
CleanupStack::PopAndDestroy();

上面對清除棧的只用方法是常規的,把指針"test"推入清除棧,因爲FunL()可能會異常退出,之後,如果一切順利,那麼就彈出這個指針並且銷燬它。我們仔細考慮一下清除棧在調用PopAndDestroy()時是如何做到銷燬對象的,根據sdk幫助文檔
“如果在清除棧上的項是一個CBase* 指針,那麼這個指針將被從清除棧上移除並且這個對象將被銷燬!如果在清除棧上的項是一個TAny* 指針,那麼這個指針將被從清除棧中移除並且所佔據的內存被User::Free()釋放。”

爲什麼清除棧必須區分指針的類型是CBase*或者TAny*?因爲一個類提供的析構函數可能是私有的!如果一個類有一個私有析構函數,那麼對調用這樣的指針將是錯誤的。因此系統只是調用User::Free()來釋放對象佔據的內存,但是不會調用對象的析構函數。

繼承CBase的類發生了什麼?如果你看一下e32base.h(CBase的聲明在裏面,實際上只是部分聲明?),你會發現CBase有一個公共的虛析構函數,這樣可以確保清除棧可以在CBase和CBase的子類的指針上調用delete。這對於把一個非繼承自CBase類的指針推入清除棧是一個好的辦法,清除棧不會調用這種類的析構函數,因此,大多數時候,你會把CBase子類對象推入清除棧,而對於其它類型的類永遠都不在堆上分配內存空間。

但是如果你真的想要給一個非CBase類分配堆空間,第三個重載函數會幫助你實現。但是你需要定義一個函數用來完成清理任務,並且被封裝成TCleanupItem類型。

爲什麼CBase類有一個公共的虛析構函數?


我們可以把這個問題分成2部分,爲什麼是虛的,爲什麼是公共的?上面所闡述的內容可以告訴你爲什麼要用public。然而致使析構函數是虛的原因很簡單。有時候你會寫如下代碼


CBase* test = CTest::NewL();        // CTest 是一個繼承自 CBase的類
CleanupStack::PushL( test );
test->FunL();
CleanupStack::PopAndDestroy();

使用了virtual關鍵字,清除棧可以利用基類指針確保子類對象被完全銷燬。

CBase的子類對象是如何被初始化爲0的?


非常的幸運,CBase所有的new操作符功能都是inline的,我們可以在e32base.inl看到每一個函數的實現。比如說"TAny* operator new(TUint aSize, TLeave)" 的實現如下:


inline TAny* CBase::operator new(TUint aSize, TLeave)
       { return User::AllocZL(aSize); }


在這裏使用了User::AllocZL(),它在當前線程的缺省堆中分配具體大小的內存單元,並且把內存單元初始化爲二進制0,當堆上的內存不足時,將發生一個異常。這就是CBase子類如何把內存空間初始化爲二進制0。

爲什麼CBase的子類被初始化爲0?


觀察下面的代碼:
CTest* CTest::NewLC()
{
       CTest* self = new ( ELeave ) CTest;
       CleanupStack::PushL( self );
       self->ConstructL()
       return self;
}

void CTest::ConstructL()
{
       iPointer = CMustLeave::NewL();        // 假定這裏會異常退出
}

CTest::~CTest()
{
       if( iPointer )
       {
            delete iPointer;
            iPointer = NULL;
       }
}

如果CBase不把對象初始化爲二進制0,並且你也不手動的把iPointer初始化爲NULL,那麼iPointer的初始值就是不確定的。一旦
CMustLeave::NewL()發生了leaves,iPointer的值依然是不確定的(大多數時候它並不是0)。在NewLC中,CTest被推入清除棧中,
所以系統將要彈出指針以及調用CTest's的析構函數。這就會導致問題的產生,因爲if條件表達式將會是true並且將會在一個指向非法地址空間
的指針上調用delete。大多數程序都會崩。如果iPointer被初始化爲0(NULL),將不會產生這樣的問題。

爲什麼不建議使用new[]來初始化CBase的子類?

http://www.360doc.com/content/09/0311/20/59579_2780982.shtml
在CBase類裏面有許多new操作符重載,但是沒有new[]操作符。所以如果你使用new[]來創建CBase 對象,你得到的內存將不會是二進制0.如果想創建CBase子類的一個數組,你可以使用RPointerArray這樣的類型來進行處理。

爲什麼CBase的 拷貝構造函數和賦值(操作符)函數是私有的?


這是一個常規的辦法用來防止淺拷貝的發生。如果你寫了如下的代碼:
CBase* pointer = new ( ELeave ) CBase;
CBase base = *pointer;             // 調用拷貝構造函數

編譯器將會說“沒有權限訪問CBase類的保護/私有成員函數CBase::CBase(const CBase&)”,因爲第二行代碼試圖調用CBase類的拷貝構造函數。如果你寫了如下的代碼:


CBase* pointer = new ( ELeave ) CBase;
CBase base;
base = *pointer;             // call 操作符 =


編譯器同樣提示錯誤,因爲調用了操作符 = 。如果你真的想要進行深層拷貝,你可以寫自己公共的拷貝構造函數和操作符 = 。CBase這樣做的原因是大多數時候你將會對CBase的子類分配堆空間,然而我們會無意識(或者可以說這是危險的)的使用缺省的拷貝構造函數或者操作符 = 。所以CBase就把這特性屏蔽了。

實際上,在Symbian裏面,提供自己的公共的拷貝構造函數和賦值 =操作符是不好的,因爲這2個函數沒有leave功能。單證在這2個函數內部有可能導致異常(可能會調用new(ELeave)或者NewL())。這是很矛盾的。好的習慣是提供一個帶L的函數,也就是說使用CloneL()進行拷貝任務。
發佈了46 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章