Symbian 的 CBase類內幕-六個本質的問題 (轉載)
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()進行拷貝任務。