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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值