CBase類的內幕 - 六個本質問題

原文地址:http://www.cnjm.net/tech/article4202.html

譯者: 無牙老虎   http://blog.csdn.net/wenstory/archive/2008/05/25/2480097.aspx

 

大家都知道Symbian中的C,所謂的C類就是指派生於CBase的類,CBase廣泛地使用在Symbian API,因爲它表示該派生類將在堆中創建,每個Symbian程序員都知道如何調用CBase派生類的NewL()NewLC() (或者調用new (ELeave))去創建對象,但很多人並不深入地去理解CBase自身爲什麼有這些有趣的特徵.
 
 
如果你能回答如下問題,說明你是個好奇心很強的程序員, 那麼你可用跳過本文了,但如果你對自己的答案並不十分肯定,我建議你好好地讀這篇文章, 因爲CBase類是Symbian系統中最基本的,同時瞭解這些特徵也是很有趣的.
問題如下:

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


爲什麼CBase有公共(public)(virtual)析構函數?
CBase派生類是如何初始化爲二進制0值的?

爲什麼CBase派生類要初始化爲二進制0?派生類是如何初始化爲二進制0值的?

爲什麼不建議使用new[]初始化CBase派生類?

爲什麼CBase有私有(private)複製構造函數和私有(private) operator =函數?
 

讓我們一個個來解決這些問題.
 
爲什麼清潔棧有3個版本的PushL(),包括PushL( CBase *aPtr )?

這是個很有趣的問題, 清潔棧(CleanupStack)3個版本的PushL(),他們分別是:PushL(TAny *aPtr), PushL(CBase *aPtr) PushL(TCleanupItem anItem),爲什麼不是隻有PushL(TAny *aPtr)PushL(TCleanupItem anItem)?先讓我們看看清潔棧(CleanupStack)是怎麼工作的.通常情況下我們的代碼是這樣的:

CTest* test = CTest::NewL();        //CTest
CBase派生類
CleanupStack::PushL( test );
test->FunL();
CleanupStack::PopAndDestroy();

這是清潔棧的通用用法,由於FunL()可能會異常退出,所以我們將”test”指針壓入清潔棧接下來如果一切正常,則彈出指針並銷燬對象.讓我們關注一下清潔棧是如何通過調用PopAndDestroy()銷燬對象的,根據SDK helper的說明,”如果清潔棧中的項爲CBase類指針,則將該指針從清潔棧中移走並調用delete銷燬,如果爲TAny類指針,則將該指針從清潔棧中移走並調用User::Free()釋放對象所佔有的內存.”
 
爲什麼清潔棧要判斷指針的類型爲CBase或者TAny?這是因爲類可能提供私有的析構函數!如果這個類的析構函數是私有的,那麼調用delete則是非法的.在這種情況下,系統只有通過調用User::Free()來釋放對象內存而不是調用其析構函數.
 
那麼在CBase派生類中將會發生什麼情況?如果你看一看e32base.h(裏面有CBase的聲明,事實上只有部分聲明),你會發現Cbase有一個公有虛析構函數,這就保證了清潔棧可以在CBase派生類的指針上調用delete,這點對於你將一個非CBase類指針壓入清潔棧是非常有用的,清潔棧不會調用該類的析構函數.所以,在很多情況下,你都會將CBase派生類和一些沒有使用堆內存的類壓入到清潔棧中.
 
但如果你真的希望使用非CBase類分配堆內存,PushL()的第三個重載版本會對你有所幫助.你所需要做的就是定義一個函數去清理內存並將該對象包裝成TCleanupItem.
 
 
爲什麼CBase有公共(public)(virtual)析構函數?

我們可以將這個問題劃分到第二題,爲什麼是虛的,爲什麼是公有的?上面的答案已經告訴你爲什麼是公有的了.將其定義爲虛函數的原因是很簡單的,很多時候你希望將代碼寫成這樣:

CBase* test = CTest::NewL();        // CTest
CBase派生類
CleanupStack::PushL( test );
test->FunL();
CleanupStack::PopAndDestroy();
 
通過virtual關鍵字,清潔棧能保證通過基類指針正確地釋放對象.

 
CBase派生類是如何初始化爲二進制0值的?

非常幸運,由於Cbase所有的new operator函數都是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()發生異常退出,此時iPointer仍然爲不確定(大多數情況下爲非0),由於NewLC()CTest已經被壓入清潔棧所以系統會將CTest指針彈出並調用其析構函數,這就導致了問題的出現,由於if條件爲真,你將釋放指向一段非法內存的指針.大多數時候程序會崩潰.但如果iPointer被初始化爲0(NULL),你將不會遇到這樣的問題.
 
爲什麼不建議使用new[]初始化CBase派生類?

CBase
new operator有很多個重載版本,但是卻沒有new[] operator版本,所以如果你使用new[]創建CBase對象,則你得到的對象並沒有初始化爲二進制0,如果你想創建CBase派生類數組,你可以使用RPointerArray等類去實現.

 
 
 
爲什麼CBase有私有(private)複製構造函數和私有(private) operator =函數?
(或者說:爲什麼CBase的複製構造函數和operator =函數聲明爲私有(private)? –--譯者)

這是一個預防開發者意外進行淺度複製的通用方法.如果你寫的代碼如下:
 
CBase* pointer = new ( ELeave ) CBase;
CBase base = *pointer;                //
調用複製構造函數

編譯器將會提示"illegal access from CBase to protected/private member CBase::CBase(const CBase&)",這是由於第二行調用了CBase的複製構造函數.
或者你的代碼如下:
 
CBase* pointer = new ( ELeave ) CBase;
CBase base;
base = *pointer;                //
調用operator =

調用operator =函數時編譯器會顯示同樣的提示,如果你真的想進行深度複製,你可以實現自己的公有的複製構造函數和operator =函數.CBase這麼做的原因是:很多時候你在自己的CBase派生類內部申請了堆內存,對這寫類使用複製構造函數和operator =函數是沒有意義的(或者我可以說是很危險的).所以CBase默認就關閉了這些特性.

 

事實上,symbian中提供自己的公有複製構造函數和operator =函數並不是個好主意.因爲這2個函數都不是異常退出函數(這裏是指這2個函數名沒有以L結尾----譯者),但是這2個函數內部的代碼可能會異常退出(調用了new (ELeave) NewL()),這是個矛盾點,較好的習慣就是提供一個異常退出函數名實現複製,例如CloneL().

 

 

Inside CBase class - Six Essential Questions
Tutorial posted August 1st, 2007 by rensijie in

JAVA手機網[www.cnjm.net]

Basics
Platforms:
Symbian OS
Keywords:
CBase

Everybody knows C-class in Symbian, the so called C-class is the one derived from class CBase. CBase is widely used in Symbian APIs, because it represents the class which should be created on heap. Every Symbian programmer knows how to call NewL() or NewLC() ( may be new (ELeave) ) of the CBase derived class to create the object, but not many people would really look into the CBase class itself to see why it has some interesting features.

If you can answer the following questions, you can skip this article, because you are a Symbian programmer with strong curiosity. If you are not sure about some answers, I recommend you to read this ariticle, because CBase class is essential in Symbian OS and it's interesting to know some features of this class. The questions are:

JAVA手機網[www.cnjm.net]

Why does cleanup stack has 3 versions of PushL() including PushL( CBase *aPtr )?
Why does CBase have a public virtual destructor?
How is CBase derived object initialized to binary zeroes?
Why is CBase derived object initialized to binary zeroes?
Why use new[] to initialize CBase derived object is not recommended?
Why does CBase has a private copy constructor and a private operator = function?
Let's get into these questions one by one.


Why does cleanup stack has 3 versions of PushL() including PushL(CBase *aPtr)?

It's an interesting question, there're 3 versions of PushL() in CleanupStack, they're PushL(TAny *aPtr), PushL(CBase *aPtr) and PushL(TCleanupItem anItem), why not just PushL(TAny *aPtr) and PushL(TCleanupItem anItem)? Let's see how cleanup stack works. Usually we use the code like this:


CTest* test = CTest::NewL();        // CTest is a CBase derived class
CleanupStack::PushL( test );

JAVA手機網[www.cnjm.net]

test->FunL();

JAVA手機網[www.cnjm.net]

CleanupStack::PopAndDestroy();

It's the regular use of cleanup stack, push the pointer "test" into the cleanup stack because FunL() may leave, after that, if everything is fine, pop the pointer and destory the object. Let's consider how does cleanup stack destory the object when calling PopAndDestroy(), according to the SDK helper,
"If the item on the stack is a CBase* pointer, the pointer is removed from the stack and the object is destroyed with delete. If the item on the stack is a TAny* pointer, the pointer is removed from the stack and the memory occupied by the object is freed with User::Free()."

Why does cleanup stack has to judge if the pointer's type is CBase* or TAny*? Becasue a class may provide a private destructor! If a class has a private destructor, calling delete on this pointer will be invalid. In this case, system only calls User::Free() to free the memory of the object itself but can't invoke its destructor.

JAVA手機網[www.cnjm.net]

What happens to CBase derived class? If you take a look at e32base.h(the declaration of CBase is inside, actually part of the declaration), you will find CBase has a public virtual destructor. This ensures the cleanup stack can call delete on the CBase and its derived classes' pointers. It's useful to keep this in mind that if you push a non-CBase class pointer into the cleanup stack, the stack won't call your class's destructor. So, in most of the cases, you would like to either push CBase derived class into cleanup stack or never allocate heap memory in other types of classes.

But if you really want to allocate heap memory in other types of classes, the third version of PushL() can help you out. What you need to do is define a function which will do the cleanup and wrap the object by TCleanupItem.


Why does CBase have a public virtual destructor?

We can divide this question into 2 parts, why virtual, why public? The answer above tells you why public. The reason to make it virtual is simple. Sometimes you want to write the code like this:


CBase* test = CTest::NewL();        // CTest is a CBase derived class
CleanupStack::PushL( test );
test->FunL();
CleanupStack::PopAndDestroy();

With the virtual keyword, cleanup stack can make sure it will destroy the object properly by the base class's pointer.


How is CBase derived object initialized to binary zeroes?

Luckily, since all the new operator functions of CBase is inline, we can see the implementation of every function in e32base.inl. For example for "TAny* operator new(TUint aSize, TLeave)" the implementation is :

JAVA手機網[www.cnjm.net]



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

JAVA手機網[www.cnjm.net]


Here it uses User::AllocZL(), it allocates a cell of specified size from the current thread's default heap, clears it to binary zeroes, and leaves if there is insufficient memory in the heap. That's how CBase derived object is initialized to binary zeroes?


Why is CBase derived object initialized to binary zeroes?

Let's consider the code below :

JAVA手機網[www.cnjm.net]

 

JAVA手機網[www.cnjm.net]

CTest* CTest::NewLC()
{
       CTest* self = new ( ELeave ) CTest;
       CleanupStack::PushL( self );
       self->ConstructL()

JAVA手機網[www.cnjm.net]

       return self;
}

void CTest::ConstructL()
{
       iPointer = CMustLeave::NewL();        // assume this leaves
}

CTest::~CTest()
{
       if( iPointer )

JAVA手機網[www.cnjm.net]

       {
               delete iPointer;
               iPointer = NULL;
       }
}

If CBase doesn't initialize the object to binary zero, and you don't initialize the iPointer to NULL manually, the initial value of iPointer is uncertain. Once CMustLeave::NewL() leaves, the value of iPointer is still uncertain(in most of the cases it's not zero). Since in NewLC, CTest was pushed into the cleanup stack, so system will pop the pointer and call CTest's destructor. This will cause the problem, because the if condition will be true and you will call delete on a pointer which doesn't pointer to a legal memory. Mostly program will crash. You will not meet this problem if iPointer was initialized to zero(NULL).


Why use new[] to initialize CBase derived object is not recommended?

JAVA手機網[www.cnjm.net]

There're a number of overloaded new operator functions in CBase class, but there's no new[] operator function. So if you use new[] to create CBase objects, you will not get the memory with binary zero. If you want to create a array of CBase derived class you can use the class like RPointerArray to deal with it.

JAVA手機網[www.cnjm.net]



Why does CBase has a private copy constructor and a private operator = function?

This is a general method to prevent the developer from the shallow copy accidently. If you write the code like this :

JAVA手機網[www.cnjm.net]



CBase* pointer = new ( ELeave ) CBase;
CBase base = *pointer;                // call copy constructor

JAVA手機網[www.cnjm.net]


The compiler will complain "illegal access from CBase to protected/private member CBase::CBase(const CBase&)", because the second line will try to call the copy constructor of CBase. If you write the code like :


CBase* pointer = new ( ELeave ) CBase;
CBase base;

JAVA手機網[www.cnjm.net]

base = *pointer;                // call operator =

JAVA手機網[www.cnjm.net]


The compiler will also complain because it will call the operator = function. If you really want to do the deep copy you can write your own public copy constructor and operator = function. The reason that CBase do this is in most cases you will allocate some heap memory inside a CBase derived class, and it doesn't make sense(or I can say it's dangerous)to use the default copy constructor or default operator = function of this kind of class. So CBase turns this feature off by default.

Actually, in Symbian, to provide your own public version of copy contructor or operator = function is not a good idea neither. Because these 2 function are not leaving functions, but the code inside these 2 functions may leave sometimes( will call new (ELeave) or NewL() ). That's a paradox. The good manner is to provide a leaving function named, let's say, CloneL() to do the copy task.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章