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));
这样进行处理时,风格就比较统一了,仅需处理错误代码即可。
相比较而言,将错误代码转化为异常退出进行统一处理的系统开销较小,也更简单 ,推荐使用这个办法。