1,Symbian命名法
1) 基本类型
TIntX 和 TUintX (其中X = 8, 16 和 32) 分别用来表示 8位, 16位 和 32位的有符号和无符号的整数。 一般情况下,使用TInt 和TUint就可以了,除非是在考虑代码优化或兼容性的时候,才会用到TInt8,TInt16这样的类型。TInt 或 TUint 类型分别对应有符号和无符号的整数。
Ø TInt64. 在版本8.0之前,Symbian系统中不支持64位的算术运算,而是用两个32位的值来实现64位的整数,在8.0版本之后,TInt64和TUInt64才被定义为long long类型,真正使用64位的内置数据类型。
Ø TReal32 和 TReal64 (TReal相当于TReal64)
这两个数据类型相当于单精度和双精度的浮点数,由于浮点数的运算要比整数慢,所以一般应尽量避免使用浮点数的运算。
Ø TTextX (其中X = 8 或 16)
分别对应窄或宽的字符(注:所谓窄字符通常ASCII码字符,而宽字符是指unicode字符集的字符 )
Ø TAny*
TAny* 意为指向任意内容的指针,在这种意义上讲,TAny相当于void, TAny* 相当于TAny*。但是,在某些场合下,void标示‘空’,如: void hello(void); 这时,不要将它改写为: TAny hello(TAny);
Ø TBool
标示布尔类型。两个常量:ETrue (=1) 和 EFalse (=0),分别表示真和假。TBool被定义为int, 而ETrue和EFalse被定义为enum。
2) 函数命名
在Symbian的函数(包括成员和非成员函数)中,常有两种后缀,一个是L,另一个是LC。
L,就是告诉你,这个函数可能Leave,这个函数会抛出异常,需要谨慎处理。L是有传递性的,如果在调用该函数的地方对此L不理不睬放任其Leave,那么,在此调用函数后面,也需要添加一个L。
LC, 这通常都是构造性的函数,它告诉你,它构造的过程中,不但可能Leave,并且分配的对象处于清理栈中。如果在本函数中的后续部分需要调用被构造对象的相关接口,应该用LC,然后自己pop,而不是L。
3)类命名
对象的成员变量,都带着前缀i;函数参数,都带着a(如果后面是原因字母开头,则需要用an,*_*)。在成员变量加前缀,这是常用的手段,可以和成员变量区分开了,帮助节约命名一个变量的脑细胞。但对函数形参加前缀,就是一件很诡异的事情了,剖有画蛇添足的艺术气息。在Symbian中,所有的常量,都应该是K开头的,包括定义的const量,_LIT定义的字符常量等等。而枚举类型,同属于T类型,以T开头,其中的枚举值,则是以E开头。给这些类型的东西建立命名法,是常见的手段,只是Symbian不走寻常路,命名方式上不屑于与别人苟同。。。
class Person
{
public:
TInt iAge; //成员变量:i开头
void SetAge(TInt aAge){iAge = aAge};//方法参数:a开头
}
类名以大写字母开头(T,C,R 或M)。
T 类
”T”代表”Type”,类似于C++中的内置类型,没有析构方法(destructor)。(1)T类不能包含具有析构方法的成员变量。一般情况下,成员变量只能是内置类型的数据或者是其它的T类的对象。在某些的情况下,也可以包含其它对象的指针或引用,不过,这时它们之前是“使用”关系,而不是“拥有”关系(也就是说,并不负责对成员的创建和销毁的工作)。正是由于没有析构方法,T类的对象可以在栈上创建,当程序流程退出函数或产生leave(一种代码异常)的时候,系统自动清除它。(2)即使T类有一个析构方法,在发生异常(在Symbian系统中,异常被称为leave)时Symbian 系统也不会调用它,因为leave没有模仿标准C++的抛出异常的做法。(3)T类的对象也可以在堆上创建。但是,应当在可能发生异常的代码之前,将这个对象放入到清除栈(cleanupStack),在发生异常的时候,清除栈(cleanupStack)会释放这个对象。
C 类
这种类都是从CBase派生来的(直接或间接)。(1)有一个虚的析构方法, 可以通过CBase指针来删除它的子类。(2)C类的对象一定要在堆上创建。CBase类和它的子类,重载了new操作符(成员变量都被初始化为0)。
class CStudent:public CBase
{
public:
CStudent(){
RDebug::Print(_L("i am a student"));
};
~CStudent() //有一个虚的析构方法
{
RDebug::Print(_L("please, don't kill me!"));
}
};
(3)CBase 类声明了私有的拷贝构造方法和赋值操作(=)。这是一个很好的策略,它可以用来防止客户代码不小心地使用了浅拷贝或赋值的方法。如果你想提供该对象的拷贝功能(要深拷贝,不要浅...),那么您可以为类添加一个的可能会发生异常的方法来完成同样的任务,例如:CloneL()或CopyL()。
做过.Net或者Java的人应该都明白,保持一个单一根的类型系统有什么好处,.Net在没有泛型的日子里,就是通过这个共根来实现一些基础的容器和方法。但这个好处,在C++,尤其是Symbian C++中体现的并不明显。因为C++有void *(在Symbian中华丽的转身为TAny *),有模板,可以来做一些类似的事情。
(4) 更重要的,在Symbian C++中,为了节约空间,把虚表的RTTI项给精简掉了,使得Symbian C++的类丧失了dynamic_cast的能力,从而导致整个Symbian在运行期的动态识别能力,很是孱弱。
R 类
R代表资源(Resource), 通常是外部资源,例如:文件的句柄(handle)。
(1)一个R类应当有一个构造方法来将它的资源句柄置为0,表明还没有资源和这个新建的对象关联在一起。但是,不要在构造方法中初始化资源句柄,因为这样有可能使构造方法产生异常。
R类中常常有类如Open(), Create() 或 Initialize()这样的方法,它们用来分配资源,设置句柄成员变量的值,并返回错误代码或是产生异常。 R类通常也有对应的Close()或Reset()类,用来释放资源,重置句柄的值------表明没有资源和该对象关联。使用R类时,一个常见的错误是忘记调用它的Close()方法(当然,该方法也可以是其它名字,但它经常被命名为Close())或是有一个析构方法释放资源,这会引起资源的泄露。
(2)R类通常都很小,除了资源句柄没有其它的成员变量。因为不需要。它通常也没有析构方法,资源的释放都是在Close()方法中进行的。大多数情况下,R类都是作为类的成员变量或局部变量存在的。只有少数情况下,在堆上创建。
(3)您必须确保,当程序发后异常的时候,资源能被正确地释放------通常是使用资源栈。如果一个R类是一个堆上的自动变量(相对于成员变量),您一但要保证资源被释放,而且,变量本身也要被释放。
R类的成员变量通常都很简单,所以一般不需要深拷贝(bitwise copy)。R类的拷贝可能会引起混乱(如果两个对象同时在一个资源句柄上调用Close()方法,或两个对象都没有释放资源,会发生什么情况?)如果,您想阻止任何对R类的拷贝,您应当声明(但不定义)一个私有的构造方法和赋值操作。
M 类
在Symbian系统中,M 类常被用来定义回调接口或者是观察者(observer)类。M类也可以被其它类继承。
class MAnimal
{
public:
virtual void EatL() =0;
};
class MDomesticAnimal : public MAnimal
{
public:
virtual void NameL() =0;
};
class CCat : public CBase, public MDomesticAnimal
{
public:
virtual void EatL(){}; // 从MAnimal, 经过MDomesticAnimal继承
virtual void NameL(){}; // 从 MDomesticAnimal继承
// Other functions omitted for clarity
};
象接口一样,由于不能被实例化,M类只能有虚(virtual)函数,不能有成员变量和构造方法。
但它可以有析构方法
CCat *cat1 = new CCat;
delete cat1; //正确,CCat从CBase派生的,含有虚的析构方法
然下面的代码却是错误的。
MAnimal *cat2 = new CCat;
delete cat1; //错误,出现系统异常(panic code 42)
将MAnimal的代码改写,则上面代码没有问题。
class MAnimal
{
public:
virtual void EatL() =0;
virtual ~MAnimal(); //增加一个虚的析构方法。
};
每个Symbian中的类,可以派生自若干个M类,但仅仅能从一个有内存资源的对象那里进行分配。理论上,作为一个纯虚类,应该提供一个虚的析构函数,但在Symbian C++中,这往往是不需要的。因为在一个没有RTTI的世界里,只有第一个被继承的接口才有可能成功析构所有对象。比如一个类,形如 class A : public Cxxx, public Mxxx。只有用Cxxx接口才能管理资源,对Mxxx接口进行delete,完全没有办法释放全部资源(除非Cxxx里面没有任何数据...)。而Symbian的堆对象往往派生自CBase,所以,不可能从一个M类来析构对象,这个析构函数成不成虚,就是无关紧要的事情了。
另外一个不含任何资源的类,就是静态类了,在Symbian C++中它们没有任何前缀,是唯一不戴帽子的家伙。这个和.Net的static class一样,只包含一堆的静态方法,需要屏蔽所有构造、析构、拷贝接口(要没有这个闲工夫,不屏蔽也无所谓了...)。虽然,C++有函数,但出于对面向对象的热衷,使用这样的静态类,还是很值得鼓励的。。。
2.对象构造
二阶段构造
为了解决对象分配的问题,Symbian琢磨了所谓的二阶段构造法,它是一个pattern,关键在于将对象中栈数据的初始化和堆对象的分配过程隔离开来。一个标准的二阶段构造类如下:
class A
{
public:
~A();
static A * NewL();
static A * NewLC();
private:
A();
void ConstructL();
}
其中内容,自动构造的每个Symbian C++类中都会有。在构造函数中,只能够执行赋值等操作,就是初始化栈中内容,整个操作不会涉及到堆中对象的分配。所有需要分配的堆中对象,推迟到ConstructL函数中进行。NewL和NewLC提供一个封装,将构造函数和二阶段构造函数封装一起。当然,仅通过这样的方式,无法解决内存泄漏的问题,一个核心机制,是清理栈,即CleanupStack。
清理栈
CleanupStack是单件的形式呈现在程序中,GUI的程序系统为你构造好了,Console的需要人肉一个。当你在一个函数中,new了一个对象,你需要先把它push到CleanupStack中,才能调用其带L的方法,并在调用完成后将它pop出CleanupStack。一旦L函数执行失败,Leave了,并在上层用TRAP宏抓到这个Leave错误,系统会自动释放存放在CleanupStack中,还没来得及pop的对象,以保证所有资源都不会泄漏。要做到这点,有两个需要解决的问题,一是如何不在人肉delete的情形下自动析构,第二个是如何知道析构栈中多少个对象。
解决第一个问题,关键就是利用栈对象的析构函数,每个push到CleanupStack中的对象,都被一个栈对象TCleanupItem封装了一下,作为一个成员变量TAny* iPtr存放起来。当这个栈对象被释放,会调用其析构函数,析构函数中包含delete iPtr的调用,如此,自动析构得以完成。当然,为了保持其通用性,TCleanupItem其实不是直接delete,而是通过一个TCleanupOperation的对象来实现的,这个对象负责在其析构函数中delete iPtr,当然,除了delete,不同的TCleanupOperation还可以是iPtr->close,iPtr->release之类的,这样可以将其机制轻松的扩展开来。
#define TRAP(_r, _s) \
{ \
TInt& __rref = _r; \
__rref = 0; \
{ TRAP_INSTRUMENTATION_START; } \
try { \
__WIN32SEHTRAP \
TTrapHandler* ____t = User::MarkCleanupStack(); \
_s; \
User::UnMarkCleanupStack(____t); \
{ TRAP_INSTRUMENTATION_NOLEAVE; } \
__WIN32SEHUNTRAP \
} \
catch (XLeaveException& l) \
{ \
__rref = l.GetReason(); \
{ TRAP_INSTRUMENTATION_LEAVE(__rref); } \
} \
catch (...) \
{ \
User::Invariant(); \
} \
{ TRAP_INSTRUMENTATION_END; } \
}
另一个问题解决之道,就是记录一个level,在函数执行前放入一个标记,一旦有错误,就消除在此标记后push进来的对象。这个机制的维系,隐藏在TRAP宏中。当你写TRAP(err, DoitL())时,TRAP会在调用DoitL()前,调用User::MarkCleanupStack()加入一个标记,并在调用结束后利用User::UnMarkCleanupStack检查并且消除该标记。放一个标记在这里,一旦你多push了少pop了,或者少push了多Pop了,都会触发异常,谨防顺手写错。而在执行函数DoitL()过程中,一旦发生Leave错误,在User::Leave()之类的函数中,都会找到最后标记的位置,清除标记后push的所有对象。
由于栈和函数调用都属于先进先出的,整个机制是可以嵌套进行的。只要你TRAP了Leave错误,所有资源都会被保证析构(如果没有TRAP,天皇老子都帮不了你...)。这种半自动半人肉的内存管理方式,虽然不能帮助复杂的内存对象生命周期的维护,但至少可以保证每一个资源在异常时正常释放,这一点在嵌入式系统中比一般系统显得更为重要(因为内存紧张,分配不成功是常态...)。但人肉方式总归是要人来解决的,不论CleanupStack多么的好,它只是一个pattern,它不能自动去做一些事情,还是需要开发人员主动的push,pop,leave,以及TRAD,少了哪一样,整个机制全部白搭。
Symbian的异常处理
Symbian的异常处理,就是著名的Leave机制,如果你打开TRAP宏,便惊奇的发现,所谓Leave,只不过老瓶装新酒,它只是给C++的异常机制,穿了个丁字裤,还是超节约布料型的。你可以将所谓的TRAD看成是catch,将Leave看成throw,将带L的函数,看成是throw exception的函数,再将err code当作是异常类型,整个Leave机制,就和C++的异常匹配上了。
当然,之所以称为老瓶装新酒,那么就有一些可以称为新的琐碎事。首先,就是对CleanupStack的维系。在TRAP宏和User::Leave中,包含了对CleanupStack的标记的管理和资源清理,没有它们,CleanupStack这套东东,就该另辟蹊径了。
而另一方面,就是对标准异常和无法估量的异常进行了分门别类的处理。C++和.Net不一样,异常都是不同根的,我们往往需要用catch(...)去处理一些杂类的状况。在TRAD中,对所以Symbian中的异常进行了分类。一类是派生自XLeaveException的异常,它们是整个Symbian的Cleanup以及Leave的管辖范围,只有在触发此类异常的时候,所谓的自动释放内存、Leave才能发挥作用;而其他所有的异常,都被归类异类,一旦发生,直接User::Invariant()来安乐死。所以,你明明是TRAP了,在读到空指针等错误发生的时候,它完全不起作用,程序直接崩溃,因为,这超出了它的能力范畴。
除此之外,Symbian开始支持标准的C++异常了,但对于一个合格的Symbian开发者而言,了解这些,还是有益无害的。。。
3. Symbian的描述符
所有的描述符都是从抽象类TDesC中派生的,他们可以分为三个大类:
1、缓冲区描述符——数据做为描述符对象的组成部分而存在,描述符对象存放在程序的堆栈中:TBuf和TBufC,
2、堆描述符——数据做为描述符对象的组成部分而存在,描述符对象存放在堆中:HBufC,
3、指针描述符——描述符对象和它所表示的实际数据是分开存放的:TPtr和TPtrC.
字符串常量可以使用_L()或_LIT()宏来定义。
_L()可以生成一个指向字符值的地址(TPtrC),它经常被用来传递字符串到函数中:
NEikonEnvironment::MessageBox(_L("Error: init file not found!"));
_LIT()可以生成个常量名,以便以后重复使用:
_LIT(KMyFile, "c:\System\Apps\MyApp\MyFile.jpg");
_LIT()宏的结果(就是上面的KMyFile)实际上是个文字描述符(literal descriptor)TLitC,它可以在任何使用TDesC&的地方使用。
如果对照C/C++语法来看:
1、TPtrC可以被看作是const char*的使用
2、TBufC可以被看作是char[]的使用
TDes和TDesC是抽象类,因此你不可能实例化它们。它们的主要用途是做为函数的参数来描述字符串和二进制数据。在这样的函数中,你应该按如下规则使用:
1、const TDesC& 表示只读的数据和字符串。
2、TDes& 表示可以被修改的数据和字符串。
所有这些描述符都可以指定数据尺度:TDes8、TDes16、TDesC8、TDesC16、TBuf8、TBuf16等
这里8表示描述符处理的数据是8bit的,而16表示是16bit数据。一般来说,你只要使用通用形式(TDes, TDesC,...)来表示文本数据而使用8bit版本(TDesC8等)来表示二进制的内容。
一定程度上等同于字符串。只不过与C++的字符串不一样,Symbian中的描述符都是用一个附加的整数描述其长度,而不是以'\0'做终结符。因此,描述符可以表达任意数据,字符串或者二进制串。
1)描述符体系
基类是TDesC,顾名思义,T是代表它是T类,后缀C表示它是一个常量,其中数据无法修改。因此,它只是定义了一些字符处理的方法,包括查找、匹配、取子串等,而不包括任何修改其中数据的接口。
可修改的描述符类,都是派生自TDes,它是TDesC的子类,额外提供了拷贝、清零、追加之类的接口。
当你需要在栈上分配一个描述符,你可能需要用到TBuf或者TBufC类。它们都是模板类,接受一个int型参数作为长度信息。从名字可以一目了然的看出其中区别,带C的自然是常量,它一次性在栈中分配好所需的资源,并且同时完成赋值和初始化工作,一经分配,则不能再次修改。而不带C的TBuf,在构造时仅是在栈中预留好所需空间,此后可以在此空间范围内,任意的修改所需内容。
从内存分布来看,TBufC对象在真实的字串信息前,还放了一个32位整数,它的前4bits存放类型信息,后28bits存放长度信息,也就是说一个TBufC对象最多包含256M长度的字符串,这已经绝对足够了。而TBuf对象,除了TBuf所包含的内容外,还额外加入了一个max-length的整数在长度信息后,它表示预分配了的内存长度,而length则用于表示真实有效的数据长度。
除了栈,更多的描述符长度不是在编译期能够确定的,需要在堆上动态的进行分配,这项任务,就交由了HBufC来完成。HBufC也包含三项数据,前两项与TBuf一致,4bits类型 + 28bits真实长度 + 1整形的分配长度。但最后一项是一个指针,它指向堆中的某个位置,在这个位置,开辟了预分配长度的字符串空间。
但HBufC的基类不和TBuf一致,而是于TBufC相同,这和它C的后缀表里如一,代表它只具有一些非数据修改性质的接口。这样的设计,一定会引发一系列的疑问,为什么明明又有max-length信息,又具有length信息,却是一个不可变的描述符对象?如果需要动态改变堆中描述符的内容,该使用什么样的类?
所有这些疑问,都可以通过TPtr这个类来解答。
单纯的从内存数据来看,TPtr与HBufC完全一致,但从实际逻辑来看,HBufC中的指针,仅仅可能指向堆中的数据,而TPtr中的指针可以指向一个堆数据,也可以指向一个栈数据,这完全取决于你用什么对象来初始化它。如果用一个TBuf来初始化,那么就指向栈中,用HBufC来初始化,就指向了堆中,整个一墙头草。但不论是指向堆还是指向栈,TPtr对所指向的数据都仅拥有使用权,而不具有控制其生死的权利,该数据需要通过其原始的控制者,TBuf或者HBufC等来负责管理。很多时候,TPtr都是作为HBufC的一个帮手而存在,当你需要修改HBufC中的字符数据时,调用Des()接口,从HBufC华丽的转身为TPtr,TPtr没有C的后缀,这意味着它秉承了TDes的能力,可以修改其中的数据。我一直不理解Symbian为什么要设计一个HBufC类,而不是HBuf类,唯一可以想到的解释就是,由于TPtr的存在,可以解决修改堆描述符数据这件事情,而不需要再多实现一些接口,虽然有点牵强,但我还是一直用这解释自欺欺人。
TPtr还有一个孪生兄弟TPtrC。和TBufC与TBuf的关系类似,TPtrC去掉了max-length这个域,分配长度即使用长度。TPtrC所有的设计逻辑,都与TPtr一致,指向堆或栈对象,只使用不管理资源,等等。它应用的最广泛的场合,就是用于表达子串。比如TBuf对象希望取出其中前10个字符给调用者使用,它就会返回一个TPtrC对象,它指向HBufC的字符位置,但仅具有10个长度,既可以控制长度,又可以保证其中数据不被修改,一举两得一石二鸟。
到此为止,描述符的整个构架算是完整了,既有栈的,又有堆的;既有可修改的,有包含不可变的;既有表达整体的,又有表征局部的。但Symbian本着买一送一,挥泪大馈赠的态度,还提供了一个RBuf类。这是一个R类,并且没有C后缀。它通过Create接口在堆上分配数据,用Release或Close析构所掌握资源,从本质上来看,它就是HBufC的一个R版。但RBuf的基类是TDes,因此直接提供了更为丰富的数据修改接口,不需要转身成TPtr来处理。并且,RBuf屏蔽了字符串为NULL和为空的区别,有的时候,在使用HBufC需要不停的判定是否为NULL或者为空,而用RBuf则不需要,NULL即空,空即NULL。但RBuf的继承体系更深,并且可以想象,它的一些操作会再次封装一些额外的检测操作,可能效率上会有一丁点的降低(只是猜测,有兴趣可以做实验证实...)。从RBuf和HBufC的区别,你也可以从中推断出两者最适合的使用场景。HBufC其实最合适的就是应该本着其C的本质来做,适合于分配了不再修改的场合,比如从一个已有的描述符拷贝出新的描述符,此时返回的往往就是HBufC。而RBuf更适合反复修改的场合(不然白瞎了叫它一声Buf...),在这样的场景下,其接口使用起来更为的简单和明了。。。
2)编码
前面提到的所有描述符,其实都不是真实的类,而是一个typedef。在非内核模式的时候,所有的描述符,如TDesC,其真实的实现是TDesC16,在内核模式的时候,则是TDesC8。还是看名取义,带8的是单字符1个字节的描述符,带16的是宽字符2字节的描述符。在非内核态的时候,统一使用16位的描述符作为默认值,是为了兼容unicode编码,帮助在不同语言下进行开发。大部分的系统API,提供的都是接受TDesC这样typedef的接口,其实也就是unicode-16的16位描述符。但在一些io相关的接口,都是接受8结尾的单字符描述符,以兼容不同的数据格式。单字符描述符通常不会对编码有任何约束,可以是二进制流,可以是utf-8,可以是一般的ascii码。具体是什么,逻辑需要调用者自己来维护。为了将io读入的数据传递给一些系统API,往往就需要将8位描述符转成16位描述符。这种转换和编码有密切联系,如果只是一般的ascii串(或者其他编码的ascii部分...),可以使用TDes的Copy接口,从8位拷贝到16位,或者从16位拷贝到8位。从8位拷贝的16位,第一个字节填充对应的8位字符的内容,第二个字节填充的是'\0',就是全部为0。而从16位到8位,可想而知,后一个字节的内容被截取抛弃。但如果是一些复杂的编码转换,比如utf-8的字符流转成系统所需的unicode-16,那么就需要用到CnvUtfConverter,它负责在不同的字符集中做转换。
3)其他字符类型类
看到Symbian的描述符,最疑惑的不仅仅是那套复杂的继承体系,还有_L和_LIT这样的宏。打开_LIT这个宏,你可以看到,它其实就是定义一个TLitC类型的const static常量字符串。从意义上来说,通过_LIT,可以将一些常用的字符串作为常量存在,使其不会反复构造和析构,是空间换时间的策略;从接口上看,它重载了很多转型运算符operator (),可以转身成为TDesC的各个版本,与该继承体系兼容;而从内存实现上来看,它存放的是:C++字符串的长度(除\0) + 原汁原味的C++字符串(就是\0结尾的一坨short int或者char数组...),通过这样的结构,一方面可以和Symbian的描述符表示相一致,又可以享用C++的原始支持,一举两得。
相比_LIT的华丽,_L这个宏就土鳖不少,代表了旧生产力的落后。它其实,就是定义一个TPtrC,TPtrC中的内容指向了一个常量的数组(typedef后叫做TText...),这个常量数组,其实就是有一个char或short int的数组转型而成,也是原汁原味的C++字符串。从本质上来看,TPtrC扮演这个角色,完全是在人手短缺时的友情客串,它本不应该来做这件事情,因为其内部是通过一个指针来指向真实的字符空间,很多操作都经过多一次的取址操作,降低了效率;而TLexC,则是量身打造精心包装天生大明星,它优化掉了那个作梗的指针,提高了效率。所以摒弃_L,拥抱_LIT,是所有Symbian教学都会呼吁的内容,还是合情合理的。。。
还有一个常用的和字符串相关联的东西,就是TLex类。它做的工作,就是大名鼎鼎的string-parsing。给它一个描述符,它可以还你一个整形数抑或是浮点数。TLex对数的解析,本质上还是基于ascii编码的,你给它的描述符编码,需要兼容ascii标准,不要拿个全角的数字为难它,它会罢工的。。。
一.不可修改的描述符(基类TdesC)
1) 通过length()方法获取描述符长度。
实际上描述符的长度都是由4个字节即32位来保存的,但实际上,其中的4位留做他用,只有28位用来保存描述符的长度,这样的话一个描述符的最大长度就是228字节,即256MB。而这4位用来表示描述符的类型,目前共有5个派生描述符类型,足够用。
2) 对于所有的描述符,要访问其中数据,通过基类TdesC的非虚方法Ptr(),获取指向数据的指针。
还有:length()、size()、ptr()、find()、match()、compare()、left()、locate()等
二.可修改的描述符(基类TDes,它继承自TDesC)
1)TDes类有一个额外的成员变量,用来保存描述符现有内存情况下所能存储数据的最大允许长度。而TDes类的MaxLength()方法返回的就是这个值。
2)TDes类定义了修改字符串数据的一系列方法:添加、填充、格式化描述府数据等
所有上述方法都不会分配内存,如果想使用上述方法来扩展描述符中数据的长度,就像Append(),在调用方法之前必须保证有足够的内存可用,否则失败。
注意:TdesC和TDes都是抽象类,不能用它们来进行实例化。我们使用描述符时,使用的都是它们的派生类实例。即基于两种布局的描述符:指针描述符(这种描述符中保存了指向字符串存储位置的指针)和缓冲描述符(字符串是其对象的一部分)。
三.指针描述符:
指针描述符的字符串数据和描述符对象本身是分离的,可以储存在ROM中、堆上或栈上。而保存着数据的内存并不属于描述符,也不通过描述符来管理。
因此,如果数据保存在堆上,那么内存的创建、必要时的重新分配还有销毁都要使用堆描述符指针(HBuf),如果指针描述符引用了基于栈的字符串,那么相应的内存就被分配到栈上了。指针描述符本身往往是被分配到栈上的,但它们也可以被分配到堆上,例如,它们可以作为一个CBase派生类的成员变量。
1、不可修改的指针描述符(TPtrc),描述符对象本身大小是2个字。
TPtrc的数据可以被访问但却无法被修改:即描述符中数据是恒定的。所有定义于Tdesc基类的不可修改的操作对于TPtrc的对象都是可以访问的。
该类还定义了一系列构造函数,从而允许TPtrc能够从其他描述符、指向内存的指针或者零终结符的C语言字符串创建。
指针本身可以改变以指向不同的字符串数据。使用Set()方法可以改变指针的指向。
2、可修改的指针描述符(TPtr),描述符本身大小是3个字。
TPtr是为访问、修改字符串或二进制数据而设计的可修改的指针描述符类。
该类定义了若干构造函数,能以包含内存地址的指针为参数,并设置适当长度和最大长度来构造对象。
另外该类还提供了一个赋值操作符operator=(),该操作符可以将数据复制到对象指针所引用的内存中去(从其他可修改的指针描述符、不可修改的指针描述符或零终结符的字符串复制数据)。如果复制的数据长度超出了描述符的最大长度,那么将产生一个系统错误。
另外和TPtrc类似,也提供了一个Set()方法,用于指向其他的数据。
注意:不要混淆Set()和operator=()。Set()将描述符重置为指向新的数据区域(相应的修改了长度和最大长度这两个成员变量),而operator=()仅仅将数据复制到已经存在的描述符,并可能修改描述符长度,而不会修改其最大长度。
四.基于栈的缓冲描述符
同样分为可修改的和不可修改的,字符串数据是描述符对象的一部分。这些字符串对于定长的字符串和相对较小的字符串(最多256个字符)都很有用。这些描述符都是基于栈的,当描述符与其创建者有着一致的生命周期时,应当使用这种描述符。
1、 TbufC<n>
用来保存不会改变的字符串或二进制数据,派生自TbufBase类,n代表分配的适当数据区域大小。(相等或稍大)
定义了若干构造函数,从而使得不可修改的缓冲区能够从任何其他描述符的附件或者零终结符的字符串构造。它们也可以创建为空的,待以后填充。尽管数据是不可以修改的,但缓冲区的内容却可以使用类中定义的赋值操作符全部替换掉。替换的数据可以是另一个不可修改的描述符或零终结符的字符串。
另外,该类也定义了一个Des()方法,它会为缓冲区所表示的数据返回一个可修改的指针描述符。因此,除了赋值操作符完全替换外,还可以通过这个可修改的指针描述符来修改。
2、 Tbuf<n>
这里n代表缓冲区最大允许长度。
五.基于堆的缓冲描述符
对于不放在ROM中和由于太大而无法保存在栈内的字符串数据,可以使用基于堆的描述符。
当描述符可能比其创建者具有更长的生命周期时可以采用基于堆的描述符。
HbufC:
注意:该类导出了一组静态NewL()函数,用来在堆上创建缓冲区。这些函数遵从两阶段构造模型,因为在空闲内存不足时可能发生异常退出。该类没有公共构造函数,所有的堆缓冲区必须采用其中某个NewL()方法来构造(或者通过TdesC类的Alloc()或AllocL()方法构造,通过该类你可以做出任何描述符的HbufC附件)
缓冲区数据虽不允许修改,但可通过赋值操作符,整个进行替换。
还可以通过Des()方法操纵基于堆的操作符创建一个可修改的指针描述符TPtr,这与TbufC是一样的。
六.字面描述符(被编译放入ROM的常量描述符)
通过一组宏来创建,这些宏定义在e32def.h头文件中。
字符宽度无关的宏_L,_S和_LIT的隐式和显式定义都是对应相同的。例如,_L在Unicode构建版本中等价于_L16,而在ASC码中等价于_L8。
1、_LIT
最高效、最被提倡使用的symbian os字面常量。
注意:TlitC16和Tlit8不是派生自TdesC8或Tdesc16,但他们和TbufC8和TbufC16具有相同的内存布局,所以这些类型的对象可以应用到任何使用TdesC的地方。
对一个_LIT常量使用sizeof(),返回的将是对应的TlitC对象的尺寸,即描述符内容的大小,单位是字节,再加上8个额外的字节(一个保存长度的Tuint和NULL终结符),如果你想使用栈的缓冲区,就必须把他们计算在内。
2、_L
好处是不必为它想一个名字。使用_L会创建临时TPtrC类型的临时开销。
4. Symbian的容器
Symbian在设计之初,没有拥抱STL,这就要求,它需要重新制作一些轮子,容器便是其中的一个。
CArray系列容器
Symbian的设计者,非常喜欢复杂的继承结构和保罗万象的类,CArray系列的容器,就是在这种理念下的产物。CArray是顺序容器,相当于STL的vector + list,以及更多。
CArray系列容器,在继承的最底端,也就是可实例化使用的类,都采用CArrayXxxYxx的命名方式,即:CArray + 对象单元存储方式 + 对象段存储方式。所谓对象单元存储方式,就是表征容器中每一个单元数据,是如何存放的,在CArray中,主要有四种:
Flat,容器中的每个数据,都是等长同类的;
Var,容器中的每个数据长度,可以是不同的;
Pak,容器中的数据分成若干部分,每一部分都有一个leading-byte表示这一段的长度,形如描述符;
Ptr,容器存放CBase子类对象的指针数据。
每个容器,都有一个重要的参数,它是一个整数,称为Granularity,即,每一组元素的个数。组是CArray容器分配内存的单位,在Granularity范畴内,元素都按照上述四种模式进行存储,但是,Granularity总是一个有限的数,当容器中元素填满Granularity大小,就需要新增空间来存储。每次新增空间,都是Granularity个单元,每一组单元之间,有两种连接模式,一种是Fix,一种是Seg。
在Symbian OS Explained中有一幅经典图片,各种Array的存储模式,一目了然,盗窃过来,如上所示。从存储上来看,Flat方式就有如数组,一个挨一个存在一起,Ptr看上去和Var一致,指针一个挨一个存在一起,指向堆中对象,但从本质上来看,Ptr的实现与Flat的底层类似,而Var则是转为指针定制。Pak的存储有些怪异,每一坨元素有一个leading-byte的个数参数,让人不由怀疑,这玩意整个就是为描述符处心积虑准备的。
而段与段之间,Fix的犹如vector,每次扩大存储容量,都需要进行内存的重新分配,适合用在定长的场景,而不是变长。而Seg,类似于List,段之间链表方式连接,如果Granularity的一个Seg对象,就彻底沦为了链表,毫无疑问,如果你需要不是的增加存储容量,Seg方式Array应该是你的最爱。。。
在接口层面,CArray支持AppendL,InsertL,Delete等数据写入的接口,也支持At,operator [] 之类的数据读取接口,还支持Sort,Find,Compress等数据查找和处理的接口。具体实现和底层的存储相关联,具体细节没看过相关资料,只能无条件信任它的实现没有如此废柴。。。
RArray系列容器
RArray家族有两个成员,RArray和RPointerArray,从Symbian的命名风格很容易想到,前者存放普通栈对象,后者存放指针。RArray系列的实现,与CArray相比,清晰纯洁多了,它们其实就是基于X *或X **的C数组的封装,接近于STL的vector模式,只不过,和STL相比,它的模板行为还是有点伪而已。
所有的Symbian书上,都会说当你需要接近vector这样的数组的时候,用RArray而不要使用CArrayFixFlat。原因是有很多的,本质上,CArray是被Symbian大而全思想脱了后腿,它的继承结构很深,每一层都要做一些额外的Check工作,降低了效率,而RArray基本就是为这个量身打造的,没有太多额外的开销,在Int,UInt等基本数据类型上,还不辞辛劳的做了模板特化,进一步提高了效率。因此,术业有专攻,性能强与CArrayFixFlat,也不是什么稀奇事情了。
其他容器
在Symbian中,还有一些容器,比如CDesCArray系列的Array,它们和CArray系列容器本是同根生,只不过换了个皮脸,针对描述符提供了额外的一些接口,可以更好的进行字符串比较等操作。而Symbian中,我并没有发现一些非线性的容器,比如map,比如hash,比如tree。如果有,那么就是我土鳖了,如果没有,其实也是我土鳖了,因为我实在琢磨不透Symbian怎么做这么个决定的。
Thin Templates
强行插播一些广告,关于Symbian的模板。众所周知,C++模板有两个特别优秀的特征:
高效。编译期决定类型,保证运行期的运行效率。
安全。编译期决定类型,保证运行期的安全。
但为了达到这样的效果,也是付出了成本的,那就是类型的膨胀,与运行期技术相比,程序体积增加了不少。作为一个手机操作系统,Symbian在模板的使用上,加了个pattern,企图鱼和熊掌兼得。具体来说,就是不利用模板高效的优点,仅利用模板安全的特征,用运行期的技术,减少程序体积,这层模板的皮,它们称之为Thin Templates。
从实现上来看,标准的一个模板容器的实现,可能就是:
template <class T>
class Array<T>
{
public:
T GetData(int index);
private:
T * data;
};
这样的话,如果有十个不同类型使用CArray,那么就会编译出十个不同的类,使得体积膨胀。于是Symbian的Thin Templates模式这么来做:
template <class T>
class Array<T> : private BaseArray
{
public:
T GetData(int index);
};
class CBaseArray
{
protected:
void * data;
};
表面上,还是一个模板类,但实际上,存储上并没有按照类型,而是统一转成了void *。用private继承,派生基类的存储,而摒弃基类的接口,重新构造基于模板参数的接口。这样一来,安全性保证了,很多错误在编译期就可以暴露出来,而由于底层实现没有用模板,节约了一部分体积开销,这种实现,类似于早期的Java模板,只是牺牲了性能优势,对于底层来说也许真的利大于弊,但对于自己的应用来说,就不一定了。因此,如果是自己做模板,考量一下应用场景,不需要拘泥所谓的Tiny Templates。。。
5. Symbian的异步框架
永远活在同步的流程里,无疑是我等码工最大的奢望之一。为了不阻塞UI,为了读写一陀陀数据,为了含辛茹苦的演算复杂的逻辑,为了大家和睦相处共同劳动,总是需要异步处理,你一下我一下共同完成任务。在Symbian中,做了一套机制来做这件事情,这就是Active Objects。
Active Objects
Active Object是一套事件驱动的多任务模型。在Symbian的标准线程中(除掉一些Java构造线程、原生C构造线程,等),都包含有一个消息循环,在循环中,会不停的查询注册在该线程所属的CActiveScheduler各个ActiveObject的TRequestStatus状态,一旦发现可执行的任务,立马激活并执行。经典的循环伪码如下:
// *** in the loop ***
User::WaitForAnyRequest();
FOREVER
{
if(activeObject->IsActive() && activeObject->iStatus != KRequestPending)
{
activeObject->iActive = EFalse;
TRAPD(r, activeObject->RunL());
if( r != KErrNone )
{
r = activeObject->RunError();
if( r != KErrNone )
Error(r);
}
break;
}
}
一码解千语。在消息循环中,会等待事件激活,接着,会遍历查询注册各个Active Object的状态。当然,简单的遍历是不够体面的,每个Active Object都是带着优先级来的,毫无疑问,优先级高的会被优先考虑执行,低的永远也超越不了。默认,大家都使用EPriorityStandard,一切太平。如果,一个Active Object是需要长时间被执行的,可以考虑使用低优先级,EPriorityIdle、EPriorityLow,这样执行的频率会低一些。而如果任务是具有一些实时要求的,就需要使用高的优先级,亦如,EPriorityUserInput、EPriorityHigh。
但这个模型,是不足够满足实时要求的,因为它是非抢占式的。一个低优先级的任务,被调度了,在那里磨叽磨叽,你优先级再高也奈何不了。在这种模式下,要保持良好的响应能力,需要自律。首先,如果一个任务的实时性高,那么就需要调高它执行线程或进程的优先级,让它优先被CPU调度,不给低优先级任务执行的机会。还有,就是不要在Active Object的RunL中放置执行效率低下的代码(如果是非主线程,那就看菜下饭了...),它会使得该线程失去响应其他请求的能力。
每个异步的任务,都需要派生自CActive类。如果用系统向导,可以发现,Symbian希望每个CActive的子类,做以下几件事情:
实现RunL,放置等待回调一方的执行代码;
实现DoCancel方法,把未有机会到达目的地的任务妥善安置,这是一个模板方法模式,它会被放在Cancel,仅在状态为执行时,会被调用;
实现RunError,在执行出错后,给该CActive子类一次自我救赎的机会;
尝试告诉使用者,这世界没有免费的午餐,需要用SetActive将请求发送给那个执行者,让它帮你搞定问题。
任务驱动的控制核心在于TRequestStatus,执行一方在接到任务后,会将TRequestStatus设置成为KRequestPending,当这个状态被再次改变了,才会触发CActiveScheduler调度执行回调。整个流程如下图所示(依然是盗窃来的...):
Client-Server框架
Client-Server框架,是Symbian的重要机制。在Symbian的内核层面,大量使用该模式,将文件管理,界面管理等功能都剥离到了各个服务中,呈微内核态势。所谓Client-Server框架,就是功能调用者Client,和功能执行者Server,各据一方,处于不同的线程或进程。它们搭建在Active Object上,通常通过异步模式进行调用。不过,Client-Server并没有对执行模式进行约束,比如文件服务,就具有同步和异步两种调用模式,当然,本质上换汤不换药,所谓同步只是阻塞异步来实现的。
Client-Server分同进程和跨进程两种,不用说,通信模式完全不一样。在跨进程模式下,需要通信,就需要在Client和Server之间建立一个Session。Session是对Symbian IPC的封装,用协定方式进行通信,只不过,大数据的传输还需要各行其道各显神通。而同进程下,传递信息就简单多了,很多时候,同用一个指针就好。在跨进程模式下,适合集中控制,统一管理资源,特适合想文件服务这样的东东。而同进程,则适合大数据的传输,和逻辑的运算,各开各的线程,各跑各的逻辑。
结语
和同步相比,异步无疑是残酷的,但有了这些框架,比吭哧吭哧动手搞线程,搞通信来的开心多了。Symbian有这点能有的东西,还真不容易*_*。
补充:描述符详述
1) 描述符(descriptor) - 字符串
在描述符中,保存了它所表示的字符串的长度和底层的内存布局的信息,
在Symbian系统第5版以后,默认的情况下,它们表示宽度为16bit的字符串(编译时的宽度设定)。一般情况下,没有必要指明是字符串的宽度(比如,TPtr8(它表示是的8bit的窄字符),TPtr16(操作16bit的宽字符)),用中立(neutral)的类型(比如TPtr)就可以了,这样使你的代码易于在宽字符版本和窄字符版本之间转换(有过编程经验的朋友应该有这样的印象,我们平常写代码,大多情况下,仅仅使用UINT类型,而较少考虑使用UINT16,UINT32类型)。
2)描述符类型
1)不可修改(non-modifiable)的描述符
基类:TDesC(后缀C代表Constant,表示这个类是不可更改的)。存放长度的4个字节中,28bit用来表示长度,剩下的4bit用来表示描述符的类型。目前,symbian系统中有5种派生的描述符类型,4bit限制了描述符的种类最多只能有16种,但这已经足够了。
(1)Length()方法返回了描述符的长度,因为,每个描述符对象在内存中的布局都是同样的,Length()方法没有被它的子类重写,它对所有子类都有效。但是,根据实现子类的方法的不同,子类访问数据的方式也不一样,Symbian系统不要求它的子类通过虚函数的方式来实现自己的访问数据的方法。不用虚函数重写的原因是,虚函数会给每个被派生的描述符对象增加4节字的额外负担,c++用这4个字节来存放指向虚函数表的指针。
(2)Ptr()方法来访问描述符的数据,Ptr()方法检查这4个bit,确定描述符的类型并返回它的数据在内存中的地址。当然,这要求TDesC基类清楚它的子类的内存布局,并在Ptr()方法中使用硬编码的方法。后面,为了表述上的方便,我们也把这种不可修改的描述符也称为常量描述符(constant descriptor)
TDesC中最常用的函数如下:
1、Ptr(),用来获得描述符数据中的指针。
2、Length(),用来获得描述符数据中的字符数。
3、Size(),用来获得描述符数据中的字节数目。
4、Cpmpare()或操作符==、!=、>=和<=等专为比较描述符数据用的。
5、操作符[],可以被当作c/c++中一样,用来获得描述符字符串中的单个字符。
下面几个函数有其特殊性:
1、Append()和Num()有很多重载形式,具体可以看SDK
2、Compare()有2个变体:CompareC()和CompareF(),以及Copy(),Find(),Locate()和Match(),这些函数都有C/F的后缀形式,C代表Collated而F代表Folded.
Collating和Folding
------------------------
Folding是个比较格式化文本的简单方法,主要用在对比较不是太要求精确的场合。
Collation是个更好的也更有效的比较字符串的方法,可以生成类似字典的顺序。
总结:不可修改的描述符类TDesC是所有的字面量描述符的基类,它提供了确定描述符长度和访问数据的方法,另外,它实现了所有的您想用来处理常量字符串的操作。
2)可修改(modifiable)的描述符
从TDes基类派生,而TDes本身又是从TDesC派生的。
(1)TDes有一个额外的成员变量,用来存放为该描述符分配数据的最大长度。MaxLength()方法返回了这个最大的长度。像TDesC中的Length()方法一样,MaxLength()方法也不被TDes的子类继承。
(2)TDes类提供了一系列的方法, 用来对可修改字符串数据的操作,包括对字符串的附加、填充和格式化操作。所有的这些方法都被派生类继承,派生类只实现一些特定的构造方法和复制赋值的方法。这些方法都不负责分配内存,假如它们超过了描述符的数据长度,例如,用Append()方法在某个字符串后面附加另一个字符串时,在调用该方法之前,您必须确保有足够的内存空间。当然,只要不超过描述符的最大存储容量,描述符的长度可以自由地伸缩。当描述符长度比最大长度短的时候,描述符的后面部分是多余未用的。这些方法使用了断言(assertion)来确保描述符的最大长度不会被超出。如果发生内存溢出,将会产生一个panic(关于panic,我们将在后面的章节介绍),这样可以方便您检查和修正程序的错误。事实上,不可能使描述符溢出,这一点保证了您代码的强壮性,而且不易产生难以跟踪的内存陷阱。需要注意的是,由于基类的构造方法是proteced类型的,所以您无法直接实例化一个TDesC或TDes类的实例。现在我们来看看描述符的派生类,您可以实例化和使用派生类的对象。
TDes类定义了修改字符串数据的一系列方法:添加、填充、格式化描述府数据等
所有上述方法都不会分配内存,如果想使用上述方法来扩展描述符中数据的长度,就像Append(),在调用方法之前必须保证有足够的内存可用,否则失败。
注意:TdesC和TDes都是抽象类,不能用它们来进行实例化。我们使用描述符时,使用的都是它们的派生类实例。即基于两种布局的描述符:指针描述符(这种描述符中保存了指向字符串存储位置的指针)和缓冲描述符(字符串是其对象的一部分)。
总结:TDes 是所有的可修改的描述符的基类, 并且它自己也是从TDesC派生的。它有一个能返回最大的内存容量的方法和一系列的用来修改字符串数据的方法。
3)内存布局
描述符有两种基本的内存布局:指针描述符和缓存区描述符。
不同之处在于,指针描述符持有一个指向字符串的指针,而这个字符串存储在内存中的基它位置。与指针描述符不同,缓存区描述符本身持有字符数据,也就是说字符数据本身构成了描述符的一部分。
1) 指针描述符(pointer descriptor)
指针描述符可分为两种:TPtrC 和TPtr(我们前面说过,每种类型的描述符,按照字符宽度,都可以分为三个版本,例如:窄字符版本TPtrC8,宽字窄版本TPtrC16和中立的版本TPtrC,所以严格来讲,有六种指针描述符)。
指针描述符所持有的字符串是跟描述符本身分开来存放的,它可以被存储在ROM中,堆中或栈中。由于保存数据的内存既不为描述符所拥有,也不通过它来管理。所以,如果要该描述符是在堆上分配的,那么应通过堆描述符(HBufC,下面将要讲解)来操作内存的分配和销毁;如果指针描述符所指向的字符串是在栈上分配的,那这个内存必须是已经在栈上分配好的。通常情况下,指针描述符是基于栈的,但有时候,它们也可以在堆上使用,例如:作为一个CBase派生类的成员变量的时候。在不可修改的描述符(TPtrC)中,指向数据的指针存放在长度的后面,因此,指针描述符的总长度为2个字(word);在可修改的指针描述符中,它存放在最大长度的后面,因此,总长度为3个字。下图比较了TPtr和TPtrC内存布局.
Ø TPtrC
TPtrC相当于C语言中的const char*。被它指向的数据可以被访问但不能被修改:也就是说,描述符中的数据是常量。所有的从基类TDesC中继承的操作都是可访问的。TPtrC定义了一系列的构造方法,使得它能从其它的描述符、指向内存的指针或以0结尾的C语言字符串构造。
_LIT(KLiteralDes, "Sixty zippers were quickly picked from the woven jute bag");
TPtrC pangramPtr(KLiteralDes); // 从字面量描述符构造
TPtrC copyPtr(pangramPtr); // 从其它的描述符构造
TBufC<100> constBuffer(KLiteralDes); // 常量缓存区描述符
TPtrC ptr(constBuffer); // Constructed from a TBufC
// TText8 is a single (8-bit) character, equivalent to unsigned char
const TText8* cString = (TText8*)"Waltz, bad nymph, for quick jigs
vex";
// 从以0结尾的字符串构造
TPtrC8 anotherPtr(cString);
TUint8* memoryLocation; // Pointer into memory initialized elsewhere
TInt length; // Length of memory to be represented
...
TPtrC8 memPtr(memoryLocation,length); // 从一个指针构造。
这个指针本身可以改变成指向其他的字符串数据(通过Set()方法)。如果您想指明,不能改变您的TPtrC所指向的数据,那么您可以将TPtrC声明为const,这样,当您试图用Set()方法更改TPtrC所指向的数据时,编译器会产生警告。
// 字面量描述符
_LIT(KLiteralDes1, "Sixty zippers were quickly picked from the woven jute
bag");
_LIT(KLiteralDes2, "Waltz, bad nymph, for quick jigs vex");
TPtrC alpha(KLiteralDes1);
TPtrC beta(KLiteralDes2);
alpha.Set(KLiteralDes2); // alpha points to the data in KLiteralDes2
beta.Set(KLiteralDes1); // beta points to the data in KLiteralDes1
const TPtrC gamma(beta); // Points to the data in beta, KLiteralDes1
gamma.Set(alpha); // Generates a warning, but points to alpha
Ø TPtr
TPtr 是可修改的指针描述符,它可用来访问和修改字符串或二进制数据。TDesC 和TDes所提供的所有的操作都适用于TPtr。这个类定义了一些构造方法,使得它能从指向内存的指针构造,并设置适当的长度值和最大长度值。
编译器也会产生隐式的构造方法和拷贝构造方法,因为它们没有被声明为保护的或私有的。一个TPtr对象可以从其它的可修改描述符构造,例如:通过在不可修改的描述符上调用Des()方法,这个方法返回一个如下所示的TPtr对象:
_LIT(KLiteralDes1, "Jackdaws love my big sphinx of quartz");
TBufC<60> buf(KLiteralDes1); // TBufC are described later
TPtr ptr(buf.Des()); // Copy construction; can modify the data in buf
TInt length = ptr.Length(); // Length = 37
TInt maxLength = ptr.MaxLength(); // Maximum length = 60, as for buf
TUint8* memoryLocation; // Valid pointer into memory
...
TInt len = 12; // Length of data to be represented
TInt maxLen = 32; // Maximum length to be represented
// Construct a pointer descriptor from a pointer into memory
TPtr8 memPtr(memoryLocation, maxLen); // length = 0, max length = 32
TPtr8 memPtr2(memoryLocation, len, maxLen); // length = 12, max = 32
另外,TPtr提供了赋值运算符=(),用来拷贝数据到指针所指向的内存(数据源可以是可修改、不可修改的指针描述符,或以0结尾的字符串)。如果要拷贝的数据的长度超过了描述符的最大长度,会引发一个系统异常。像TPtrC一样,TPtr也定义了一个Set()方法,用来改变描述符所指向的数据。
_LIT(KLiteralDes1, "Jackdaws love my big sphinx of quartz");
TBufC<60> buf(KLiteralDes1); // TBufC are described later
TPtr ptr(buf.Des()); // Points to the contents of buf
TUint16* memoryLocation; // Valid pointer into memory
...
TInt maxLen = 40; // Maximum length to be represented
TPtr memPtr(memoryLocation, maxLen); // length = 12, max length = 40
// Copy and replace
memPtr = ptr; // memPtr data is KLiteralDes1 (37 bytes), maxLength = 40
_LIT(KLiteralDes2, "The quick brown fox jumps over the lazy dog");
TBufC<100> buf2(KLiteralDes2); // TBufC are described later
TPtr ptr2(buf2.Des()); // Points to the data in buf
// Replace what ptr points to
ptr.Set(ptr2); // ptr points to contents of buf2, max length = 100
memPtr = ptr2; // Attempt to update memPtr which panics because the
// contents of ptr2 (43 bytes) exceeds max length of memPtr (40 bytes)
您一定不要混淆了Set()方法和=()赋值操作。前者将描述符的指针重置,使它指向新的数据区域,而后者将数据拷贝到描述符中,一般来说,这会更改描述符的长度,但不会更改它的最大长度值。
2) 基于栈(stack-based)的缓冲区描述符
基于缓冲区的描述符也可以分为可修改的TBuf和不可修改TBufC的两种类型。对这种描述符来讲,字符串数据本身就是描述符的一部分。下图给出了描述符的内存布局:
这两种描述符通常用来存储定长的或相对较小的字符串,常用来存放长度小于256个字符的文件名。类似于C语言中的char[],但是,它们具有检查内存溢出的功能。
Ø TBufC<n>
TBufC<n>是不可修改的缓冲区类型,它主要用来存放字符串常量或是二进制数据。该类从TBufCBase类派生,尖括号<>内的数字表示分配给该描述符的数据区的大小。它定义了一些构造方法,允许从其它的描述符或以0结尾的字符串构造。也允许创建一个空的描述符,然后再填充。
由于该描述符的数据是不可修改的,它的整个内容可以被置换(通过该类的所定义的赋值操作),用来置换的数据可以是其它的不可修改的描述符或是0结尾的字符串,但是,无论是何种情况,新数据的长度都不能超过长度n(也就是创建该类的时候指定的模板参数)。
_LIT(KPalindrome, "Satan, oscillate my metallic sonatas");
TBufC<50> buf1(KPalindrome); // Constructed from literal descriptor
TBufC<50> buf2(buf1); // Constructed from buf1
// Constructed from a NULL-terminated C string
TBufC<30> buf3((TText*)"Never odd or even");
TBufC<50> buf4; // Constructed empty, length = 0
// Copy and replace
buf4 = buf1; // buf4 contains data copied from buf1, length modified
buf1 = buf3; // buf1 contains data copied from buf3, length modified
buf3 = buf2; // Panic! Max length of buf3 is insufficient for buf2 data
该描述符中的数据可以被整体置换,但不能被直接修改,但有时候我们的确需要修改缓存区中的数据,该怎么办呢?系统提供了另一种途径来修改数据。该类定义了Des()方法,它为缓存区中的数据返回一个可修改的指针描述符(TPtr)。我们可以通过这个指针描述符间接地修改缓冲区中的数据。当数据通过指针描述符被修改以后,指针描述符和缓冲区描述符中的iLength的值会跟着改变,但要记住,缓存区描述符的长度值只可能减小,而是不可能增大的,因为,描述符类是不提供内存管理管理功能的。
_LIT8(KPalindrome, "Satan, oscillate my metallic sonatas");
TBufC8<40> buf(KPalindrome); // Constructed from literal descriptor
TPtr8 ptr(buf.Des()); // data is the string in buf, max length = 40
// Illustrates the use of ptr to copy and replace contents of buf
ptr = (TText8*)"Do Geese see God?";
ASSERT(ptr.Length()==buf.Length());
_LIT8(KPalindrome2, "Are we not drawn onward, we few, drawn onward to
new era?");
ptr = KPalindrome2; // Panic! KPalindrome2 exceeds max length of ptr(=40)
Ø TBuf<n>
这也是一个模板类,它是一个可修改的缓冲区描述符类,后面的<n>表示缓冲区大小。TBuf从TBufBase类派生,而TBufBase是从TDes派生的,因此,它继承了TDes和TDesC类所有的方法。像TBufC<n>一样,TBuf<n>也定义了一系列的构造方法和赋值操作。对所有的描述符类型来讲,内存管理是您的责任,尽管这个缓冲区中的数据是可修改的,但它的长度不能超过在构造方法中所给定的最大值(n)。假如缓冲区的内容需要扩展,那么您必须决定是在编译的时候就给定一个足够大的值,或是在运行的时候动态分配内存。但无论哪种情况,都要确保数据长度不要超过缓存区的最大长度。
如果需要使用动态分配的内存,您可以使用基于堆的描述符,这个我们在后面要讲到。要是您觉得管理内存分配的任务太过繁重,您也可以选择使用动态数组。不过,您应当记住,使用动态数组的额外开销是很高的。
_LIT(KPalindrome, "Satan, oscillate my metallic sonatas");
TBuf<40> buf1(KPalindrome); // Constructed from literal descriptor
TBuf<40> buf2(buf1); // Constructed from constant buffer descriptor
TBuf8<40> buf3((TText8*)"Do Geese see God?"); // from C string
TBuf<40> buf4; // Constructed empty, length = 0, maximum length = 40
// Illustrate copy and replace
buf4 = buf2; // buf2 copied into buf4, updating length and max length
buf3 = (TText8*)"Murder for a jar of red rum"; // updated from C string
3) 基于堆的(Heap-Based)缓冲区描述符
当您要使用非常长的字符串时,有另外一种选择:基于堆的描述符。它能拥有比它的创建者更长的生存期。当您在编译的时候还不能确定缓冲区长度的时候,堆描述符也是很有用的,这时,它的作用相当于C语言中的malloc。
Ø HBufC
也许您已经发现,HBufC的类名以“H”开头,这不符合Symbian系统中惯用的命名习惯。这的确是一个特例,“H”表示这个类一般是在堆(Heap)上分配的。HBufC定义了静态的NewL()方法,用来在堆上创建一个缓存区。正如您所见到,HBufC中的字母“C”表示这个表述符是不可修改的。对该类的操作几乎和TBufC<n>一样:该类提供了一套赋值操作,允许整个缓冲区中的内容被替换掉;同样,新内容的长度不能超过缓存区的大小,否则会引起系统异常;通过调用Des()方法,可以返回一个可修改的指针描述符(TPtr),可以通过这个指针描述符来更改缓冲区中的内容。
_LIT(KPalindrome, "Do Geese see God?");
TBufC<20> stackBuf(KPalindrome);
// Allocates an empty heap descriptor of max length 20
HBufC* heapBuf = HBufC::NewLC(20);
TInt length = heapBuf->Length();// Current length = 0
TPtr ptr(heapBuf->Des()); // Modification of the heap descriptor
ptr = stackBuf; // Copies stackBuf contents into heapBuf
length = heapBuf->Length(); // length = 17
HBufC* heapBuf2 = stackBuf.AllocLC(); // From stack buffer
length = heapBuf2->Length(); // length = 17
_LIT(KPalindrome2, "Palindrome");
*heapBuf2 = KPalindrome2; // Copy and replace data in heapBuf2
length = heapBuf2->Length(); // length = 10
CleanupStack::PopAndDestroy(2, heapBuf);
记住,堆描述符可以按您的要求的尺寸动态分配内存,但它不会自动按您的期望更改缓冲区的大小。在修改缓存区 的内容之前,您要确保缓存区的内存是足够的。为了帮您简化这些操作,HBufC提供的一套ReAllocL()方法,它可以用来扩展堆的缓存区(这个操作有可能会使缓冲区从一个内存区域搬到另一个区域)。
如果您在HBufC上调用Des()方法来获取了TPtr, 在经过重新分配内存后,TPtr中的成员变量iPtr有可能变成无效的。因此,为了确保安全,在重新分配内存后,应该再次调用Des()来创建一个新的TPtr对象。
注:出于性能上的考虑,Symbian系统并没有提供可修改的堆描述符HBuf。
总结:Symbian系统中总共有5种类型的描述符,TPtrC,PTtr,TBufC<n>,TBuf<n>和HBufC。下面的图示表明了它们的继承关系。
4)字面量描述符(Literal Descriptors)
下面我们来看看字面量描述符,它相当于C语言中的static char[]。字面量描述符是通过一系列的宏来创建的,这些宏可在头文件e32def.H中找到
#define _L8(a) (TPtrC8((const TText8 *)(a)))
#define _S8(a) ((const TText8 *)a)
#define _LIT8(name,s) const static TLitC8<sizeof(s)>
name ={sizeof(s)-1,s}
#define _L16(a) (TPtrC16((const TText16 *)L ## a))
#define _S16(a) ((const TText16 *)L ## a)
#define _LIT16(name,s) const static TLitC16<sizeof(L##s)/2>
name ={sizeof(L##s)/2-1,L##s}
首先,我们来看_LIT,这是最有效率也是被使用得最多的一个。这个宏的用法如下:
_LIT(KMyLiteralDescriptor, "The quick brown fox jumps over the lazy dog");
后面KMyLiteralDescriptor就可以作为一个常量来使用,例如可以将它写到文件或显示给用户。_LIT 宏构建了一个名为KMyLiteralDescriptor的TLitC16对象,其中保存了字符串的值(在这个例子中是The quick brown fox jumps over the lazy dog),在二进制程序中可以找到这个值,因为它是被写到文件中的。如您所料,_LIT8和_LIT16的用法相似。因为描述符的宽度为16bit,所以,在将C字节类型的字符串转换为描述符能用的数据时,宏将字符串的长度除以2。
作为参考,下面给出类TLitC16的定义,其中__TText被定义为宽的,16bit的字符。TLitC8
也有类似的定义。
template <TInt S>
class TLitC16
{
public:
inline const TDesC16* operator&() const;
inline operator const TDesC16&() const;
inline const TDesC16& operator()() const;
... // Omitted for clarity
public:
TUint iTypeLength;
__TText iBuf[__Align16(S)];
};
template <TInt S>
inline const TDesC16* TLitC16<S>::operator&() const
{return REINTERPRET_CAST(const TDesC16*,this);}
template <TInt S>
inline const TDesC16& TLitC16<S>::operator()() const
{return *operator&();}
template <TInt S>
inline TLitC16<S>::operator const TDesC16&() const
{return *operator&();}
从上面的定义中可以看到, TLitC16 (和TLitC8) 并不从TDesC8 或 TDesC16派生,但是它们与TBufC8 或TBufC16具有相同的内存布局。这就使得TLitC16 (和TLitC8)可以用在任何可以使用TDesC的地方。您也可以用如下的方法从一个字面量构造一个指针描述符:
TPtrC8 thePtr(KMyLiteralDescriptor);
从字面量构造缓冲区描述符需要一点小技巧。如果您用size()去获得_LIT常量,它会返回相应的TLitC对象的尺寸大小,这个尺寸相当于描述符内容的尺寸加上额外的8个byte(用来存放长度值的4字节和表示结束符的NULL)。如果您想用它来构造基于堆的描述符,必须要将这额外的8个字节考虑进去。
// 定义一个包含44字符的字面量
_LIT8(KExampleLit8, "The quick brown fox jumped over the lazy dog");
TInt size = sizeof(KExampleLit8); // 52 bytes (contents + 8 bytes)
TBufC8<(sizeof(KExampleLit8)-8)> theStackBuffer(KExampleLit8);
对基于堆的描述符,您可以用描述符实际内容的长度来分配缓冲区,然后将内容拷贝到描述符中。为了得到正确的长度,您可以用公共(public)的成员变量iTypeLength,或者,也可以用更简单的方法,使用()操作符来将字面量转换成一个描述符,然后用这个得到的描述符来得到内容的长度。但最简单的方法是,使用()操作符将对象转换成描述符后,直接调用TDes::AllocL()方法,返回一个HBufC*,代码如下:
TInt descriptorLength = KExampleLit8.iTypeLength; // 44 bytes
// Form a stack buffer descriptor around the literal
// Create a heap buffer copying the contents of the literal
HBufC8* theHeapBuffer = KExampleLit8().AllocL();
// 对宽字符字面量的操作类似
_LIT16(KExampleLit16, "The quick brown fox jumped over the lazy dog");
size = sizeof(KExampleLit16);// 96 bytes (contents in bytes + 8 bytes)
descriptorLength = KExampleLit16.iTypeLength; // 44 bytes (contents)
用_L 和 _LIT生成的字面量,它们的内存布局是有差异的,如下图所示:
现在我们简单地看看 _L 和 _S 宏, 这两个宏已经过时, 但在测试代码中还经常用到。
RDebug::Print(_L("Hello world!"));
这个代码的作用相当于:
_LIT(KLit,"Hello world!");
RDebug::Print(KLit);
从上面的代码可以看到,使用_L的好处在于,您可以直接使用它,而无需在使用之前,在别的地方声明。字符串(”Hello world!”)被作为一个基本的以0结尾的字符串写到二进制文件中,它前面没有长度值(这不同于_LIT产生的字符串)。由于没有长度值,字面量的内存布局不同于描述符,并且当代码运行的时候,_L的第个实例都会产生一个临时的TPtrC,这个TPtrC的指针指向字面量的第一个字节在ROM中的存储位置。只要是在创建该字面量的生存期中使用这个临时的描述符,这都是安全的。然而,创建临时变量要求设置指针、长度和描述符的类型,这对内联的构造方法来说是一个负担,如果代码中有很多这样的字面量,也会使得二进制程序的体积增大。如果仅从存储方式上看,_S 宏和_L是相同的, 但有一点不同------它不产生临时的TPtrC描述符。如果您仅将它作为以0结尾的描述符使用,那么就使用_S宏。
到目前为止,我们已经讨论了关于描述符的基本知识,包括如何实例化每一种具体的描述符,如何访问和修改描述符的数据,以及如何置换描述符的内容。现在我们来关注一下操作数据的方法和在使用描述符时一些常见的问题。
5)描述符作参数和返回类型
在编写代码的时候,您可能不想被限制于只能使用TBuf,原因是仅仅因为某个特定的库函数要求使用它。同样的道理,作为函数的提供者,您可能对调用者传递进来的参数类型不感兴趣。事实上,您不应该要求调用者传递特定类型的参数,因为您可能在后面要修改函数的实现,您可能要改变描述符的类型,如果您将这样的函数作为编程接口,最后您不得不让您的客户也改变他们的代码。这样的改动是非常不理想的,因为它破坏了代码的兼容性。
除非您来掌管描述符(负责描述符的创建和销毁工作),您甚至可以不用知道描述符是基于堆的还是基于栈的。事实上,只要标准类型的描述符(我们前面提到的5种描述符类型之一),就可以在它上面调用适当的方法,客户代码完全可以忽略描述符的内存布局和它在内存中的位置。基于以上的原因,当您定义函数的时候,应当尽量使用抽象的基类作为函数的参数和返回值。为了有效率,描述符参数应当使用引用传递的方式,要么是const TDesC&或者是TDes&。
例如,类RFile定义了read()和write()方法
IMPORT_C TInt Write(const TDesC8& aDes);
IMPORT_C TInt Read(TDes8& aDes) const;
在这两个方法中,输入的描述符被显式地声明为8bit的宽度,这样可以既写入字符串,也可以写入二进制数据。被用来写入到文件中的参数是对一个不可修改的描述符的引用,而在读文件的时候,使用了可修改的描述符的引用。可修改描述符的最大长度决定了可以从文件中读入多少数据,所以不需要再给文件服务器传递一个表示长度的参数。文件服务器将会填充满描述符。当文件中的数据不够描述符的最大长度时,文件服务器会把所有可得的数据写入描述符。调用函数后,描述符的长度反映了写入数据的长度。这样,调用者也无需再另外传递一个参数用来表示返回的数据长度。
当写一个函数的时候, 如果参数是可修改的描述符,实际上您不必考虑它是否有足够的空间用来存放数据,因为描述符本身有边界检查的机制,如果出现了内存溢出现象,会产生系统异常。
当然,您也可能不希望在描述符数据区过短的情况下,描述符的方法会发生系统异常。这时,您应当在文档中说明,如果描述符的长度不够将会如何处理。有时候,一个比较好的方法是,给调用者返回一个长度值,这样,调用者可以采用适当的步骤来分配一个正确长度的描述符。
HBufC* CPoem::DoGetLineL(TInt aLineNumber)
{// Code omitted for clarity. Allocates and returns a heap buffer
// containing the text of aLineNumber (leaves if aLineNumber is
// out of range)
}
void CPoem::GetLineL(TInt aLineNumber, TDes& aDes)
{
HBufC* line = DoGetLineL(aLineNumber);
CleanupStack::PushL(line);
// Is the descriptor large enough (4 bytes or more) to return an
// integer representing the length of data required?
if (aDes.MaxLength() < line->Length())
{
if (aDes.MaxLength() >= sizeof(TInt))
{// Writes the length required (TPckg is described later)
TPckg<TInt> length(line->Length());
aDes.Copy(length);
}
// Leave & indicate that the current length is too short
User::Leave(KErrOverflow); // Leaves are described in Chapter 2
}
else
{
aDes.Copy(*line);
CleanupStack::PopAndDestroy(line);
}
}
另一个方案是,在函数中分配堆缓冲区,把它返还给调用者,由调用者负责销毁它。
6)常用的方法
Ø Ptr()
基类TDesC 实现了Ptr()方法,用来访问描述符的数据,该方法返回一个指向字符数组首地址的指针。您可以通过这个指针来直接操作字符串数据。 代码如下所示:
Ø Size() 和 Length()
TDesC 实现了 Size() and Length() 方法, 前者返回描述符所占有的字节数,而后者返回的是描述符的字符长度。对8bit的描述符来讲,它们是相等的,而对16bit的描述来说,Size() 返回的数值是 Length() 的两倍。
Ø MaxLength()
可修改的描述符TDes实现的这个方法返回描述符的最大长度。
Ø SetLength()和SetMax()
前者用来设置描述符的长度,这个长度值必须是小于描述符的最大长度的,否则会引起系统异常。后者将描述符的当前长度设置成最大值,注意,它不并不能扩展描述符数据区的长度。
Ø Zero()和FillZ()
前者将描述符的长度设置为0,而后者是用0来来填充描述符的内容置。如果您要用其它字符填充描述符的内容,可用Fill()方法。这个方案类似于C语言中的memset()函数。
Ø Copy()
TDes 实现了一系列的重的Copy() 方法, 下面是其中的两个:
IMPORT_C void Copy(const TDesC8 &aDes);
IMPORT_C void Copy(const TDesC16 &aDes);
这些方法将参数描述符中的数据拷贝到目标描述符中,同时为目标描述符设置新的长度。如可源描述符的长度超过目标描述符的最大长度,将会引发一个系统异常。
7) 使用HBufC 堆描述符
我们已经讨论过描述符的一些特性,现在来关注一下使用描述符时经常容易范的错误。
首先,我们将创建和使用堆描述符HBufC。前面提到过,在已有的描述符上调用Alloc()或AllocL()方法,可以产生一个新的HBufC。这里是一个例子:
void CSampleClass::UnnecessaryCodeL(const TDesC& aDes)
{
iHeapBuffer = HBufC::NewL(aDes.Length());
TPtr ptr(iHeapBuffer->Des());
ptr.Copy(aDes);
...
// 以上代码完全可以被下面的代替,下面代码更有效率。
iHeapBuffer = aDes.AllocL();
}
Another common way to introduce complexity occurs in the opposite
direction, that is, the generation of TDesC& from a heap descriptor.
当从一个堆描述符产生一个TDesC&的时候,也容易范一个错误,这个错误同样为代码增加了复杂性。代码如下所示:
const TDesC& CSampleClass::MoreAccidentalComplexity()
{
return (iHeapBuffer->Des());
// 以上代码完全可以写成
return (*iHeapBuffer); //这样更简洁高效
}
另外一个比较微妙问题是,当您分配一个HBufC以后,然后在它上面调用Des(),可以返回一个TPtr对象。
HBufC* buf = HBufC::NewL(9);
TPtr p = buf->Des();
可是,假如您回忆一下,可以知道在HBufC中,并没有一个字(word)用来保存最大长度的信息------因为HBufC是不可修改的(non-modifiable),它不需要最大长度的信息。然而,,TPtr需要这个最大长度的信息,这时问题来了,您从哪里得到这个最大长度呢?答案在于:当您调用Des()的时候,系统用HBufC的最大长度来设置TPtr的最大长度(iMaxLength)。
在这个例子中,buf的最大长度是多少呢?它是9吗?答案是不一定。堆描述符的最大长度有可能并不是您所期望的值(在这个例子中为9)。这是由于您并没有指定一个字对齐(word-aligned)的最大长度,所以最后的实际的长度可能比您所指定的长度要大一些(但我们不能确定这个值到底是多少)。
_LIT(KPanic, "TestPointer");
const TInt KBufferLength = 9;
void TestPointer()
{ // Create a buffer with length KBufferLength = 9 bytes
HBufC8* myBuffer = HBufC8::NewMaxL(KBufferLength);
TPtr8 myPtr(myBuffer->Des());
TInt len = myPtr.Length(); //len的值为0
TInt maxLen = myPtr.MaxLength(); //得到一个比KBufferLength稍大数,并不固定
myPtr.SetLength(KBufferLength); //或myPtr.SetMax();否则下面的语句不起作用
myPtr.Fill(’?’); // 用’?’填充描述符
char* ptr = (char*)myPtr.Ptr();//确保已经调用了SetLength()或SetMax()方法。
ptr[0] = 'x';
HBufC8* buf = HBufC8::NewLC(9);
TPtr8 ptr(buf->Des());
TInt maxLength = ptr.MaxLength(); // maxLength比9大,但不一定等于12(字的边界)
3.11 TFileName的滥用
对TFileName 对象的滥用是一个潜在的危险。TFileName是在文件 e32std.H中定义的:
const TInt KMaxFileName=0x100; // = 256 (decimal)
typedef TBuf<KMaxFileName> TFileName;
由于每个宽字符相当于两个字节(前面说过,默认情况下,TBuf是16bit宽度的), 所以,无论文件名的长度为多少,每次您在栈上创建一个TFileName 对象的时候都消耗了524 个字节 (2 × 256 描述符数据字节 + 描述符本身的12字节)。在Symbian OS系统中,标准的栈空间的大小为8K字节,不必要地使用有限的资源是非常浪费的,所以尽量不要使用基于栈的TFileName 对象,也不要用值传递的方式使用它们,应当使用引用传递的方式。您可以在堆上使用它们,比如,您可以把它们作为C类(从CBase派生的类)的成员变量。如果,您不需要使用完整的路径,你也可以用HBufC来存放文件名,尽量少用资源总是好的。
您最好不要使用TParse 类(在f32file.H中定义)。因为这个类保存了一个描述符的拷贝,在拷贝中包含了要解析的文件名,拷贝会用掉宝贵的栈空间。您应当考虑使用TParsePtr 和 TParsePtrC 类;它们提供了同样的功能,但它们不拷贝文件名,而仅仅保存对文件名的引用。
有用的辅助类
在讨论了这些普遍的问题之后,我们在这一章的结尾来看看两个常用的辅助类。
1) TLex(TLex8,TLex16)类
像描述符一样,TLex也有8bit和16bit两种版本,分别是TLex8 and TLex16,一般情况下,你应当使用TLex,而无需指定特定的版本。该类实现了一般目的词法分析,和语法成分解析以及从字符串到数字转换的功能。
2) Pckg类
另外一套有用的辅助类分别是:TPckgBuf和TPckg以及TPckgC,它们是分别派生自TBuf<n>, TPtr 和 TPtrC的模板类,在文件e32std.H中可以找到它们的定义。
打包类(package class)能很方便地将扁平的(flat)数据对象存储到描述符中,这在跨线程或跨进程的数据共享的时候很有用。 实际上,能将一个T类对象完整地打包到描述符中,这样很容易以一种类型安全的方式在线程间共享它。
有两种Package指针类:TPckg,TPckgC,它们分别对应于可修改和不可修改的类型,都持有一个指向被包装对象的指针。
class TSample
{
public:
void SampleFunction();
void ConstantSampleFunction() const;
private:
TInt iSampleData;
};
TSample theSample;
TPckg<TSample> packagePtr(theSample);
TPckgC<TSample> packagePtrC(theSample);
在这个例子中,TPckg<TSample>表示这是一个模板类,模板参数为TSample,packagePtr(theSample)定义了一个对象,它持有一个指向theSample的针指;可以在
包对象上调用()方法,返回被包装对象的引用。代码如下:
packagePtr().SampleFunction(); //合法
packagePtrC().SampleFunction();//编译错误!只能调用const方法
packagePtrC().ConstantSampleFunction();//合法
TPckgBuf类创建并存储一个新的被包装类型的实例(注意,是窗建新的实例而不是保存指针) ,TPckgBuf自己管理这个拷贝的对象;在TPckgBuf对象上调用()方法,可以返回对拷贝的引用,然后可以在这个引用上调用其它的方法。这个TPckgBuf对象所拥有的拷贝可以被修改。代码如下:
TPckgBuf<TSample> packageBuf(theSample);
packageBuf().SampleFunction();
由于TPckgBuf拥有原始数据的拷贝,所以,如果在上面调用了可修改成员变量的方法,那么被修改的只是拷贝的数据,而原来的数据不受影响(这类似于函数调用时的值传递方式)。