Symbian学习--内存管理、清除栈、两阶段构造、异常退出

(一)对象创建和释放

1)堆对象

Symbian OS中每个进程都有一个默认的的堆(Heap),此动态内存空间用来存放本进程中的一些对象:

·         占用空间比较大的对象,例如:长度大于256字节的字符串

·         需要运行时才创建的对象

·         对象生命周期与创建它的函数的生命周期不一致的对象

Symbian中需要存放在堆上的对象大多都是CBase的派生类对象,即C类对象。CBase基类为派生类实现了一些默认的基本操作,其中比较重要的操作是:

·         初始化操作:对象创建时将所有成员变量的值置0,指针值置为NULL

·         析构操作:CBase类将析构函数定义为虚函数,以便正确释放对象

·         重载new操作符(当然也可以用User::Alloc()在进程的默认堆上分配空间)

下面比较一下标准C++Symbian C++在对象创建和销毁上的不同:

假设有以下类:

 class CMyClass : public CBase { public: CMyClass(); ~ CMyClass(); void Function(); private: int iMember; } 标准C++的创建和销毁: Symbian C++的创建和销毁: CMyClass* ptr = new CMyClass; CMyClass* ptr= new(ELeave)CMyClass; If(ptr != NULL) ptr->Function(); { delete ptr; ptr->Function(); ptr = NULL; } delete ptr; ptr = NULL; 

标准C++中如果创建成功,那么ptr将保存对象在对上的地址值,iMember置为0。如果创建失败,那么new将返回NULLptr,这样在执行Function之前就可以通过if判断ptr是否有效。

Symbian C++中使用new(ELeave)方法创建对象时,如果内存分配失败,new(ELeave)将立即异常退出,而不执行Function,因此无需进行if判断也是安全的。这样设计带来的好处是可以减少代码量,同时也是为配合清除栈一起使用来减少内存泄露的可能。

注意:关于二次删除,所谓二次删除就是指重复使用delete操作删除同一个指针指向的内存空间中的内容。为什么会出现这样的情况呢?假设ptr指向内存空间A,使用完毕后delete ptr,这时ptr指向的内存空间A已被释放,并回收利用。如果由于某种原因(可能是编程逻辑错误),在ptr生命周期结束之前,delete ptr再次被执行,这时就有可能删除掉已经被回收利用的内存空间,但实际上此时空间A的内容已经不属于ptr所有。因此,为了避免二次删除,一个较好的习惯是每当delete一个指针之后,立即将其置为NULL,因为delete一个NULL的指针不会造成程序崩溃。

但需要提醒的是,在类的析构函数中使用delete释放成员变量指针后,将指针置为NULL是没有必要的。因为析构函数退出后,所有变量包括成员变量指针都将被销毁,因此即使指针没有被置空,也没有二次删除的危险。

 

2)栈对象

Symbian OS中每个线程负责管理自己的栈空间,默认每个应用程序有8KB栈空间。(可以通过在.mmp文件中做相应的设置来改变栈空间的默认大小,例如:epocstacksize 0X5000,将栈空间增大到20KB)。栈上分配的对象不需要手工申请和释放空间,但我们需要谨慎使用这些宝贵的空间,一般在栈上创建和释放的对象包括:

·         Symbian OS内置对象,例如:TintTReal等等

·         枚举类型对象

·         无需析构的类对象,例如:TBufTPtrC等等

 

(二)清除栈

PC机器上几KB的内存泄露可能微不足道,无需加以处理,但是对于Symbian面对的手持设备来讲,内存小且长时间不重启,那么内存泄露显得是如此致命。因此,为防止内存泄露,系统开发者设计了清除栈。

1)清除栈原理

下面是可能产生内存泄露的典型情况:

void CClass::FunctionL() { Tint integer; CMyClass* object = new(ELeave) CMyClass; … object->FunctionMayLeaveL(); … delete object; }

正常申请CMyClass对象堆空间后(如果堆空间申请失败,object会被栈空间管理进程安全删除),堆和栈的空间分配如下:

 

程序继续运行,到object->FunctionMayLeaveL();,如果此时发生异常退出,那么堆和栈的空间分配如下:

 

FunctionMayLeaveL()异常退出后,objectinteger被栈空间管理进程删除,但CMyClass对象在堆空间上的内存没有被释放,且我们已经失去了唯一指向CMyClass的指针object,因此这块内存在设备重启之前无法被再次分配和使用,造成了内存泄露。

使用了清除栈之后的情况如下:

void CClass::FunctionL() { Tint integer; CMyClass* object = new(ELeave) CMyClass; CleanupStack::PushL(object); … object->FunctionMayLeaveL(); … CleanupStack::Pop(); delete object; }

正常申请CMyClass对象堆空间后,堆和栈的空间分配如下:

 Symbian学习--内存管理、清除栈、两阶段构造、异常退出_第1张图片

FunctionMayLeaveL()异常退出后,objectinteger被栈空间管理进程删除,但此时我们并没有失去对CMyClass对象内存空间的控制权,因为我们可以通过清除栈上的object’来释放CMyClass对象的内存空间。

2)清除栈的使用

可以压入清除栈的对象有三种:

·         TAny*T类对象压入清除栈,当异常退出发生时,清除栈会对栈中指针调用User::Free()释放堆上空间而不是delete操作。

·         TCleanupItemR类对象压入清除栈,需要自行设置TCleanupItem类(TCleanupItem类实际上就是封装了需要压栈的指针和一个回调函数地址)(一般SDK中提供的R类都定义了因此我们只需继承R类就自动获得了这个类),当异常退出发生时,清除栈调用TCleanupItem类自行定义的回调函数,来完成一些特殊的清除操作。

·         CBase*:压入清除栈中的一切C类对象都将被清除栈视为CBase*变量,当异常退出发生时,清除栈会使用delete操作释放C类对象空间。

(三)两阶段构造

C++C在创建堆对象的时候,一个重要的区别就是:Cmalloc操作只是在堆上申请一块空间,而new操作在申请空间之后还会调用类的构造函数完成对象的构造。而构造函数如果异常退出,则有出现内存泄露的危险。请看以下例子:

class CClass1 : public CBase { CClass1(); ~CClass1(); private: CClass2* iClass2; } CClass1::CClass1() { iClass2 = new(ELeave)CClass2; } CClass1::~CClass2() { delete iClass2; }

下面开始创建CClass1的对象:

CClass1* object = new (ELeave) CClass1; CleanupStack::PushL(object); … CleanupStack::PopAndDestroy;

从以上代码可以看出,当创建CClass1对象的时候,假设申请CClass1对象的堆空间成功,然后调用CClass1的构造函数,继续申请CClass2对象的堆空间,此时如果失败,CClass1的构造函数异常退出,遗憾的是此时我们还没有来得及将object放入清除栈,CClass1对象的堆空间又成了一块无法访问的内存,又是一次内存泄露!

Symbian是如何避免这种情况的呢?那就是让构造函数绝对不会异常退出!即将可能异常退出的函数分离出来放到一个专门的函数中执行,这个函数通常命名为ConstructL

class CClass1 : public CBase { CClass1(); ~CClass1(); Void ConstructL(); //存放可能异常退出代码的函数 private: CClass2* iClass2; } CClass1::CClass1() { //不放置任何代码最安全,即使放置也不能有可能异常退出的代码 } Void CClass1::ConstructL() { iClass2 = new(ELeave)CClass2; } CClass1::~CClass2() { delete iClass2; }

下面开始创建CClass1的对象:(两阶段构造)

CClass1* object = new(ELeave)CClass1; CleanupStack::PushL(object); Object->ConstructL(); … CleanupStack::PopAndDestroy;

有了两阶段构造,在创建堆对象时,可能出现内存泄露的风险就没有了,同时,Symbian为了规范代码统一风格,将两阶段构造封装成NewLNewLC两个静态工厂函数,因此我们在编写代码时也需要遵守这样的规则。

class CClass1 : public CBase { public: static CClass1* NewL(); static CClass1* NewLC(); ~CClass(); private: CClass1(); Void ConstructL(); private: CClass2* iClass2; } Void CClass1::ConstructL() { iClass2 = new(ELeave)CClass2; } CClass1* CClass1::NewLC() //L表示此函数可能异常退出,C表示函数执行后没有弹出清除栈的操作 { CClass1* self = new(ELeave) CClass1; CleanupStack::PushL(self); Self->ConstructL(); return self; } CClass1* CClass1::NewL() { CClass1* self = CClass1::NewLC(); CleanupStack::Pop(); return self; }

(四)异常处理

1)抛出异常

User静态类中定义了几个常用的抛出异常函数:

  • static void Leave(TInt aReason);
  • static TInt LeaveIfError(TInt aReason);
  • static TAny* LeaveIfNull(TAny* aPtr);
  • static void LeaveNoMemory();

static void Leave(TInt aReason):自身没有条件判断,直接抛出异常并返回错误代码aReason

void CMyClass::DivideL(TInt aValue) { If (aValue == 0) { User::Leave(KErrDivideByZero); } … }

static TInt LeaveIfError(TInt aReason)aReason如果是负值,则抛出异常并返回aReason

 

RFs fsSession; User::LeaveIfError(fsSession.Connect());

static TAny* LeaveIfNull(TAny* aPtr)aPtr为空时,抛出异常,异常错误代码是KErrNoMemory。通常此函数用来判断参数是否是有效指针或者申请堆空间是否成功。

void CMyClass::FunctionL(CAnyClass* aObjPtr) { User::LeaveIfNull(aObjPtr); … CExample* myExample = new CExample; User::LeaveIfNull(myExample); }

但我们通常用断言宏__ASSERT_ALWAYS__ASSERT_DEBUG来判断函数参数是否有效。由于new(ELeave)在创建对象失败时抛出一个KErrNoMemory的异常。所以以上代码可改写为:

void CMyClass::FunctionL(CAnyClass* aObjPtr) { //第一个参数返回EFalse,那么执行Panic函数直接退出程序且弹出对话框提示KMyPanicText信息 __ASSERT_ALWAYS(aObjPtr!=Null, User::Panic(KMyPanicText, KMyErrorCode)); … CExample* myExample = new(ELeave) CExample; … }

static void LeaveNoMemory():只是简单的异常退出,但异常退出码被硬编码成KErrNoMemory,它的效果等同于调用User::Leave(KErrNoMemory)

void CMyClass::FunctionL(CAnyClass* aObjPtr) { CExample* myExample = new CExample; if(Null==aObjPtr) { User:: LeaveNoMemory(); } … }

2)捕获异常

Symbian使用TRAPDTRAP两个宏来捕获异常,标准用法如下:

 

TRAPD(error, DoStartL()); __ASSERT_ALWAYS(!error, User::Panic(KAppName, error));

TRAP的使用方法相同,只是使用之前,必须自己定义TInt error变量。

注意:在一些不允许异常退出的函数中,有时我们又不得不加入一些可能异常退出的代码,这时我们可以用TRAPD捕获这些异常加以处理,这样就可以不影响不允许异常退出的函数的运行。

 

参考文献:基于Symbian OS的手机开发与应用实践     清华大学出版社

 

你可能感兴趣的:(object,user,null,Integer,delete,Symbian)