这是学习Symbain C++很重要的一篇文章,一直没找搜到中文版,想想还是自己译一下吧,复习复习英语,也敦促自己仔细读读此文档。本人英语不是一般的菜,译文中如有不当之处,欢迎批评指正。
原文:S60 Platform: Comparison of ANSI C++ and Symbian C++ (version 2.0) 点此下载
本文译者:svYee http://svyee.iteye.com/ (转载请注明,谢谢合作)
第1章. 简介
此文档是为已熟悉C++开发的程序员而准备的,如移植现有C++ code 到Symbian OS并使之能正常运行。本文描述了ANSI C++ 和 Symbian OS C++的主要区别。
通常,在任何平台使用C++编程仅知道ANSI C++标准是不够的,因为C++标准并没有包含GUI库、线程处理等等。如果你一直在某一特殊的平台下编程,则标准C++与该平台Code的界线就显得模糊,你甚至会怀疑了解ANSI C++标准的用处。但是,如果你是在各种不同的平台下开发,你将发现ANSI C++标准提供了一个区别于各平台的参照物(a common ground)从而提供了把Code 从一个平台移植到另一平台的方法。
意识到你的Code与所谓的参照物之间的差别是很重要的,同样重要的是,你得意识到没有所谓参照物的情况,比如你始用了标准并未提及的API或约定等。掌握这些知识,你就知道当把Code从一个平台移到另一平台时哪些需要重写,另外也就知道你的哪些技术经验需要提升或重新学习了。
本文首先描述了在ANSI C++中有但Symbian C++没有使用的部分并讨论相关的替代技术;之后介绍在Symbian C++中可视为ANSI标准的扩展的部分(译注:即Symbian C++有而ANSI C++没有的部分);最后将简要说明Symbian OS 对ANSI C的支持,尽管只用C通常很难甚至不可能写出一个完整的Symbian OS应用程序,这是因为Symbian OS 本身主要是由C++写的。
当在新平台上学习C++时,有必要了解平台本身的使用及历史背景,例如,Symbian OS的开发在时间上要早于ANSI C++标准的订制,后者是在1998年完成。另外,Symbian是为手持设备而设计的,在这类设备中有效的利用内存和电池就显得尤其重要,这些就导致了Symbian C++ 和 ANSI C++的不一致,当然也有一些差别只是编码上的约定并不是硬限制。想了解更多关于从其他平台移植C++ Code到Symbian OS的信息请看参[1] .
第2章. Symbian OS 与 ANSI C++ 的区别
程序员应该认识到在Symbian OS上写C++与ANSI C++的差别,本章将描述最重要的注意事项。
2.1 可写静态数据
用过其他平台的开发人员可能会熟悉可写静态数据(WSD:Wraieable Static Data)的使用,WSD即在Dll中的全局变量。然而,运行在Symbian OS v8.1a或更早的目标硬件的Dll是不支持WSD的。
S60 3rd版是第1个基于Symbian OS且支持WSD的S60平台,但是,由于其内存消耗上的昂贵代价,WSD仍不被推荐使用,在Symbian OS的模拟器上也仅是有限支持(limited support)。就算是在支持WSD的Symbian OS版本, Symbian希望WSD只是用做最后的手段(更多信息,请看参[2] ,参[3] )。
在不支持WSD的Symbian OS或S60版本中,所有的应用程序都编译为Dll,事实上,这意味着在S60 3rd之前版本上写的应用程序都不能使用全局变量。也就是说,当移植程序Code到S60 3rd之前的版本时,必须移除全局变量(注意:S60 3rd版中不存在此问题,因为此平台上的应用程序编译为可执行文件(executable))。
S60平台 | Symbian OS 平台 | 针对硬件的Dll中的可写静态数据 | 应用程序 |
S60 第1版 | v6.1 | 不支持 | Dll 不支持WSD |
S60 第2版 | v7.0s v8.0a (FP2) v8.1a (FP3) |
不支持 | Dll 不支持WSD |
S60 第3版 | v9.1 | 支持,但不推荐 模拟器提供有限支持,低效 |
EXE 支持WSD |
表1:S60和Symbian OS平台的对应关系及对WSD的支持
在S60 第1版和第2版中,替代WSD的是线程本地存储(TLS:Thread Local Storage),在S60 1st 及 2ed 版使用类Dll 访问,在S60 3rd版中使用类UserSvr 来访问TLS。
TLS是每个线程所特有的一个32位指针,指向一个模拟全局可写静态数据的对象。所有的全局变量必须组装进一个对象中。当创建这个线程时,这些数据的实例将被分配到堆中并在线程的局部存储区里保存了指到该数据的指针(使用Dll::SetTls() 或 UserSvr::DllSetTls() ),当线程销毁时,这些数据也随之销毁。使用Dll::Tls ()或User::DllTls() 来访问这些全局变量。
更多关于WSD和TLS的信息,请看参[2] 和参[3] .
2.2 模板和标准模板库(STL)
C++模板类(例如由STL生成的类)提供了一种在类型安全方式中参数化容器类的方法,然而使用模板将带来复制Code的缺点,因为每当声明不同类型的容器时,就会为复制一份这种类型的容器类Code,这不符合Symbian OS中保持最小代码量的要求。
因此,Symbian希望限制C++模板的使用在“瘦模板”的概念内。此模式使用的是未定义类型(void*)(事实上是等价于void*的TAny*类型)作为参数的基本容器类,容器的Code在此模板化的基类中指定,并通过私继承来访问其具体实现。该模板类为调用者提供了一个类型安全的容器接口,并通过内联方式实现调用。
由此模板类产生的代码量是可以忽略不计的,因为它是内联的,也因为它是参数化的。同样该容器是类型安全的。此概念避免了code的复制,是如此的“苗条”。在Symbian 开发者库中可以找到关于瘦模板概念的好例子,详见参[4] 。
正因为只使用瘦模板的约定和效率方面的考虑,在Symbian OS中没有实现STL。但既然不是硬限制,Symbian OS中的STL就是可实现的,也确实存在第三方的实现,尽管不鼓励使用。同样类似于STL的泛函类可优先考虑,因为它们是针对该平台最优化的。Symbian OS 容器类的描述详见参[5] ,以及在各SDK中的Symbian库。
2.3 C++异常
由于历史和效率的原因,在Symbian OS v9版之前的平台并不支持ANSI C++的try, catch(), throw异常处理机制,当编译Symbian C++时,编译器将把所有使用C++关键字try, throw, catch()的标为Error。
取而代之的是Symbian OS采用一轻量级的异常处理机制,在v9版之前的Symbian平台,开发者除了用此替代ANSI标准异常处理外别无选择。然后,尽管在v9版中支持标准C++异常处理,但这应该仅仅用于从其他平台移植过来的Code,不可与Symbian OS的异常处理机制相混淆。
Symbian C++提供了它自己的一套异常处理机制:异常捕获处理(trap handlers)(TRAPs)和leave。leave 用来抛出异常,该异常会将按调用栈向外抛出,直到被TRAP捕获并处理。Code的有效执行就在leave点中止,直到在TRAP中才被唤醒,leave会设置栈指针到TRAP的上下文中并跳转到相应置调用栈对象的析构函数。leave没有终止程序流,而是相当于是一条longjmp()指令。这里提到了一个重点:可以置于栈里的对象类型,更多请看3.1节“命名约定”和3.4节“清除栈”。
尽管与C++的throw有些类似,但在Symbian OS中的概念还是有些不同的。一个函数可以说成“会leave”,但这并不是说它就会造成异常,抛出异常或leave。Symbian中的leave要比异常简单得多,它不会抛出一个对象,只会返回一个32位的错误码给宏TRAP或TRAPD,宏TRAP和TRAPD是就像C++中的try-catch()一样的异常捕获模块。
造成leave可能的原因有:
由于Symbian OS异常处理不是C++语法的一部分,因此编译器无法检验一个可能会leave的函数是否在某个异常捕获模块中使用。这一问题的解决的方案是采用命名约定:一个可能leave的函数的函数名必须使用“L”后缀。非除leave被捕获,否则调用有可能leave的函数的函数,也要使用后缀“L”,虽然它不会直接leave。
void TestFunctionL(TProgrammer& aWorker) { //译注:该函数不会直接Leave, //但调用了可能会Leave的函数aWorker.WorkL //因此函数名中也用采用后缀“L” TTime timeNow; timeNow.HomeTime(); aWorker.WorkL(timeNow); } void TProgrammer::WorkL(TTime& aTime) { // Calculates if it is OK to do work now // or calls User::Leave(KErrNotSupported) if // the time is invalid (e.g., weekend, late at night) }
这个命令约定告诉开发者,当调用一个以“L”为后缀的函数时,必须注意在堆上分配内存而leave的潜在影响。这将更多的在3.4节“清除栈”中讨论。正是因为了解一个函数是否会Leave的极为重要,希望开发者能使用Symbian提供的工具LeaveScan,它能分析源代码并标明哪些函数会leave但没有按命名约定来命名。更多关于leave, 宏TRAP的使用以及Symbian OS的错误处理资料可以在Symbian出版社系列出版物中找到,例如参[6] 。
2.3.1 异常,Leave,严重错误(panic)的关系
除了leave-TRAP异常处理,Symbian中还有一个特殊的机制来处理致命错误,这被称为严重错误(panic)。panic不能被捕获,而是直接终止产生致命错误的线程,如果此线程是进程中主线程,则该进程也被终止。panic可以通过调用User::Panic() 来产生。
3. Symbian OS C++的扩展
本章描述Symbian OS C++的独有特性,这些并非蜕变自ANSI C++标准,可视为与标准无关的Symbian独有的约定。其中的大部分只是惯例,因此违反这些惯例并不会导致编译器报错或链接报错,但可能导致运行时出错和低效程序。
3.1 命名约定
Symbian OS 程序员使用一系列的命名规范,它们表示各种不同的含意,例如类的功能,对象所有权,清空(cleanup)等。这些约定的目的是避免编码错误,尊从这些约定是明智的,特别是当你的Code要给其他开发者使用的时候。
对于Symbian OS 和 S60编码规范的完整描述,请参阅参[7]、参[8]。但是理解这些命令约定及含意对理解后面章节相当重要,因此将其列示如下:
类名以C,T,R,M为前缀,其含意如下:
只有静态函数的类的类名不用加特殊的前缀,例如:类User,Math等。
3.1.1 Symbian OS 的基本类型
Symbian OS基本类型符号应当取代C++中的类型符号的使用,解释如下表:
描 述 |
Symbian OS 类型 |
32位 布尔型 |
TBool(ETrue或EFalse) |
8位 无/有符号整型 |
TUint8 / TInt8 |
16位 无/有符号整型 |
TUint16 / TInt16 |
32位 无符号整型 |
TUint32, TUint |
32位 有符号整型 |
TInt32, TInt |
64位 无/有符号整型 |
TUint64 / TInt64 |
双精度浮点型(IEEE754 64位表示法) 尽管浮点数中一般推荐此类型,但是只要有可能就应该使用整型来实现你的运算。 |
TReal64,TReal |
指向未定类型的指针 |
TAny* |
表2:Symbian OS 基本类型
3.2 字符串处理和Symbian OS描述符
在Symbian OS中,字符串处理采用描述符来实现,这和传统的以null结尾的C++字串符稍有不同。之所以称之为描述符,正因为它们是对自身的描述;描述符对象包含数据,数据长度以及最大空间分配长度。也就是说,描述符类可以避免数据的越界。另外,由于不再使用以null结尾的字串符,描述符除了可以存在文字数据,也可以存放二进制数据,因此描述符为处理两种数据提供了统一的接口,这是Symbian OS中最小化程序体积重用Code的一个好实例。
描述符操作是使用两个基类:TDesC(不可修改)和TDes(可修改)所提供的函数。默认情况下,描述符使用Unicode格式,即每个字符16位,当然,也可通过明确指定窄类型描述符来使用8位的描述符。
本文并没有提供对描述符详细全面的介绍,更多的信息你参考参[5] 和参[6]
3.3 继承规则
Symbian OS对继承有自已的约定,呈列如下:
3.3.1 C类
C类必须派生自基类CBase,该基类中提供了虚析构函数和重载的new操作符。虚析构函数保证了当CBase派生类销毁时完成析构;重载的new操作符有4个变种,最常用的是如下形式:
CMyClass = new(ELeave) CMyClass();
正如2.3节“C++异常”中所言,如果没有足够的内存空间可以分配,这将导致产生一个Symbian OS leave
重载的new操作符,会使用二进制0填充新分配的对象,因此,当C类对象实例化时没有必要再把指针变量初始化为NULL。
3.3.2 多重继承
多重继承是C++强大的一面,但通常要防止造成设计或代码晦涩难懂,在Symbian OS中约定限制多重继承中使用混合类,这将避免复杂的继承模式以及ANSI C++中随之引起的不确定问题。
具体而言,多重继承的限制如下:
3.4 清除栈
Symbian OS的leave有造成内存或资源泄漏的潜在可能,考虑以下案例:
void LeavingFunctionL() { CMyClass* p = new(ELeave) CMyClass(); p->MethodThatMightLeaveL(); delete p; }
处此,指向对象的指针p位于栈中,有可能因为在MethodThatMightLeaveL()中产生Leave而没有执行到delete就被删除。因为p是指向该对象的唯一的指针,没有其他的办法来清空这个堆中的对象,这就导致这个对象游离于堆中造成内存泄漏。
Symbian OS提供一个称为清除栈的机制来防止这类资源泄漏,清除栈能使分配的内存“安全leave” 。清除栈用来存储指向已分配资源的指针,当发生leave,由清除栈来负责遍历它所存储的指针,并清除相关联的对象。
以上的例子可以就通过使用清除栈来达到安全leave,具体如下:
void SafeLeavingFunctionL() { CMyClass* p = new(ELeave) CMyClass(); CleanupStack::PushL(p); p->MethodThatMightLeaveL(); CleanupStack::Pop(p); delete p; }
本例中最后2行的Code,可以使用如下更为简单的一行Code来替代:
CleanupStack::PopAndDestroy(p);
清除栈可以用来存储:
另外还有一系列的辅助函数来完成最常用的清除操作,例如:CleanupClosePushL(T& aRef),在执行清除时根据所提供的指针调用Close()接口。
需要注意的是清除栈的目的是防止发生leave时造成内存泄漏,并非一般和自动的垃圾收集器。更多关于清除栈的功能及使用方法请查阅参[9] 或Symbian出版社系列的读物。
3.5 两阶段构造
类CMyClass定义如下:
class CMyClass : public CBase { public: CMyClass(); ~CMyClass(); ... // Omitted for clarity private: CSomeOtherClass* iPtr; }; CMyClass::CMyClass() { iPtr = new(ELeave) CSomeOtherClass(); }
想像一下如下Code在堆上创建一个对象会发生什么?
CMyClass* p = new(ELeave) CMyClass();
首先,调用new操作符在堆上分配一块CMyClass对象足够大的内存,如果没有足够的内存,就会产生一个leave,如果分配内存成功,就会调用CMyClass的构造函数。
但是CMyClass的构造函数在堆上分配内存(即为CSomeOtherClass对象)时由于可用空间不足而失败将会发生什么事呢?如果构造函数leave,那为CMyClass分配的内存就会游离,清除栈对此也束手无策,因为CMyClass才构造一半。唯一的解决办法是禁止在构造函数中写入会Leave的code,而且C类应该采用两阶段构造来初始化。
正如其名,两阶段构造被分为两步:
需要两阶段构造的类会实现一些静态的辅助函数,通常命名为NewL()和NewLC(),它们将调用第2阶段构造函数,因此没有必要特别注意第2阶段构造函数的调用。两个阶段的构造函数都将标记为protect或private,因此只有通过静态辅助接口来完成对象的实例化。
所以,上文所提到的类,用两阶段构造来实现如下:
class CMyClass : public CBase { public: static CMyClass* NewL(); static CMyClass* NewLC(); ~CMyClass(); private: CMyClass(); void ConstructL(); ... // Omitted for clarity private: CSomeOtherClass* iPtr; }; CMyClass::CMyClass() {} void CMyClass::ConstructL() { iPtr = new(ELeave) CSomeOtherClass(); } CMyClass* CMyClass::NewLC() { CMyClass* self = new(ELeave) CMyClass(); CleanupStack::PushL(self); self->ConstructL(); return self; } CMyClass* CMyClass::NewL() { CMyClass* self = NewLC(); CleanupStack::Pop(self); return self; }
正如你所看到的,当新创建一个对象, 并接着调用一些可能发生异常的方法的时候, NewLC()函数是很有用的。只用NewL()将导致把同一对象压入清除栈又弹出这类无用的操作。NewLC()也说明了一个新的命名约定:函数以C为后缀,这表示该函数在清除栈中留下了什么,而调用者就要负责当其不再需要时让其弹出栈。
3.6 Symbian OS中的客户端/服务器模式
Symbian OS的架构基于微内核模型,这种模型中内核要尽可能的小,因而操作系统所提供的大多数服务都被实现为服务器。这些服务器是系统的必要组成部分,并在运行于他们自己的进程中,这就使用用户程序(客户端)和客服器之间的进程通信。
通常,服务器会提供客户端类来封装进程间的通信,例如文件服务器类RFs和Rfile。因此进程间通信的具体实现在客户端是不可见的。
3.6.1 异步调用
多数客户端/服务器调用都采用异步方式,这样客户端的调用在请求没完成之前还可以继续运行。完成请求时,通过线程请求信号量向客户端发送通知。此信息量并不是被直接处理的,而是由服务器使用RequestComplete()函数(来自类RThread或User)来修改信号量。服务器通过改写客户端所提供的TRequestStatus对象的一个32位值来指示请求的成功或失败,该客户端使用User类的WaitForRequest()或WaitForAnyRequest()函数来实现等待。
这里提供一个客户端使用异步调用的简单例子,其实本例中是一个同步等待。(注意,异步调用很容易识别,因为它们总带一个TRequestStatus&的参数):
TRequestStatus status; server.AsyncRequest(status); User::WaitForRequest(status); // Blocks until AsyncRequest completes if( status != KErrNone ) { // handle error } else { // handle event }
当然,在这个准同步的实例中,很多东西都没有,例如,因为该线程会于WaitForRequest调用而阻塞,无法响应来自用户界面的事件。有两种解决方案:使用线程,或实现一个循环等待。因为使用线程将导致额外开销(如:每个线程都要有其自己的栈和对应的内核端对象),所以在 Symbian OS 中并不推荐使用多线程。因而在Symbian OS中使用循环等待来处理事件成为所写的异步代码的首选方案,事实上,程序员通常用不着自己来实现等待循环,因为Symbian OS 提供了已封装的好的类:活动调度器和活动对象。
3.6.2 活动调度器和活动对象
CactiveScheduler 就是Symbian OS封装了等待循环的类。Symbian OS应用程序框架提供了一个活动调度器,因此程序员通常使用的唯一函数就是CActiveScheduler::Add(),它用来增加一个活动对象到调度器。
活动对象派生自CActive,并包含特殊请求的code,它们含有提出请求给异步服务提供者(asynchronous service provider)的代码,和活动调度器在完成该请求时所调用的一个函数。
以下例子展示了如何实现一个基本的活动对象类(CMyActive),异步服务提供者是类CProvider,异步请求为:
CProvider::Request(TRequestStatus& aStatus).
class CMyActive : public CActive { private: CProvider* iProvider; .. };
CMyActive::CMyActive() : CActive(CActive::EPriorityStandard) { CActiveScheduler::Add(this); }
void CMyActive::RunL() { // check the iStatus for possible errors and // resubmit the request if necessary }
void CMyActive::DoCancel() { iProvider->CancelAll(); }
这是最简单的形式。如果提供者提供多于一个的异步调用,就需要一个状态机制来检查哪个异步调用在RunL()中完成。注意,如果你想同步地多个异步调用,你必须为每个异步调用实现一个活动对象,因为一个活动对象在一个时间点只能有一个活动请求。