Symbian异常三步曲之二:清除栈(CleanupStack)

来源:互联网 发布:编程好书 编辑:程序博客网 时间:2024/05/20 06:23

一、为什么使用清除栈
清除栈主要是用来处理在异常退出发生时那些或许可以称之为被遗弃或泄漏的内存。
看下面的代码:
void UnsafeFunctionL()
{
       CClanger* clanger = new(ELeave) CClanger();
       clanger->InitializeL();
       ……..//略去
        delete clanger;
}

分析:一旦clanger->InitializeL()运行时发生异常,则clanger所指的堆内存将会泄漏,这个例子我们在三步曲之一中提到过。
那么有什么办法来处理这种潜在的错误呢?
我们很容易就想到,可以使用TRAPD捕获异常来进行堆内存的释放,即将上面代码进行如下修改:
void UnsafeFunctionL()
{
       CClanger* clanger = new(ELeave) CClanger();
       TRAPD(error,clanger->InitializeL());
       If(KErrNone != error)
{
               delete clanger;
}
       ……..//略去
       delete clanger;
}
也就是说通过TRAPD捕获异常,然后对异常错误码进行判断,如果确实发生异常了,那么就调用delete来释放堆内存。
当然,上面办法是可行的,但是,如果存在多个可能异常的函数,那么我们都使用TRAPD捕获异常的话,所造成的系统开销就会非常之大,清除栈就是为了解决这个问题而存在的,并且它很好的解决了这样类型的一系列问题。

二、使用清除栈
类CleanupStack定义在头文件e32base.h中,可以通过这个类提供的静态成员函数来访问清除栈。
清除栈的原理:
在调用可能发生异常退出的代码之前,非异常退出安全的对象应该被置于清除栈上,这可以保证在异常退出发生时,这些对象可以被正确的销毁。当发生异常退出时,清除栈能够将所有已经置于其上的对象回收。

下面就是一个使用清除栈的小例子:
void SafeFunctionL()
{
       CClanger* clanger = new(ELeave) CClanger;
       CleanupStack::PushL(clanger);
       clanger->InitializeL();
       clanger->DoSomethingElseL();
       CleanupStack::Pop(clanger);
       delete clanger;
}
实际上这个函数中的最后两条语句
CleanupStack::Pop(clanger);
delete clanger;

可以使用CleanupStack::PopAndDestroy(clanger);这一条语句来替代,它们是等价的。
如果在调用clanger->InitializeL();或clanger->DoSomethingElseL();的时候异常退出了,clanger对象就会被清除栈销毁。

comments:只有当你的调用可能(只要有可能)会导致异常退出,你就必须把它弄到清除栈上去,否则的话(也就是你能相当确认后续的操作不会导致异常发生),完全没有必要,而且如果你这样做的话也是浪费系统资源,清除栈上放一个指针虽然只有4个字节,但也是肉啊!

顺序问题:
对象必须以严格的顺序压入和弹出清除栈,一组Pop()操作必须与PushL()调用的顺序相反。可能会发生调用Pop()或PopAndDestroy()弹出对象而没有命名这些对象的情况,但最好还是要对弹出对象进行命名。
举例:
void ContrivedExampleL()
{
       CSiamese* sealPoint = NewL(ESeal);
       CleanupStack::PushL(sealPoint);
       CSiamese* chocolatePoint = NewL(EChocolate);
       CleanupStack::PushL(chocolatePoint);
       CSiamese* violetPoint = NewL(EViolet);
       CleanupStack::PushL(violetPoint);
       CSiamese* bluePoint = NewL(EBlue);
       CleanupStack::PushL(bluePoint);
     
       sealPoint->CatchMouseL();//入清除栈语句放在可能异常退出的代码之前
     
       CleanupStack::PopAndDestroy(bluePoint);
       CleanupStack::PopAndDestroy(violetPoint);
       CleanupStack::PopAndDestroy(chocolatetPoint);
       CleanupStack::PopAndDestroy(sealPoint);

}

可以看到出栈的顺序和入栈的顺序正好是相反的,不过在这里显得复杂了一点,上面的四个出清除栈语句可以使用CleanupStack::PopAndDestroy(4);或CleanupStack::PopAndDestroy(4,sealPoint);进行代替效果基本是一样的。

comments:清除栈,清除栈,也是一个栈啊,栈的操作就是先进后出,所以,最后push到栈上的要先pop出来,先push的最后出来,很简单吧!


创建在堆上的对象由谁销毁?

对于一个对象,清除永远不能超过一次。如果清除栈上有一个指向对象的指针,而后来又保存到其它地方了,譬如成了另一个对象的成员变量,而这个对象在异常退出后仍可以被访问,那么就应该从清除栈上弹出这个指针。如果在清除栈上保留了这个指针,那么清除栈会销毁它所指的对象,但是保存了该指针的对象也会试图通过析构函数销毁这个指针所指的对象。
因此,对象应该只被清除栈或另一个对象所引用,而不能同时被两者引用。类似的,永远不要将类成员变量压入清除栈。
小例子:
void TransferOwnershipExampleL()
{
       CClanger* clanger = new( ELeave ) CClanger();
       CleanupStack::PushL(clanger);//压入清除栈
       iMemberObject->TakeOwnershipL(clanger);//类成员iMemberObject获得了对象所有权
       CleanupStack::Pop(clanger);//这里就必须从清除栈中弹出对象指针
                          //调用完异常代码后将对象指针从清除栈中弹出

}

comments:
永远不要将类成员变量压入清除栈,会导致double-deletion

命名问题:
如果有对象被压入清除栈,并直至函数返回时还保留在清除栈上,则该函数应该以”C”作为后缀。这就告诉函数调用者,如果函数正常返回,清除栈上仍然有多余的对象。
CSiamese* CSiamese::NewLC(TpointColor aPointColor)
{
       CSiamese* me = new( ELeave ) CSiamese( aPointColor );
       CleanupStack::PushL( me );
       me->ConstructL();
       return me;
}
上面的函数实际是用在对象的二阶段构造中,其中,压入清除栈后并没有弹出,因此命名时必须要用”C”结尾。

三、对非CBase派生类使用清除栈

看一下CleanupStack::PushL()的三种重载形式:
(1)CleanupStack::PushL(CBase* aPtr)
(2)CleanupStack::PushL(TAny*)
(3)CleanupStack::PushL(TCleanupItem)


第一种形式:用在CBase的派生类对象上,当popanddestroy时,会调用该派生类对象的析构函数,跟C++一样,先调用派生层次最深类的析构函数,然后沿着派生层次顺次向上调用析构函数,最后调用CBase的空析构函数。

第二种形式:如果在定义一个C类时,忘记了从CBase派生,那么就会很危险(千万不要这么做,如果你这么做了,出什么事情,你自己要负全责),因为在调用PushL时,实际上调用的是CleanupStack::PushL(TAny*)这个方法,这样在pop时就不会调用这个类的析构函数,仅仅是清除它所指向的堆内存而已。实际上该方法被用来将没有析构函数的、基于堆的对象的指针压入清除栈(比如T类对象或结构体)。

第三种形式:接收一个TcleanupItem类型对象作为参数,这个函数可以使我们将其他类型的对象或那些具有定制的清除例程的对象压入清除栈。TCleanupItem对象封装了一个指向要保存在清除栈上对象的指针和一个指向提供相应对象清除操作的函数指针。

另外,Symbian OS还提供的三个用于清除的工具函数--
CleanupReleasePushL(), CleanupDeletePushL(), CleanupClosePushL(),分别对应是Release()、Delete()、Close(),都会生成一个TCleanupItem对象让我们能够自己定义清除的过程。并结合下面三个入栈方法。当然,这里入栈的对象引用,可以是创建在堆上的任意对象,比如C类对象,R类对象,T类对象,M类对象等等均可。
(1)       CleanupReleasePushL( T& aRef注意参数不是T*) //

异常退出的处理或PopAndDestroy()调用将对T类对象调用Release()。
举例:
class MExtraTerrestrial
{
public:
          virtual void CommunicateL() = 0;
          …..//出于整洁,略去接口其他代码
          virtual void Release() = 0;
}

class CClanger : public CBase , MExtraTerrestrial
{
public:
          static MExtraTerrestrial* NewL();
          virtual void CommunicateL();
          virtual void Release();
private:
          CClanger();
          ~CClanger();
private:
          ……..
}
void TestMixinL()
{
MExtraTerrestrial* clanger = Clanger::NewL();
CleanupReleasePushL(*clanger);//参数不是指针,这点和普通PushL不同
……..//执行可能发生异常退出的代码
CleanupStack::PopAndDestroy(clanger);//这里是指向对象的指针
}
注意:入清除栈和出清除栈时的参数是不一样的。
(2)       CleanupDeletePushL( T& aRef)
通过使用CleanupDeletePushL()可以使异常退出处理或PopAndDestroy()调用对对象施以delete操作,进而调用对象的析构函数,并且相应的堆内存也会被释放。这就类似于使用接受CBase指针的CleanupStack::PushL()重载函数。当必须要将M派生类指针置于清除栈上时,该函数尤为有用。

(3)       CleanupClosePushL( T& aRef类对象内置了Close()方法,不用另外添加了。) //R
如果对象是通过CleanupClosePushL()压入清除栈的话,则异常退出处理或PopAndDestroy()调用将对对象施以close()操作。
void UseFilesystemL()
{
RFs theFs;
User::LeaveIfError(theFs.Connect());
CleanupClosePushL(theFs);
……..//执行可能发生异常退出的代码
CleanupStack::PopAndDestroy(&theFs);
}

Symbian OS 提供的这三个工具模板函数,它们分别对应于Release()、Delete()、Close()这三个清除方法,这3个工具函数都会生成一个TCleanupItem类型的对象并将其压入清除栈中。

comments:其实是上面的三个函数会生成一个TCleanupItem对象,然后这个对象会自动调用相应的Close()或是其他的函数。

小结:
系统中每个分配了资源的可执行单元(或者线程)都有它自己的清理栈和最高级别的TRAP/TRAPD宏来做异常处理和一些退出后的善后工作。之所以引入清除栈,就是为了解决堆内存泄漏的问题,注意是堆内存,如果对象被创建在了栈上的话,这是不关清除栈的事的,因为栈上的对象所占空间由栈自动管理。

注意:对于C类对象而言,CleanupStack::Pop()方法仅仅是将C类对象指针从清除栈中弹出了而已,并没有调用这个C类对象的析构函数,若要析构,需要再加语句delete c,或者可以直接使用CleanupStack::PopAndDestroy()同时完成上面两个动作。