第二章:数据类型与语法
作者:谢兴 [email protected] 转载需注明出处 下载word版本
Symbian系统已经提供了一套已经定义好的内置的数据类型。为了保证你的代码是编译器无关的,应当使用下面symbian系统提供的数据类型,而不要使用原生数据类型(native types,这里指标准C中的int ,char等)。
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
标示布尔类型。Symbian系统提供了两个常量:ETrue (=1) 和 EFalse (=0),分别表示真和假。
注意:在Symbian系统中,TBool被定义为int, 而ETrue和EFalse被定义为enum,所以,如果一个函数的返回值为TBool,不要用如下的代码来比较函数的返回值:
TBool isLarger(TInt a, TInt b)
{
return (a>b)?ETrue:EFalse;
}
if(isLarger(4,3)==ETrue){...} //错误,编译不过。
if(isLarger(4,3)){...} //正确
2类和对象
2.1 Symbian系统中的命名习惯:
在Symbian系统中编写代码时,应当遵守种样几个规则:成员变量的命名以小写字母i开头,方法的参数以小写字母a开头,例如:
class Pernon
{
public:
TInt iAge;
void SetAge(TInt aAge){iAge = aAge};
}
在symbian系统中存在几种不同类型的类(class),不同类型的类,其特性也各不相同。
有的在堆(heap)上创建,有的在栈(stack)上创建,特别的是,类的实例(instance)的清除方式也不尽相同(下面,为了方便我们把类的类别称为型别)。型别(class type)可以体现这些不同的特点。每个型别都有一套定义好的关于如何创建和清除实例的规则。为了容易区分型别,Symbian系统使用了一个简单的命名规则:类名以大写字母开头(T,C,R 或M)。作为类的设计者,你先要考虑这个类的行为,看它到底与哪种型别匹配,一旦确定了它的类型,然后你就可以专注于该类的功能。同样,对一个类的使用者来讲,如果他不熟悉这个类, 但类的命名规则可以帮助他弄清你的意图------如何用安全的方式初始化、使用和销毁一个类的对象(object)。
下面,我主要讨论不同型别的主要特性。
Ø T 类
T类的行为类似于C++中的内置类型,因此,它们以T作前缀(”T”代表”Type”)。象内置类型一样,它们没有析构方法(destructor),这导致的结果是:T类不能包含具有析构方法的成员变量。所以,一般情况下,T类的成员变量只能是内置类型的数据或者是其它的T类的对象。在某些的情况下T类也可以包含其它对象的指针或引用,不过,这时它们之前是“使用”关系,而不是“拥有”关系(也就是说,这个T类对象并不负责对成员的创建和销毁的工作)。不能拥有外部数据的原因是因为T类没有析构方法。正是由于没有析构方法,T类的对象可以在栈上创建,当程序流程退出函数或产生leave(一种代码异常)的时候,系统自动清除它。即使T类有一个析构方法,在发生异常(在Symbian系统中,异常被称为leave)时Symbian 系统也不会调用它,因为leave没有模仿标准C++的抛出异常的做法。
T类的对象也可以在堆上创建。但是,应当在调用有可能发生异常的代码之前,将这个对象放入到清除栈(cleanupStack),在发生异常的时候,清除栈(cleanupStack)会释放这个对象。
Ø C 类
这种类都是从CBase派生来的(直接或间接)。
//.h file
class CStudent:public CBase
{
public:
CStudent(){
RDebug::Print(_L("i am a student"));
};
~CStudent()
{
RDebug::Print(_L("please, don't kill me!"));
}
void SampleFunction(){};
private:
TInt iCode;
TInt iScore;
};
CBase有两个特点:首先,它有一个虚的析构方法,这样,可以通过CBase指针来删除它的子类。代码如下所示:
CBase *pStu = new CStudent();
delete pStu;
结果: i am a student
please, don't kill me!
其次,CBase类和它的子类,重载了new操作符,这使得当它在堆上创建的时候,自动初始化为0,也就是说,当它一开始被创建出来的时候,所有的成员变量都被初始化为0,所以您不必在构造方法中去做这件事情。但是,在栈上创建对象时,情况并非这样, 因为这时没有用到new操作。这将潜在地导致堆上创建的对象和栈上创建的对象的行为不一致。因此,C类的对象一定要在堆上创建。
很明显,当一个堆上的C类对象不再被需要时,我们需要消耗它。 一个C类的对象可能以两种方式存在:其它类的指针成员变量或是一个局部的针指变量。在前一种情况下,我们可以在类的析构方法中调用delete来删除它;后一种情况要复杂一些,在调用任何有潜在的异常(leave)的代码之前,要把这个指针放到清除栈(cleanup stack)中,否则有可能发生内存泄露。CBase 类声明了私有的拷贝构造方法和赋值操作(=)。这是一个很好的策略,它可以用来防止客户代码不小心地使用了浅拷贝或赋值的方法。由于基类的拷贝构造和赋值是私有的,所以,如果您希望您的类可以能够使用拷贝构造方法,您必须显式地声明和定义拷贝构造方法和赋值操作。但是,考虑到C类的特性,深拷贝可能造成发生异常(leave)的隐患,而您绝对不能让类的构造方法(或析构方法)发生异常(我们在本教程的后面解释原因)。所以,如果您确实需要一个拷贝的方法,那么您可以为类添加一个的可能会发生异常的方法来完成同样的任务,例如:CloneL()或CopyL()。如果您提供的这个类是从CBase派生的,您就不必为了防止客户代码使用有潜在安全问题的“浅”拷贝,而在代码中将这些方法声明为私有的。
Ø R 类
前缀“R” 在这里代表资源(Resource), 通常是外部资源,例如:文件的句柄(handle)。
和C类一同,Symbian系统中不存在一个对应的RBase类,所以一个R类应当有一个构造方法来将它的资源句柄置为0,表明还没有资源和这个新建的对象关联在一起。但是,不要在构造方法中初始化资源句柄,因为这样有可能使构造方法产生异常。R类中常常有类如Open(), Create() 或 Initialize()这样的方法,它们用来分配资源,设置句柄成员变量的值,并返回错误代码或是产生异常。 R类通常也有对应的Close()或Reset()类,用来释放资源,重置句柄的值------表明没有资源和该对象关联。使用R类时,一个常见的错误是忘记调用它的Close()方法(当然,该方法也可以是其它名字,但它经常被命名为Close())或是有一个析构方法释放资源,这会引起资源的泄露。
R类通常都很小,除了资源句柄没有其它的成员变量。因为不需要。它通常也没有析构方法,资源的释放都是在Close()方法中进行的。大多数情况下,R类都是作为类的成员变量或局部变量存在的。只有少数情况下,在堆上创建。
您必须确保,当程序发后异常的时候,资源能被正确地释放------通常是使用资源栈。如果一个R类是一个堆上的自动变量(相对于成员变量),您一但要保证资源被释放,而且,变量本身也要被释放。
R类的成员变量通常都很简单,所以一般不需要深拷贝(bitwise copy)。R类的拷贝可能会引起混乱(想象一下:如果两个对象同时在一个资源句柄上调用Close()方法,或两个对象都没有释放资源,会发生什么情况?)如果,您想阻止任何对R类的拷贝,您应当声明(但不定义)一个私有的构造方法和赋值操作。
Ø M 类
当提到多继承的时候,它意味着从一个主要的类派生,同时也混杂基它类的功能。前缀M是单词Mixin的首字母。Symbian系统不赞成多继承的做法,因为这个引入额外的复杂性,M类是一个抽象类,它的作用相当于java中的接口(interface)。在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
};
上面的例子演示了一个从CBase类和一个M类派生的具体类。而类MDomesticAnimal又是从MAnimal派生的。象接口一样,由于不能被实例化,M类只能有虚(virtual)函数,不能有成员变量和构造方法。但它可以有析构方法, 条件是,实现它的具体类必须是从CBase派生的。在定义完类以后,然后可以用使用它。代码如下:
CCat *cat1 = new CCat;
delete cat1; //正确
然下面的代码却是错误的。
MAnimal *cat2 = new CCat;
delete cat1; //错误
当用M类的指针引用一个对象的时候,如果用delete删除这个指针,则这个M类必须提供一个虚拟的析构方法,否则会出现系统异常(panic code 42)。将MAnimal的代码改写,则上面代码没有问题。
class MAnimal
{
public:
virtual void EatL() =0;
virtual ~MAnimal(); //增加一个虚的析构方法。
};
3描述符(descriptor)
在Symbian 系统中,字符串被称为“描述符”(descriptor),因为它们是自我描述的。在描述符中保存了它所表示的字符串的长度和它的底层的内存布局的信息。描述符比标准C中的字符数组和字符指针要复杂,您可能需要多花些时间来学习和掌握它的用法。关键是,它们的特殊设计使得它们在少量内存的设备上非常有效率,仅用非常少的内存就可以保存自己的长度和内存布局的信息。现在,让我们来深入了解描述符的设计思想。
在Symbian系统中,描述符是相当让人迷惑的,因为它的种类繁多。不同种类的描述符具有不同的特性和用法,但又经常能相互转换。它们不同于标准C++中的string,java语言中的string类或MFC中的CString,因为程序员必须自己管理底层的内存分配和清除工作。它们具有防治内存溢出的机制,并且不依赖NULL终结符号来决定字符串的长度,从这方而来讲,它也不同于C语言中的字符串。
现在我们来讨论:什么是描述符?它们是如何工作的?在探讨这些不同的描述符之前,先让我们需要弄清楚一个基本的概念:什么是字符串数据的“宽度”?这个长度指的是单个字符是8bit的,还是16bit的宽度。在早期的版本中,字符的宽度都是8bit的,后来为了支持Unicode字符集,从第5版起,Symbian系统将16bit 的字符作为标 准。Symbian系统现在支持这两种字符长度的描述符,除了Copy()和Size()两个方法以外,这两种宽度的描述符的行为是完全一致的,这两个方法的使用,我们后面再介绍。另外,有一套中立的描述符类型,它们既可以被定义为窄的描述符类型,也可以被定义为宽的描述符类型,这要取决于编译时的宽度。您可以从它的名字上很容易看出这个类型所表示的宽度。假如,它以8结尾(例如:TPtr8,就意味着它表示是的8bit的窄字符,而以16结尾的描述符类(例如:TPtr16)则操作16bit的宽字符。 对中立(neutral)的类型来讲,没有数字结尾(例如:TPtr),在Symbian系统第5版以后,默认的情况下,它们表示宽度为16bit的字符串。它们之间的关系比较类似于TInt,TInt16或TInt32 之间的关系,这一点应当是比较易于理解的。
一般情况下,您没有必要指明是字符串的宽度,用中立的类型就可以了,这样使你的代码易于在宽字符版本和窄字符版本之间转换(有过编程经验的朋友应该有这样的印象,我们平常写代码,大多情况下,仅仅使用UINT类型,而较少考虑使用UINT16,UINT32类型)。
另外一个问题是:描述符和字面量(literal)的区别。所谓字面量是指在编码的时候就已经确定的量,例如,标准C中的
char* p = "Hello world";
其中的"Hello world"就是字面量。在Symbian系统中,对它们的处理是很不一样的,这点我们在后面再介绍。
有了这样的一些认识, 现在我们可以来看看有哪些描述符类型。在Symbian系统中描述符类型有两大种类:不可修改(non-modifiable)的描述符和可修改(modifiable)的描述符。
3.1不可修改(non-modifiable)的描述符
在Symbian系统中,所有的描述符都继承自TDesC,在前面我们已经讨论了类名前缀T所代表的意义,在这里,我们更关心类名的后缀C所代表的意义,这个C是单词Constant的首字符,表示这个类是不可更改的。这个类提供了一些用来返回字符串的长度和操作数据的方法。Length()方法返回了描述符的长度,因为,每个描述符对象在内存中的布局都是同样的,用4个字节来表示它所包含的数据的长度(实际上,只用了32个bit中的28个bit,剩余的4bit留作它用,所以描述符能表示的最大的长度为228 字节,256 MB,不过这已经足够了)。所以,Length()方法没有被它的子类重写,它对所有子类都有效。但是,根据实现子类的方法的不同,子类访问数据的方式也不一样,Symbian系统不要求它的子类通过虚函数的方式来实现自己的访问数据的方法。 不用虚函数重写的原因是因为,虚函数会给每个被派生的描述符对象增加4节字的额外负担,c++用这4个字节来存放指向虚函数表的指针。我们前面说过,在设计描述符的时候要让它尽可能高效,额外的字节开销被认为是不理想的。存放长度的4个字节中,28bit用来表示长度,剩下的4bit用来表示描述符的类型。目前,symbian系统中有5种派生的描述符类型,4bit限制了描述符的种类最多只能有16种,但这已经足够了。子类可以通过调用基类TDesC的Ptr()方法来访问描述符的数据,Ptr()方法检查这4个bit,确定描述符的类型并返回它的数据在内存中的地址。当然,这要求TDesC基类清楚它的子类的内存布局,并在Ptr()方法中使用硬编码的方法。后面,为了表述上的方便,我们也把这种不可修改的描述符也称为常量描述符(constant descriptor)
总结:不可修改的描述符类TDesC是所有的非字面量描述符的基类,它提供了确定描述符长度和访问数据的方法,另外,它实现了所有的您想用来处理常量字符串的操作。
3.2可修改(modifiable)的描述符
所有的可修改的描述符都从TDes基类派生,而TDes本身又是从TDesC派生的。TDes有一个额外的成员变量用来存放为该描述符分配数据的最大长度。MaxLength()方法返回了这个最大的长度。像TDesC中的Length()方法一样,MaxLength()方法也不被TDes的子类继承。 TDes类提供了一系列的方法, 用来对可修改字符串数据的操作,包括对字符串的附加、填充和格式化操作。所有的这些方法都被派生类继承,派生类只实现一些特定的构造方法和复制赋值的方法。这些方法都不负责分配内存,假如它们超过了描述符的数据长度,例如,用Append()方法在某个字符串后面附加另一个字符串时,在调用该方法之前,您必须确保有足够的内存空间。当然,只要不超过描述符的最大存储容量,描述符的长度可以自由地伸缩。当描述符长度比最大长度短的时候,描述符的后面部分是多余未用的。这些方法使用了断言(assertion)来确保描述符的最大长度不会被超出。如果发生内存溢出,将会产生一个panic(关于panic,我们将在后面的章节介绍),这样可以方便您检查和修正程序的错误。
事实上,不可能使描述符溢出,这一点保证了您代码的强壮性,而且不易产生难以跟踪的内存陷阱。
但需要注意的是,由于基类的构造方法是proteced类型的,所以您无法直接实例化一个TDesC或TDes类的实例。现在我们来看看描述符的派生类,您可以实例化和使用派生类的对象。正如前面所说,这个地方是比较让人迷惑的,因为描述符存在大量的派生类。 前面,我们已经解释过为什么每个类会有三个不同的版本,例如:TDes8, TDes16 和 TDes,分别对应窄字符,宽字符和中立的类。现在,让我们看看有哪些主要的描述符类型,在深入讨论每种类型的细节之前,我们先考察一下它们在一般情况下的内存布局。描述符有两种基本的内存布局:指针描述符和缓存区描述符。不同之处在于,指针描述符持有一个指向字符串的指针,而这个字符串存储在内存中的基它位置。与指针描述符不同,缓存区描述符本身持有字符数据,也就是说字符数据本身构成了描述符的一部分。
总结:TDes 是所有的可修改的描述符的基类, 并且它自己也是从TDesC派生的。它有一个能返回最大的内存容量的方法和一系列的用来修改字符串数据的方法。
3.3 指针描述符(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()方法和=()赋值操作。前者将描述符的指针重置,使它指向新的数据区域,而后者将数据拷贝到描述符中,一般来说,这会更改描述符的长度,但不会更改它的最大长度值。
3.5 基于栈(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.6 基于堆的(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。下面的图示表明了它们的继承关系。
3.7字面量描述符(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宏。
到目前为止,我们已经讨论了关于描述符的基本知识,包括如何实例化每一种具体的描述符,如何访问和修改描述符的数据,以及如何置换描述符的内容。现在我们来关注一下操作数据的方法和在使用描述符时一些常见的问题。
3.8描述符作参数和返回类型
在编写代码的时候,您可能不想被限制于只能使用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);
}
}
另一个方案是,在函数中分配堆缓冲区,把它返还给调用者,由调用者负责销毁它。
3.9常用的方法
Ø 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);
这些方法将参数描述符中的数据拷贝到目标描述符中,同时为目标描述符设置新的长度。如可源描述符的长度超过目标描述符的最大长度,将会引发一个系统异常。
3.10 使用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 类;它们提供了同样的功能,但它们不拷贝文件名,而仅仅保存对文件名的引用。
4有用的辅助类
在讨论了这些普遍的问题之后,我们在这一章的结尾来看看两个常用的辅助类。
4.1 TLex(TLex8,TLex16)类
像描述符一样,TLex也有8bit和16bit两种版本,分别是TLex8 and TLex16,一般情况下,你应当使用TLex,而无需指定特定的版本。该类实现了一般目的词法分析,和语法成分解析以及从字符串到数字转换的功能。
4.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拥有原始数据的拷贝,所以,如果在上面调用了可修改成员变量的方法,那么被修改的只是拷贝的数据,而原来的数据不受影响(这类似于函数调用时的值传递方式)。
下面的图示表明了它们的内存布局:
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wh_xiexing/archive/2007/11/22/1897843.aspx