Posted by felix on December 4th, 2008
C++的模板对于那些可以被多种类型重用的代码是非常有用的,如:Symbian C++中的泛型集合RArray和RPointerArray就是使用的模板来实现的。但是,使用普通C++模板会带来代码尺寸大大增加的问题。本文将分为“C++模板基础”、“TBuf分析”、“瘦模板”三个部分,逐步深入讲解Symbian C++瘦模板的概念和使用方法。
一、C++模板基础
在这一部分中不会详细的介绍C++的模板,只会已不同的代码的形式介绍C++模板的几种不同的使用方法,在这里只会以类模板作为例子。如果大家想对C++模板进行更深一步的了解,请参阅《C++ Templates》一书。
1、类模板的声明
template <typename T> class TStudent { ... };
而在Symbian SDK中,我们看到的最常见的形式是:
template <class T> class TStudent { ... };
在上面的代码中,T通常成为“模板参数”。以上两种模板声明方式效果是一样的,template <class T>的声明形式会容易让人产生误解,在此不一定非要类类型才能作为模板参数,相比之下template <typename T>的形式更好一些。
2、类模板的特化
类似于函数重载,类模板提供了对模板参数的重载,实现了对不同类型的参数的不同处理,这一个过程就叫做类模板的特化。通过以下代码可以简单的说明这一点:
1 template <typename T> class TStudent 2 { 3 public: 4 void DoPrint() 5 { 6 console->Write(_L("T")); 7 } 8 }; 9 10 template <> class TStudent<TInt> 11 { 12 public: 13 void DoPrint() 14 { 15 console->Write(_L("TInt")); 16 } 17 }; 18 19 LOCAL_C void MainL() 20 { 21 TStudent<TUint> stu1; 22 stu1.DoPrint(); 23 24 console->Write(_L("/n")); 25 26 TStudent<TInt> stu2; 27 stu2.DoPrint(); 28 } 29
以上代码的作用:如果TStudent的模板参数为TInt型,则在屏幕上打印“TInt”,否则打印“T”。
输出结果:
3、非类型的类模板参数
假如我们有这样一个需求:由一个模板类TStudent,内部有一个模板数组T iValue[],要求该数组大小有用户来定,且在编译器就已经确定,实现这个类。
如果没有“在编译器确定数组大小”这个需求,我们可以很简单的把这个类设计好:
1 template <typename T> class CStudent 2 { 3 private: 4 T* iValue; 5 TInt iSize; 6 7 public: 8 CStudent(TInt aSize) : 9 iValue(new T[aSize]), iSize(aSize) 10 { 11 } 12 13 ~CStudent() 14 { 15 delete []iValue; 16 iValue = NULL; 17 } 18 19 void DoPrint() 20 { 21 TInt size = sizeof(T) * iSize; 22 TBuf<10> buf; 23 buf.Num(size); 24 console->Write(buf); 25 } 26 }; 27 28 LOCAL_C void MainL() 29 { 30 CStudent<TInt>* stu = new CStudent<TInt>(10); 31 32 stu->DoPrint(); 33 34 delete stu; 35 stu = NULL; 36 } 37
但是,以上代码中CStudent类的成员变量iValue指向的数组大小是在运行期才确定的,这显然不能满足我们的需求。
再看以下代码:
1 template <typename T, TInt S> class TStudent 2 { 3 private: 4 T iValue[S]; 5 6 public: 7 void DoPrint() 8 { 9 TInt size = sizeof(iValue); 10 TBuf<10> buf; 11 buf.Num(size); 12 console->Write(buf); 13 } 14 }; 15 16 LOCAL_C void MainL() 17 { 18 TStudent<TInt, 10> stu; 19 stu.DoPrint(); 20 } 21
我们将模板参数类型替换成TInt,S就是非类型的类模板参数。从以上代码来分析,使用非类型的类模板参数的好处和限制:
- TStudent类中的成员变量iValue数组的长度在声明stu变量(第18行)时就已经确定,在编译期可以由用户确定iValue数组的大小;
- 模板可以具有值模板参数,而不仅仅是类型模板参数;
- 对于非类型模板参数,不能使用浮点数、class类型的对象作为实参。如:不能使用template <float T>,或template <TDesC T>。
二、TBuf分析
首先贴出SDK中TBuf的声明代码,为了篇幅整洁,省去了一些不相关的代码:
1 template <TInt S> 2 class TBuf : public TBufBase16 3 { 4 public: 5 inline TBuf(); 6 inline explicit TBuf(TInt aLength); 7 inline TBuf(const TText* aString); 8 inline TBuf(const TDesC& aDes); 9 inline TBuf<S>& operator=(const TText* aString); 10 inline TBuf<S>& operator=(const TDesC& aDes); 11 inline TBuf<S>& operator=(const TBuf<S>& aBuf); 12 private: 13 TText iBuf[__Align(S)]; 14 };
从以上声明我们可以看出以下几点:
- 从第一行可以看出,TBuf类模板只有一个模板参数,此参数的类型为“非类型的模板参数”;
- 第13行,__Align为一个宏,相关代码:
#define __Align(s) ((((s)+__Size-1)/__Size)*__Size) #define __Size (sizeof(TUint)/sizeof(TUint16))
运算步骤:
sizeof(TUint) = 4;
sizeof(TUint16) = 2;
__Size = 2;
如果s = 10;
11 / 2 = 5, 5 * 2 = 10
__Align(s) = (10 + 2 - 1) / 2 * 2 = 10;
如果我们定义TBuf<10>,对象内部会转变为TText iBuf[10]; - 因为存在第10行的运算符“=”号重载方式,所以以下代码能够正确执行:
TBuf<20> buf1; TBuf<10> buf2; buf1 = buf2;
而我们上面的TStudent类代码是能够这样编写的。
三、瘦模板
1、C++模板弊端
C++编译器会在编译的时候将类模板代码“拆分”,生成针对于不同类型模板参数的拷贝,这些拷贝的区别只在于类型的不同。如下代码:
template <typename T> class TStudent { ... }; TStudent<TInt> stu1; TStudent<TUint> stu2;
在程序编译的时候,会生成类模板的两个副本:TStudent_TInt,TStudent_TUint,来区分不同类型的类模板参数调用。
所以,使用C++模板不会带来运行效率的降低,但是会带来编译后代码尺寸的增大!
如果一个类不大,这种尺寸的开销不足为惧。但是如果一个类很大且存在很多不同类型的模板参数调用,这个问题就大了(提醒一下:大家在做内存受限的系统开发,不是PC机)。
2、RArray和RPointerArray
RArray类和RPointerArray类实现了瘦模板机制,有关以上两个类的说明,请参看“集合与数组(1)- RArray和RPointerArray”这篇随笔。
我们来看一下SDK中RArray类的相关声明:
1 template <class T> 2 class RArray : private RArrayBase 3 { 4 ... 5 inline const T& operator[](TInt anIndex) const 6 { return *(const T*)At(anIndex); } 7 inline T& operator[](TInt anIndex) 8 { return *(T*)At(anIndex); } 9 ... 10 } 11 12 class RArrayBase 13 { 14 protected: 15 ... 16 IMPORT_C TAny* At(TInt anIndex) const; 17 ... 18 }
我们从以上代码可以学习到:
- RArray类的绝大多数函数都是从RArrayBase继承的,而RArrayBase不是类模板;
- 瘦模板的设计方法:
在通用基类RArrayBase中实现必要的逻辑代码,但是使用非类型安全的TAny*指针,因为为类型不安全的,所以都要放在protected块中;
在派生类中使用模板,已达到类型安全的目的; - 虽然RArray经过编译后也会产生RArray_TInt、RArray_TUint等等这样的类,但由于其继承自RArrayBase,主要逻辑代码在RArrayBase中,所以代码大小的开销会很小,RArray_TInt、RArray_TUint在这里就相当于RArrayBase的不同的“壳”而已;
- RArray私有继承自RArrayBase,这样,在外部调用者看来,RArray和RArrayBase就没有任何继承上“is a”的关系,如:以下代码是不可行的:
RArray<TInt> arr; RArrayBase arrBase = arr;
所以,我们在编写Symbian OS C++类模板时,要按照以上几点进行设计,以保证生成代码的最小化。
另:TBuf类的设计也属于瘦模板,主要逻辑代码都继承自TBufBase,而TBuf只保存模板参数S所指定的不同大小的缓冲区结构。
四、参考文献
- C++编程思想 第1卷 标准C++引导
- C++ Templates 中文版
- Symbian OS C++ 高效编程
- C++私有继承和保护继承
Symbian编程总结-网络与通信-套接字(1)-套接字体系结构与相关API
Posted by felix on December 3rd, 2008
套接字编程在网络与通信中起着举足轻重的作用。套接字的API最初为方便在BSD Unix中建立TCP/IP的连接而设计的,现在已经成为多种平台(包括Symbian)建立TCP/IP连接的标准API。除了TCP/IP外,套接字API足以通用于其他对应的网络类型和协议。Symbian利用了这个事实,可以使用套接字API在红外线、蓝牙等协议上建立连接。本系列将从网络套接字入手,概述套接字编程的核心实质,同时以大家熟知的.net Socket编程作为参照物,介绍Symbian中的套接字编程。
一、Symbian OS 套接字体系结构
1、客户机/服务器模式
Symbian OS的套接字是基于客户机/服务器(C/S)模式的。这一点可以从套接字编程中使用到的以下几个类体现:RSocketServ、RSocket、RConnection。RSocketServ派生自RSessionBase,作为与系统服务器通信的父会话;RSocket和RConnection都派生自RSubSessionBase,作为RSocketServ的子会话与系统服务器通信。简单的说,要想进行Socket编程,必须得通过RSocketServ连接系统的套接字服务器。有关Symbian OS的客户机/服务器框架的只是,请参阅“深入篇-客户机/服务器框架系列”文章。
2、客户端的套接字与服务器端的套接字
这里的“客户端”、“服务器端”与Symbian OS的“客户机/服务器”不是一个概念。这里的“客户端”、“服务器端”指的是在网络中两个互相暴露的两个连接的端点。Socket服务器端被动等待Socket客户端的连接,反过来,Socket客户端主动请求与Socket服务器端连接。一旦连接完毕,两个端点就可以向对方发送或接收数据。
3、面向连接的套接字和面向无连接的套接字
在此所说的面向连接的套接字为TCP模式,而面向无连接的套接字为UDP模式。有关TCP和UDP的相关知识,请参阅《TCP/IP详解》第二卷的相关章节。
4、异步套接字与同步套接字
在.net中,Socket类的许多操作都对应有同步版本和异步版本,如:Accept与BeginAccept、EndAccept,Receive与BeginReceive、EndReceive等等。而在Symbian中,RSocket提供的对应方法都为异步函数,我们可以使用活动对象或者User::WaitForRequest方法进行封装调用,请参阅“活动对象正解(4)-异步函数的同步调用 ”。下面例举几个异步方法的原型:
IMPORT_C void Send(const TDesC8& aDesc,TUint someFlags,TRequestStatus& aStatus); IMPORT_C void Recv(TDes8& aDesc,TUint flags,TRequestStatus& aStatus); IMPORT_C void Accept(RSocket& aBlankSocket,TRequestStatus& aStatus);
二、Socket编程流程及相关API
一般情况下,我们会在手机上建立客户端的套接字,去连接远程服务器端的套接字,而且使用的是TCP方式连接。所以,在此我们将以客户端的TCP Socket编程作为入手点,简单介绍Socket编程流程。
首先先用时序图描述一下具体流程:
接下来简单的介绍相关API:
- 连接套接字服务器:RSocketServ::Connect()
此函数返回错误代码,由于套接字服务器可能已经连接,所以此处可能返回KErrAlreadyExists,应该使用如下方法进行判断:TInt err = iSocketServ.Connect(); if (err != KErrNone && err != KErrAlreadyExists) { User::Leave(err); } - 打开套接字:RSocket::Open()
IMPORT_C TInt Open(RSocketServ& aServer,TUint addrFamily,TUint sockType,TUint protocol);第一个参数传入套接字服务器,后面几个参数表示连接模式的常量。针对于TCP连接,应该使用如下代码:
iSocket.Open(iSocketServ, KAfInet, KSockStream, KProtocolInetTcp);如果记不住每种连接模式对应这三个参数的值,还可以通过使用RSocketServ::FindProtocol的方法,传入模式名称获取一个TProtocolDesc对象,在这个对象中描述了对应的addrFamily、socketType、protocol的值。具体用法请参看SDK。
- 将网络端点表示为 IP 地址和端口号:TInetAddr
TInetAddr类似于.net中的IPEndPoint类。使用TInetAddr::Input方法传入IP地址,使用TInetAddr::SetPort方法传入端口号。 - 连接服务器端Socket:RSocket::Connect
此方法为异步函数,原型如下:IMPORT_C void Connect(TSockAddr& anAddr,TRequestStatus& aStatus);其中参数anAddr为我们创建的绑定远程服务器IP地址和端口的TInetAddr类实例。
- 向服务器端发送数据:RSocket::Send
此方法为异步函数,原型如下:IMPORT_C void Send(const TDesC8& aDesc,TUint someFlags,TRequestStatus& aStatus); - 接收服务器端返回的数据:RSocket::Recv
此方法为异步函数,原型如下:IMPORT_C void Recv(TDes8& aDesc,TUint flags,TRequestStatus& aStatus);
三、小演练:连接服务器端的套接字
1、在此我们使用C#建立一个TCP套接字服务器端,将完成以下几点功能:
- 监听IP 127.0.0.1和端口8532,等待客户端的连接;
- 客户端连接后,在屏幕上打印出客户端的IP地址和端口;
- 等待客户端发送文本数据;
- 收到数据后将数据内容打印到控制台上;
- 将收到的数据返回给客户端。
2、使用Symbian C++建立一个TCP套接字客户端,完成以下几点功能:
- 运用我们以上学到的几个API,连接IP 127.0.0.1和端口8532;
- 向服务器发送字符串“12345678”;
- 等待服务器端返回数据;
- 将服务器端返回的数据(12345678)打印到屏幕上。
客户端和服务器端运行效果如下:
四、小结
这一节作为Socket编程的入门,所涉及的知识点比较少且相对较简单,在下一节里我将详细介绍RSocketServ、RSocket、RConnection的使用方法,并针对TCP、UDP方式进行说明。
五、参考文献
- Series 60 应用程序开发
Symbian编程总结-基础篇-动态缓冲区(1)-回顾HBufC
Posted by felix on December 1st, 2008
当数据尺寸在编译期不固定,而在运行期有可能要扩展到很大尺寸时,动态缓冲区在保存二进制数据方面显得非常有用。我们可以使用C++数组保存二进制数据,然后调用类似于memcpy的函数去动态的改变数组所占用空间的大小;我们还能够使用HBufC描述符,获取其可修改的描述符向其写入数据,然后调用ReAlloc方法扩展数组。以上两点方法可行,但是不好,因为我们得自己管理内存的分配。Symbian C++考虑到了这一点,于是引入了动态缓冲区的概念。
基于堆的缓冲描述符HBufC的前缀H显然不符合Symbian C++的命名规范(请参看Symbian编程总结-基础篇-类类型)。在这里,“H”仅表明数据是存放在堆(Heap)上的。虽然HBufC类以“C”为后缀,意思是不可修改的,但是我们可以通过HBufC::Des()方法获取其可修改的TPtr指针,对HBufC的内容进行修改。
一、堆描述符的构建
- 从TDesC类的栈内容构建
TDesC类有几个方法,允许将栈中的内容复制到堆中,并返回一个HBufC指针,这些方法的函数原型如下:IMPORT_C HBufC16 *Alloc() const; IMPORT_C HBufC16 *AllocL() const; IMPORT_C HBufC16 *AllocLC() const;特别的,如果创建不成功,Alloc返回NULL,AllocL会抛出异常。
以下代码说明TDesC::Alloc的用法:1 LOCAL_C void HBufCFromDesLC(HBufC*& aBuf) 2 { 3 _LIT(KText, "Hello World!"); 4 aBuf = KText().AllocLC(); 5 } 6 7 LOCAL_C void MainL() 8 { 9 HBufC* buf; 10 HBufCFromDesLC(buf); 11 console->Write(buf->Des()); 12 13 CleanupStack::PopAndDestroy(buf); 14 } - 使用HBufC::New(L,LC)方法构建
通过HBufC::New(L,LC)方法创建时,需要在参数中指定所创建的内容占用堆空间的最大长度,如果接下来填充的数据长度超过了定义的长度则会抛出异常:IMPORT_C static HBufC16 *New(TInt aMaxLength); IMPORT_C static HBufC16 *NewL(TInt aMaxLength); IMPORT_C static HBufC16 *NewLC(TInt aMaxLength);示例代码:
1 LOCAL_C void HBufCFromNewLC(HBufC*& aBuf) 2 { 3 aBuf = HBufC::NewLC(19); 4 _LIT(KText, "Hello World My Girl!"); 5 6 TPtr ptr = aBuf->Des(); 7 ptr.Append(KText); 8 } 9 10 LOCAL_C void MainL() 11 { 12 HBufC* buf; 13 HBufCFromNewLC(buf); 14 console->Write(buf->Des()); 15 16 CleanupStack::PopAndDestroy(buf); 17 }按照字面理解,以上代码第3行申请了最大长度为19的堆内存,而字符串“Hello World My Girl!”的长度为20,程序能正常运行且没有抛出异常。为什么呢?SDK是这样解释的:
therefore, the resulting maximum length of the descriptor may be larger than requested.
HBufC预留的空间会比我们申请的空间大一些,如果我们将第三行代码换成aBuf = HBufC::NewLC(18);应用程序就会抛出异常。
- 使用HBufC::NewMax(L,LC)方法创建
HBufC::NewMax方法与HBufC::New方法类似,唯一不同的是,在使用New方法构建HBufC时,HBufC的Length为0,而使用NewMax方法,HBufC的Length会置为MaxLength。
示例代码如下:1 LOCAL_C void HBufCFromNewMaxLC(HBufC*& aBuf) 2 { 3 aBuf = HBufC::NewMaxLC(19); 4 _LIT(KText, "Hello World My Girl!"); 5 6 TPtr ptr = aBuf->Des(); 7 ptr.Copy(KText); 8 } 9 10 LOCAL_C void MainL() 11 { 12 HBufC* buf; 13 HBufCFromNewMaxLC(buf); 14 console->Write(buf->Des()); 15 16 CleanupStack::PopAndDestroy(buf); 17 }注意此处第7行使用的是Copy方法而不是Append方法。因为NewMax方法已经将Length设为19,如果再使用Append方法会使插入的数据超过最大缓冲区长度,从而抛出异常。
二、 堆描述符的内容扩展
当为HBufC所分配的现有内存不够用时,需要扩展HBufC的内容,我们可以使用ReAlloc函数对HBufC占用的堆空间进行扩展。ReAlloc方法将进行以下三个步骤:
- 在内存中创建一个新的基于堆描述符
- 将旧的堆描述符的内容复制到新的堆描述符
- 删除旧的堆描述符
从以上3点我们可以了解到,HBufC::ReAlloc方法会在堆中开辟另外一块内存空间,指向原来HBufC空间的指针将会被删除。所以,为了保证程序能正确运行,我们在调用ReAlloc的时候,不要忘记调用以下方法:
- 如果堆描述符被加入了清理栈,将堆描述符从堆中弹出
- 如果在ReAlloc之前使用Des方法获取了指向堆数据的指针描述符,在ReAlloc调用之后得重新获取
以下是示例代码:
1 LOCAL_C void HBufCReAllocLC(HBufC*& aBuf)
2 {
3 aBuf = HBufC::NewLC(20);
4
5 _LIT(KText, "Hello World My Girl!");