与标准C++相比,Symbian操作系统中的C++语言有些不同的地方。这些不同,有些是为了便于在移动设备这种特殊的软硬件平台上进行开发所做的改造,其中异常处理是比较典型的。今天读了Symbian开发者库中异常处理部分的文档,觉得值得记录一下,便索性将此部分翻译为中文如下。
错误的类型和恢复
错误情况主要可以分为三类:
- 程序错误,例如试图访问数组或缓存边界外的元素
- 环境错误,例如内存不够,磁盘空间不够,或缺少其它资源
- 用户错误,例如试图在对话框中输入错误数据,在一个字处理程序中无效的操作,或源文件中不正确的语法
程序错误通过asserts(__ASSERT_DEBUG macro)检查,并被紧急报告。恢复这样的错误,要重写包含错误的部分程序。
环境和用户错误通常有两种方法处理:
- 如果错误能在操作执行之前发现,那么返回除KErrNone外的一个值,是一种报告错误的便利方法。
这种方法易于编程,并且如果需要做清除(cleanup)工作,通常容易识别和处理。 - 另外,程序可以使用这部分讨论的异常处理和清除技术。
当错误发生在对所需操作进行处理的深层时,这种方法更加适合:如果使用返回值方法,则每一个函数都要返回这样的错误,并且在每一次函数调用中都要做所需的清除工作。逻辑变得重复,而且很容易把它看作一种异常处理方案。
对于可能的环境和用户错误情况,编程时要考虑这两种处理方法,选择最适合的。
当异常发生时,应用程序必须做正确的清除工作,因为应用程序设计为长时间运行(几个月甚至几年),其间没有中断也没有重启系统。
C++异常处理
Symbian操作系统提供了自己的异常处理机制。没有使用C++的异常处理(try,catch和throw),因为:
- 在Symbian操作系统设计时,编译器就不支持,或者不适合支持C++异常处理
- Symbian操作系统的异常处理被特殊设计,与其它Symbian操作系统规范(C和T类,32位整型错误码)一起使用,这比C++的异常支持需要更少的系统开销
其它平台上使用了异常处理的C++程序,在这里使用之前,必须要修改。
注意,清除和两阶段构造方法是Symbian操作系统异常支持的一部分,即使使用C++异常处理,这也仍然需要。
基本异常支持
操作系统对异常的基本支持是
- TRAP宏和由它衍生的TRAPD,使代码运行在trap保护(trap harness)的环境中
- User::Leave()调用,它结束当前函数,返回到trap保护之处,并指定一个错误码。
这些类似于C++的异常处理支持(try/catch和throw)。
按照约定,所有能直接或间接leave的函数,函数名字后面都要附加一个L(因此称为L函数)。由trap保护调用的函数都是L函数。
怎样使用User::Leave()
当意外情况发生时,你能使用User::Leave()函数立即结束一个函数的执行。下面的例子中,如果new失败,函数调用带有一个错误码的User::Leave()。函数的返回类型是void,因为没有必要返回错误信息。
void doExampleL()
{
CExample* myExample = new CExample;
if (!myExample) User::Leave(KErrNoMemory);
// leave used in place of return to indicate an error
// do something
myExample->iInt = 5;
testConsole.Printf(_LIT("Value of iInt is %d./n"),myExample->iInt);
// delete
delete myExample;
}
怎样使用TRAP
在trap保护中执行能leave的函数
能leave的函数,包括调用了其它能leave的函数,必须在trap保护中执行。
如果函数中的User::Leave()被执行了,那么控制立即返回到最近的TRAP。每一个trap都使用一个变量接收在leave中指定的错误码。
如果没有发生leave,则当函数结束时,执行返回到TRAP,并且leave变量的值为KErrNone。
通常在一个TRAP之后,函数要检查leave变量,看看是正常返回还是由leave返回的,并采取适当的处理。后面会讨论,在异常之后采用特殊机制来处理清除。
TInt E32Main()
{
testConsole.Title(); // write out title
testConsole.Start(_LIT("Example")); // start a new "test"
// The leave variable
TInt r;
// Perform example function. If it leaves,
// the leave code is put in r
TRAP(r,doExampleL());
// Test the leave variable
if (r)
testConsole.Printf(_LIT("Failed: leave code=%d"), r);
testConsole.End(); // finish
testConsole.Close(); // close it
return KErrNone; // and return
}
注意
- 没有必要所有L函数都直接由trap保护调用。大多数情况下,能leave的函数通常由其它函数调用。只需要在调用链上层的某个地方放置trap保护。
使用TRAPD
方便起见,有一个TRAPD形式的宏,其中定义了要使用的作为leave码的变量。多数时候这会节省一行源码。
TRAPD(leaveCode,SomeFunctionL()); // call a function
if (leaveCode!=KErrNone) // check for error leave code
{
// some cleanup
}
trap保护和函数返回值
当调用有返回结果的函数时,trap保护也可以使用。
TRAPD(leaveCode,value=GetSomethingL()); // get a value
if (leaveCode!=KErrNone) // check for error leave code
{
// some cleanup
}
else { // didn’t leave: value valid
}
怎样使用失败时自动leave的new
new失败是很普遍的,于是便写了操作符new()版本,带单一参数ELeave,表示如果不能分配内存就必须leave。
使用操作符new(),就不必再检查new的结果。
void doExampleL()
{
// attempt to allocate, leave if could not
CExample* myExample = new (ELeave) CExample;
// new (ELeave) replaces new followed by check
// do something
myExample->iInt = 5;
testConsole.Printf(_LIT("Value of iInt is %d./n"),myExamplezai/ delete
delete myExample;
}
在哪里放置trap保护?
trap保护可以嵌套。如果函数leave了,则控制转到调用栈中最近的trap保护。这使得独立的子模块可以做自己的异常处理。有效的trap需要程序能够正确识别恢复单元。
最基本的方式是依靠顶层的trap保护,它是应用程序框架的一部分,为所有GUI程序提供的。如果发生了leave,又没有显示的代码保护进行处理,框架就会显示一个对应leave码的错误消息。
一些应用程序的恢复单元可能要处理用户命令。在特殊的应用程序中,恢复单元可以与特殊的命令处理部分相关联——一种很细致(finer-grain)的方法。
编写粗糙(coarse-grain)恢复单元的好处是,只有一个trap保护和恢复代码,但坏处是,恢复代码可能是通用的,复杂的,而且对于用户会有小错误导致严重结果的危险(例如,没有足够的内存应用粗体格式会导致字处理程序的结束,造成数据丢失)。
编写太细致的恢复单元会导致太多的trap保护,对每一种情况都要单独处理的恢复代码,以及潜在的代码量的大量增加。
正确的选择是根据具体的应用程序进行处理。对于大型的应用程序,可能需要有一个粗糙的恢复单元,同时在特殊的地方还要有其它的保护