CBase剖析

来源:互联网 发布:淘宝软文是什么 编辑:程序博客网 时间:2024/05/22 09:50

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()进行拷贝任务。
原创粉丝点击