手机应用程序开发

实验三:Carbide C++

引言

在本实验中,我们将熟悉Carbide.C++语言的使用和原理。主要论题包括:

1.内存管理

2. 基于ERC的“异常处理”

实验准备:新建工程向导

1.打开Carbide.c++ v2.0,一个短暂的初始化过程之后,Carbide.C++窗口就会显示出来了。选择“File->New->Symbian OS C++ Project”,弹出“New Symbian OS C++ Project”对话框,选择S60下的Open C Console Application,如下图所示。

2.单击“Next”输入Project name,然后一直默认单击“Next”直到最后单击“Finish”,这样一个Open C控制台应用程序就建成了,这时会发现窗口左边的上面Project Explorer和下面Symbian Project Navigator都多了一个自定义工程名文件夹。这里取工程名为“Test”,如下图所示。

3.点击窗口左边上面Project Explorer中的Test前面的+号,再点击其中src子文件夹前面的+号,这时会发现一个Test.cpp文件,双击它,我们要实现控制台应用程序的某些功能都在这个文件里进行添加或修改,如下图所示。

第一部分:内存管理

1.栈上的对象和堆上的对象

    在Symbian中,如果对象是在栈中声明的,那么在该对象离开作用域时就会被自动删除。Symbian规定栈的默认大小是8KB。

    如果对象是在堆空间中动态分配的,那么程序开发者就需要手动对资源进行释放,这可以通过使用delete命令实现。如果出现内存泄露,那么堆空间的大小会随着可用内存的多少而变化,并且应该至少大于0.5MB。

    下面举例说明两种声明方法以及对象在内存空间的存储形式,如下图所示。

实验1.1 根据实验二中第四部分继承性的设计,如下图所示,在阅读完上面说明材料的基础上,参考下面的例子,完成标准C++到Symbian OS C++的转换。

比如:

2.异常处理

(1)抛出异常

(2)捕获异常

实验1.2 根据实验二中第五部分异常的设计,如下图所示,在阅读完上面说明材料的基础上,完成标准C++到Symbian OS C++的转换。

3.清理栈

实验1.3 如下图所示,参考下面的例子,完成清理栈。

比如:

4.二阶段构造

    二阶段构造提出的目的是为了解决构造函数的资源分配问题,下面先举个简单的例子:

    其中,CMyAppAppUi对象是在CMyAppDocument::CreateAppUiL()中创建的,这在后面图形化界面设计时会详细讨论。如果CMyAppAppUi的内存空间在CMyAppDocument::CreateAppUiL()成功分配,但是在iAppContainer时发生Leave,那么指向CMyAppAppUi对象内存空间的指针就将丢失,从而造成一段内存空间无法访问,引起内存泄露。正是为了解决这一问题,Symbian提出了二阶段构造的方法,它的基本思路就在于将不会发生异常的代码放在普通的构造函数中,把可能发生异常(动态内存资源分配)的部分放在二阶段构造函数中。例如上面的例子,就可以引入一个二阶段构造函数来解决问题。

    同时,为了简化二阶段构造过程,可以使用静态函数NewL()和NewLC()的形式:

    可以看到,NewL()函数调用NewLC()函数,然后NewLC()函数调用普通构造函数,然后再调用二阶段构造函数,从而保证了不会产生内存泄露。“C”的含义就是压入清理栈。这里需要说明的是写代码的规范问题,建议所有同学自己定义的C类都应该定义NewL()和NewLC()函数,并且都是公有静态函数,同时定义私有的二阶段构造函数和C++构造函数。这样做的好处是可以增加程序的可读性,使代码维持更加方便。将C++构造函数设为私有是为了防止类的使用者跳过二阶段构造函数而直接运行C++的构造函数。但是,在少数情况下,比如C类是抽象类,不能实例化,那么就不能定义NewL()或NewLC()函数,但需要定义私有或保护属性的二阶段构造函数BaseConstructL()和C++构造函数。

    下面详细说明一下类的构造和析构中的一些原则问题。

    1.构造方面

    1)C++默认构造函数不能包含可能发生Leave的代码,比如:

    2)可能发生Leave的函数必须通过二阶段构造函数来调用,比如:

    3)构造函数的形参和成员变量的初始化可以由两个构造函数的任何一个来完成,而且基类C++构造函数是按传统c++的方式进行的,比如:

    4)如果基类有二阶段构造函数ConstructL(),那么在派生类的构造函数ConstructL()中必须调用它,并且通常是在最开始的时候进行调用,比如:

    2.析构方面

    1)C类应该在其析构函数中删除自己的对象,由于删除空指针没有什么影响,所以不用进行条件判断,但是由于习惯,在很多工程中都可以看到类似的代码。

    2)在删除对象后,应该将其指针设为空(NULL)。

    3)不要删除不归某个类所有而且使用很少的对象,这在观察类接口进行指针传递的时候很典型,比如:

    可见,iObserver并不是该类所有的,所以在析构函数里将其设为空指针就可以了,不能删除。

    4)在删除对象并将其指针设为空之后,再进行内存的分配。假设有一个类CMyClass有一个对象是在堆上分配的空间,然后它需要更新数据。

    如果两次调用UpdateObject(),那么就会两次分配内存,从而造成内存泄漏,如果要纠正这个错误,就需要把代码改为:

    但是,这样修改之后还有一个问题,那就是如果对象创建失败,也就是NewL()发生了Leave,iObject将依然指向先前分配的对象,这样如果再执行删除操作,就会造成二次删除从而发生严重的错误。所以,为了解决这一问题,还需要对代码进行修改:

实验1.4 根据实验二中第三部分构造函数和析构函数的设计,如下图所示,在阅读完上面说明材料的基础上,参考下面的例子,完成标准C++到Symbian OS C++的转换。

比如:

第二部分:基于ERC的“异常处理”(选做题,不作要求)

阅读并运行如下的代码,并回答问题:

// Compile under both MS VC 6.0 and Carbide C++
#include 

class Env
{
  int i0;
  int i4;
  int i8;
  int i12;
  int i16;
  int i20;
};

typedef Env JmpEnv[1];

JmpEnv globlEnv;

int __declspec(naked) mySetJmp (JmpEnv env)
{
  __asm
  {
    mov   eax, [esp+4]
    mov   [eax], ebx
    mov   [eax+4], edi
    mov   [eax+8], esi
    mov   [eax+12], ebp
    mov   [eax+16], esp
    mov   ecx, [esp]
    mov   [eax+20], ecx
    xor   eax, eax
    ret
  }
}

void __declspec(naked) myLongJmp (JmpEnv env, int val)
{
  __asm
  {
    mov   edx, [esp+8]
    cmp   edx, 0
    jne   L
    inc   edx
L:
    mov   ecx, [esp]
    mov   eax, [esp+4]
    mov   ebx, [eax]
    mov   edi, [eax+4]
    mov   esi, [eax+8]
    mov   ebp, [eax+12]
    mov   esp, [eax+16]
    mov   eax, [eax+20]
    mov   [esp], eax
    mov   eax, edx
    ret
  }
}

int main ()
{
  printf ("entering main ()\n");

  switch (mySetJmp (globlEnv))
  {
  case 0:
    printf ("0\n");
    myLongJmp (globlEnv, 1);
    break;
  case 1:
    printf ("1\n");
    myLongJmp (globlEnv, 0);
    break;
  case 2:
    printf ("2\n");
    break;
  default:
    printf ("other cases\n");
    break;
  }
  
  printf ("leaving main ()\n");

  return 0;
}
问题:
  1. 请解释该程序的行为。
  2. 研究可以如何把该程序用作C或C++的异常处理。
  3. 该机制能替换标准C++的异常处理机制么?为什么?