Symbian OS异常三步曲之一:异常退出(leave)
Symbian的异常处理有别于标准C++的异常处理机制,主要原因是最初在设计Symbian的异常处理机制时,C++还没有引入异常处理,但是从Symbian OS 9.1开始,Symbian开始支持标准C++的try—catch异常处理机制,不过考虑到系统开销以及兼容性的因素,我们提倡使用Symbian特有的异常处理机制即异常退出。
一、异常退出函数
当调用异常退出函数或显式调用系统函数时可能会发生异常退出。如果一旦异常退出发生,就会抛出一个异常,并同时产生一个错误码,这个错误码会沿着调用栈传递,直到被最近的一个异常捕获模块所捕获。
注意:一旦发生异常退出,异常退出点后的代码不再执行,而是转入最近的捕获模块,捕获完毕接着执行捕获模块后的代码。这样做,并不会中止程序流,这和侦测程序错误的断言assert是不同的(Symbian中常用的断言宏有_ASSERT_ALWAYS和_ASSERT_DEBUG),断言会中止程序继续执行。
异常退出函数是执行了不能保证一定成功的操作(例如在低内存容量下分配内存)的函数。
关于异常退出函数的返回值:除非需要将函数中所分配资源的指针或引用作为返回值,否则异常退出函数应该是没有返回值的。关键是我们将一个函数做成一个异常退出函数好还是做成一个返回错误代码的普通函数合适。后面我们会做详细分析。
举几个异常退出函数声明的例子:
void InitializeL( );
static CTestClass* NewL( );
RClangerHandle& CloneHandleL( );
从上面3个例子可以看到:异常退出函数的名字都是以“L”结尾的,这是必须的,如果不用L表明异常退出函数的话,别人在调用你的函数时,可能由于没有捕获异常,导致潜在的内存泄漏发生。
函数在下列3种情况下可能发生异常退出:
(1) 调用了可能异常退出的代码,并且在调用代码的周围没有使用异常捕获模块。
(2) 调用了一个会产生异常退出的系统函数,如User::Leave( )或User::LeaveIfError( )
(3) 使用了以ELeave为参数的new操作符重载形式。
下面看一下这几个系统异常退出函数的理解:
(1)User::LeaveIfError( ):会测试传入其中的一个整数参数值,如果该值小于零,譬如在e32std.h中定义的某个KErrXXX错误常量,则产生一个异常退出。这个函数可用来将返回标准Symbian OS错误码的无异常退出函数转化成一个以对应值作为异常退出码的异常退出函数。
(2)User::Leave( ):不作任何参数值的检查,而只是简单的异常退出,并以传入的整数值为异常退出码。
(3)User::LeaveNoMemory( ):只是简单的异常退出,但异常退出码被硬编码成KerrNoMemory,它的效果等同于调用User::Leave(KErrNoMemory)。
(4)User::LeaveIfNull( ):接受一个指针作为参数,如果该指针为NULL,则以KerrNoMemory为异常退出码发生异常退出。
后缀“L”在编译时不被检查,所以由于我们忘记添加“L”或给原来的无异常退出函数添加了异常退出代码等原因,为此可以使用Symbian OS为我们提供的工具LeaveScan。
二、使用new(ELeave)进行基于堆的内存分配
使用new(ELeave)来为对象在堆上分配内存,当内存不足时将会发生异常退出。因此我们可以直接使用其返回的指针,而无需做进一步的测试来确定内存是否分配成功。实质是,new(ELeave)中已经对指针是否为NULL进行了if判断。
举例:
CClanger* InitializeClangerL( )
{
CClanger* clanger = new(ELeave)CClanger();
if(clanger == NULL)//这条语句多余,可以省略
{
CleanupStack::PushL(clanger);
clanger->InitializeL();
CleanupStack::Pop(clanger);
}
return clanger;
}
上面的例子中对clanger进行了是否分配内存成功的测试,实际是多余的,完全可以省略。
三、构造函数和析构函数
这两个函数是绝对不允许发生异常退出的。因为如果构造时发生异常,那么可能会因为缺乏足够的资源而无法创建或初始化对象,但对象空间已分配,导致内存泄漏;如果析构时发生异常,将导致对象的不完全析构,这就可能造成资源的泄漏。
解决上面问题的办法其实很简单:将构造函数中可能异常的代码提取出来,放在一个单独的异常退出函数ConstructL()中,利用二阶段完成对象的构造。也可将析构函数中可能异常的代码提取出来,放在CommitL()或FreeResource()中,在析构之前调用它,给调用者一个机会来处理可能发生的问题。
四、使用异常退出函数
例1:
void FunctionMayLeaveL()
{
CTestClass* ironChicken = CTestClass::NewL();
ironChicken->FunctionDoesNotLeave();
delete ironChicken;
}
例2:
void UnsafeFunctionL()
{
CTestClass* test = CTestClass::NewL();
test->FunctionMayLeaveL();
delete test;
}
比较上面两个例子:
(1) 两个函数都是异常退出函数,都可能异常退出,并且ironChicken和test都是局部变量。
(2) 我们在调用两个函数中的类CTestClass的NewL方法产生对象指针以后,无需对指针进行是否NULL的判断。因为构建成功的话,指针绝对不会为NULL;而构建失败的话,NewL方法将异常退出。
(3) 由于采用二阶段构造,即使NewL方法构建对象失败,仍然能够保证ironChicken和test的堆内存安全释放。
(4) 但是例1中,ironChicken调用不会异常的函数,因此delete ironChicken可以使ironChicken的堆内存安全释放;而例2中,test调用可能异常的函数,一旦异常发生,那么delete test语句将会执行不到,从而导致test所指的堆内存不能得到释放,从而导致内存泄漏,这是需要特别注意的。
针对于例2,再看下面这个例3:
void CTestClass::SaftFunctionL()
{
iMember = CclangerClass::NewL();//iMember是类成员
FunctionMayLeaveL();
}
在这个例子中,我们首先构造了iMember,然后同样是调用一个异常退出函数FunctionMayLeaveL(),但是这里即使发生异常,iMember所指的堆内存仍然可以安全释放,因为在这里iMember是类CTestClass的成员,它的堆内存释放是由析构函数来完成的,而不是在FunctionMayLeaveL()语句后执行delete释放的,所以即使这里发生异常,只要能够调用析构函数,也不会发生堆内存泄漏。
五、用TRAP和TRAPD捕获异常退出
1、使用格式
Symbian OS提供了TRAP和TRAPD这两个宏来捕获异常。它们使用区别,仅仅在于使用TRAP之前,需要事先声明保存异常错误码的变量,而TRAPD不需要,可以直接使用。并且在捕获异常之后往往会有一个if的异常结果判断语句。
TRAPD(result,MayLeaveL());
If ( KerrNone != result )
{
//错误处理
}
TInt result;
TRAP( result,MayLeaveL( ) );
If(KerrNone != result)
{
//错误处理
}
2、宏TRAPD的嵌套和同名变量的使用
(1)同名
TRAPD(result , MayLeaveL( ));
If ( KErrNOne == result )
{
TRAPD ( result , MayAlsoLeaveL( ));
}
User::LeaveIfError(result);
这个例子的本意是只要这里的两个异常函数MayLeaveL()和MayAlsoLeave()有一个发生异常,都会导致User::LeaveIfError( )发生。但事实上,由于错误码重名,导致User::LeaveIfError()仅能看到第一个result,而第二个result实际上被第一个result屏蔽了,因此为了看到第二个异常的结果,我们应该使用另一个异常错误代码名字,可以在if语句外面定义一个新的错误码变量,然后使用TRAP捕获第二个异常。
实际上,User::LeaveIfError()永远不能捕获到第二个异常的错误码,因为一旦MayLeaveL()发生,就不会进入if语句,因此,第二个异常函数也就根本运行不到了。
(2)嵌套
为了系统开销上面的考虑,我们尽量不要使用TRAPD嵌套,尽量考虑使用其他的办法来替代。
譬如我们可以将如下代码:
TInt MyNonLeavingFunction( )
{
TRAPD( result , FunctionMayLeaveL( ) );
if(KErrNone == result )
TRAPD(result , AnotherFunctionWhichMayLeaveL( ) );
if(KErrNone == result )
TRAPD(result , PotenialLeaveL( ) );
return result;
}
为了避免嵌套,将可能产生异常的函数集中到一个函数中,将上面代码改为:
MyNonLeavingFunction()
{
TRAPD(result , MyLeavingFunctionL( ) );
return result;
}
void MyLeavingFunctionL()
{
FunctionMayLeaveL();
AnotherFunctionWhichMayLeaveL();
PotentialLeaveL();
}
3、不应该写这样的函数:将错误码作为返回值返回,同时又有可能发生异常退出。
TInt OpenFileObjectL(CFileObject* aFileObject)
{
Rfile file;
TInt error = file.open(…..);
If(KErrNone == error)
{
CleanupClosePushL(file);
aFileObject = CFileObject::NewL(file);
CleanupStack::Pop(&file);
}
return error;
}
上面这个函数就写的不太好,这个函数的返回值是错误码,但是在使用这个函数时还可能产生异常退出,从而传出异常退出码。
我们一般这样使用上面这个函数:
void ClientFunctionL()
{
CFileObject* fileObject = NULL;
TInt errorCode = OpenFileObjectL();
If ( KErrNone != errorCode)
{
User::Leave(errorCode);
}
}
上面的使用仅检测了OpenFileObjectL方法的错误码,而没有捕获异常,不妥。
或者这样:
TInt ClientFunction()
{
CFileObject* fileObject = NULL;
TInt errorCode;
TRAPD(r,errorCode = OpenFileObjectL() );
If(KErrNone!=r)
return r;
if(KErrNone!=errorCode)
return errorCode;
}
这里既分析了错误码,又捕获了异常,但总感觉放在一起判断时,有点不伦不类,也不妥。
那么应该怎么办呢?
有两种办法:
(1) 用异常错误码取代错误码,也就是统一为异常退出。
即将原来的TInt OpenFileObjectL(CFileObject* aFileObject)方法做如下修改:
CFileObject* LeavingExampleL() //CFileObject对象指针通过函数返回值获得,
//而不是之前那样通过引用形参获得。
{
RFile file;
User::LeaveIfError(file.Open(…..));//错误码转化为异常抛出
return CFileObject::NewL(file);
}
然后可以这样使用:
void ClientFunctionL()
{
CFileObject* fileObject = NULL;
TRAPD(r,fileObject = LeavingExample());
switch(r)
{
case(KErrNoMemory)
…….//释放一些内存
break;
……….
default:
User::Leave(err);
break;
}
}
这样进行处理时就比较统一了,仅对异常进行switch进行讨论即可,风格比较统一了。
(2) 用错误码取代异常错误码,也就是统一为错误码。
TInt Example(CFileObject*& aFileObject)//函数返回值为错误码
{
RFile file;
TInt error = file.open(…..);
If(error == KErrNone)
{
TRAP(error,aFileObject = CFileObject::NewL(file));//将异常转换成错误码
}
return error;
}
这样进行使用:
CFileObject* fileObject = NULL;
User::LeaveIfError(Example(fileObject));
这样进行处理时,风格就比较统一了,仅需处理错误代码即可。
相比较而言,将错误代码转化为异常退出进行统一处理的系统开销较小,也更简单,推荐使用这个办法。