C++BUILDER中几种容器的使用

类别:编程语言 点击:0 评论:0 推荐:
 

C++BUILDER中几种容器的使用

                             -------BCB中控件数组的实现

C++BUILDER是Borland公司基于C++的快速开发工具,它简单的使用方法和强大的功能一直深受很多编程人员的好评。C++BUILDER(以下简称BCB)的组件库跟DELPHI一样,都是VCL。跟微软的MFC不同,VCL完全是用Object Pascal语言编的。也因此,使BCB同时获得了Pascal和C++的强大功能。

介绍在BCB中实现控件数组的文章不少,但是实现方法上,大多用VCL自带的Tlist类实现。实际上,除了这种方法,在BCB中实现控件数组还是有不少其他方法的。这里,我就谈谈这几种实现控件数组的方法,实际上,也是对BCB中常用的容器做一个小结。

BCB完全支持VCL和标准C++,这就导致了存在有VCL版本和标准C++两个版本的容器。在这里,我会给出三种实现方法,一种是VCL的TList方法,一种是VCL的DynamicArray(动态数组)方法,还有一种就是标准C++的STL实现方法。相信这些基本已经涵盖了BCB中常用的几种容器类型。

我设计了一个例子来演示我说的容器以及它们的使用方法。

首先先让我介绍一下我的例程。很简单却很有代表性的一个小程序,下面是它的界面:

  你可以看到,在这个例程里,我们管理了一组TImage控件(就是那里面的“山”图标),我们可以往我们这堆“山”里随意添加新的“山”(会以随机的形式在Panel中出现),我们还可以删除任意一个我们不想要的“山”。同时,我还提供了“全部清除”,可以用遍历的方式全部清除所有的“山”。我要讲的这三个容器的都是以这个程序作为例程,外表上唯一的不同就是这个窗体的标题会因例而异。

我就先说说大家用的最多的Tlist类。

VCL提供的Tlist类实现了可以动态存储的线形链表对象。它实际上是以存储指针(链式存储)的方式实现的,但它同时提供了顺序存储的查找方式。可以通过Items属性像访问一个数组那样访问Tlist对象的每一项。除此之外,Tlist类提供方便全面的链表功能。例如,全套插入删除查找排序的对象方法以及包含Count属性访问链表中项目的个数。

概念说完了,我们就来看看我这个TList版本的“今天能看见山”是怎么实现的。窗体是再简单不过了,无非是一个Form,一个Panel,一个ListBox,三个Button而已。这几个控件的简单堆砌我就不在赘述了。

TList是一个容器,但是它本身也是一个类,因此我们在使用之前也得先创建这个类。为了简单起见,我把这个TList类的指针定义成了一个私有指针以便这个类中的所有函数都能方便地访问到它。因此我在头文件的Private段里有如下定义:

TList *HillList;

接下来我们再定义三个私有函数用来响应三个不同的按键。于是我的Private段中又多了这样一些东西:

void __fastcall AddHill();

void __fastcall DeleteHill();

void __fastcall ClearAll();

好了,现在我们该来关系函数的具体实现了。首先,我得让我的容器创建起来,于是我在窗体的构造函数中执行了:

HillList = new TList();

HillList记载了指向这个容器的指针。下面我给出函数实现的原代码并在其中加以说明。

/**往控件数组中增加成员**/

void __fastcall TfrmHill::AddHill()

{

TImage *temImage;                  //定义临时TImage指针

HillList->Add(new TImage(this));       //构造新Image对象并把它加入控件数组内

HillListBox->Items->Add("A Hill");     //向ListBox中写入“A Hill”

temImage = (TImage *)HillList->Items[(HillList->Count-1)];  //获取刚构造的Image对象

temImage->Picture->LoadFromFile("hill.ico");             //装入图片

temImage->Top  =  random(HillPanel->Height);          //将新Image的位置定为

temImage->Left  =  random(HillPanel->Width);          // Panel中的随机值

temImage->Parent = HillPanel;                         //此句必不可少,用来说明

}                                                 // Image是在Panel中出现。

//---------------------------------------------------------------------------------------------------------------------

/**删除控件数组中选定的成员**/

void __fastcall TfrmHill::DeleteHill()

{

if(HillListBox->ItemIndex>=0)       //判断ListBox中是否有选中的对象     

{

  TImage *temImage = (TImage *)HillList->Items[HillListBox->ItemIndex];  //装入临时指针

  delete temImage;                                              //删除指针所指对象

  HillList->Delete(HillListBox->ItemIndex);                //删除控件数组中的指针记录

  HillListBox->Items->Delete(HillListBox->ItemIndex);      //在ListBox中删除一个对象

}

}

注意,在上面删除对象的操作中,我们先得到指向控件数组中对象的指针,然后用delete操作符调用对象的析构函数销毁该对象,这一步千万不能省,这就是一个可能发生内存泄露的机会。接着,还得在控件数组中删除指向该对象的指针,这样一来,如同诛灭九族一般所有跟这个对象有关系的就全部被销毁了。

//---------------------------------------------------------------------------------------------------------------------

/**清空控件数组**/

void __fastcall TfrmHill::ClearAll()

{

  TImage *temImage;                //建立临时指针

  for(int i = 0;i<HillList->Count;i++)    //便历控件数组

  {

    temImage = (TImage *)HillList->Items[i];    //将临时指针指向待操作对象

    delete temImage;                        //销毁该对象

  }

  HillList->Clear();       //清空控件数组,此时控件数组内的所有指针指向的对象

//都已经被销毁,如果在这时访问这些地址,将会引起一

//个AV错误

  HillListBox->Items->Clear();         //清空ListBox

}

删除全部对象,同样也需要两步,因此我先遍历控件数组,销毁里面的所有对象,然后再清空控件数组,如此才真正实现了清空。

主要函数讲完了,这个时候还有一个工作要做,别忘了,我们的容器类是动态申请的,这样一来,跟其他的类一样,我们的这个容器也需要销毁,这样,我们必须在窗体的析构函数中加入:

delete HillList;

以上这些就是用TList实现控件数组的方法。可以看到,这个方法很简单,数组以指针的形式储存(TList里也只能以指针的形式存储)。添加和删除对象都很方便且有效率。但是,TList却不是一个类型安全的类,它的指针都是以void类型存放的。所以我们也可以看到在调用其中的对象的时候我们都使用了强制类型转换。因为这个原因,就为我们的程序带来了不安全的隐患。还有,当它存储5000以上的对象时,效率会有很大的下降。TList类是VCL定义的类,因此它的可移植性就很差。

下面我们介绍一个类型安全的容器,这就是VCL提供的动态数组(DynamicArray)。VCL的动态数组是以类的方式实现的。它提供了DynamicArray类模板,使用这个类模板可以声明实际动态数组类。动态数组通过设置Length值可以在运行时动态改变数组元素个数。而且它还重载了C风格数组的“[]”运算符,所以就可以像访问普通数组那样访问动态数组。

下面我为你展示“今天能看见山”的DynamicArray实现版本。

使用动态数组,我们就要先声明这个容器。在头文件的Private段中:

DynamicArray<TImage *>HillDA;

然后是那三个重要函数:

void __fastcall AddHill();

void __fastcall DeleteHill();

void __fastcall ClearAll();

这都跟我们的第一个版本没有什么区别。下面来看看程序的实现。下面的程序中有很多操作跟第一个版本中相同的就不再解释了。

 

因为动态数组是一个模板类,因此在使用前我们不需要先创建它的对象。

//---------------------------------------------------------------------------------------------------------------------

/**往控件数组中增加成员**/

void __fastcall TfrmHill::AddHill()

{

TImage *temImage;                   

HillDA.Length+=1;                       //动态数组长度加一给新对象留出空间

HillDA[HillDA.High] = (new TImage(this));   //将新空间装入一个指向新创建的Image

//对象的指针

HillListBox->Items->Add("A Hill");           //向ListBox中写入“A Hill”

temImage = HillDA[HillDA.High];            //获取刚构造的Image对象

temImage->Picture->LoadFromFile("hill.ico");  

temImage->Top  = random(HillPanel->Height);

temImage->Left = random(HillPanel->Width);

temImage->Parent = HillPanel;

}

//---------------------------------------------------------------------------

/**删除控件数组中选定的成员**/

void __fastcall TfrmHill::DeleteHill()

{

if(HillListBox->ItemIndex>=0)

{

  delete HillDA[HillListBox->ItemIndex];                //销毁选中的对象

  for(int i = HillListBox->ItemIndex;i < HillDA.High;i++)   //被销毁对象后的记录一

{                                                //一前移覆盖被删除对象的

HillDA[i] = HillDA[i+1];                           //指针

  }

  HillDA.Length-=1;                                  //控件数组总长度减一

  HillListBox->Items->Delete(HillListBox->ItemIndex);    

}

}

//--------------------------------------------------------------------------------------------------------------

/**清空控件数组**/

void __fastcall TfrmHill::ClearAll()

{

  for(int i = 0;i<HillDA.Length;i++)     //同样遍历控件数组销毁所有的类

  {

    delete HillDA[i];

  }

  HillDA.Length = 0;                //将控件数组的Length设为0以释放该控件数组

  HillListBox->Items->Clear();

}

//--------------------------------------------------------------------------------------------------------------

DynamicArray的方法讲完了,细心的读者看出什么来了吗?如果我再告诉你一个事实,你一定会看出来的。DynamicArray是以连续空间存储的,它实现长度在运行时改变的机制是这样的:当你在运行的时候改变了它的长度后,它将依据新长度从新分配一块内存,然后把原来的数据全部Copy到新的地址中去。我的天~~~~,这样你总知道了吧,我们这个DynamicArray的控件数组开销太大了,在我们新增和删除的时候,它总是在不停的Copy来Copy去,在对象少的时候还不明显,当对象变的很多的时候,这样的系统开销实在让人难以接受。

那好,前两种方法都有一定的缺陷,那我来讲讲第三种方法:STL方法!

Standard Template Library(STL)是C++标准库的一部分,它定义了一组容器和对容器进行操作的算法。这些算法都是具有很高效率的。Borland C++ Builder 6内置了STL的执行器“STLport 4.5”。通过使用这些泛型的容器和算法,可以使我们的程序更健壮而且效率大大提高。

根据控件数组的要求,我准备选用STL中的vector容器。Vector为内置数组提供了一种替代表示。它同样采用连续的内存空间存放数据。这样的结构就造成了它随机访问的效率很高但是在它的任意位置,而不是vector末尾插入数据,则效率很低(这一点到跟我们的DynamicArray差不多,只是DynamicArray在末尾插入数据时效率一样低),那vector会不会就跟DynamicArray一样了?当然不会,vector在末尾插入数据的时候要比DynamicArray效率高的多,因为它使用了自增长机制。就是说,为了提高效率,实际上vector并不是随每  元素的插入而增长自己,而是当vector需要增长自身时,它实际分配的空间比当前分配的空间要多一些,也就是说,它分配了一些额外的内存空间,或者说它预留了这些存储区(分配的额外空间的具体数目由具体实现定义)。我们这个存放指针的vector,在第一个元素被插入的时候容量就会扩展到256。而当我们插入的元素到了256个的时候,它就会申请双倍于现在容量的空间,把原有的值拷贝到新分配的内存空间中,释放原来的内存空间。

无论如何,如果你准备往你的控件数组中插入的对象在(至少在)一千万个以内,vector的效率都是最高的(跟其他STL容器和其他传统形容器比)。但是,如果你对控件数组内元素的操作要求很苛刻,那你可以选用STL中的List容器。我对这个要求没那么苛刻,所以我的程序用vector来实现。

在BCB中要实现vector,别忘了包含它的头文件:

#include "HillSTL.h"

接着我们还要声明我们的名字空间:

using namespace std;

在头文件的Private段中定义vector:

vector <TImage *> HillVec;

三个函数:

void __fastcall AddHill();

void __fastcall DeleteHill();

void __fastcall ClearAll();

//--------------------------------------------------------------------------------------------------------------

/**往控件数组中增加成员**/

void __fastcall TfrmHill::AddHill()

{

TImage *temImage;

HillVec.push_back(new TImage(this));           //在控件数组中加入新对象的指针

HillListBox->Items->Add("A Hill");             

temImage = *(HillVec.end() - 1);                //取得刚加入对象的指针

temImage->Picture->LoadFromFile("hill.ico");

temImage->Top  = random(HillPanel->Height);

temImage->Left = random(HillPanel->Width);

temImage->Parent = HillPanel;

}

//--------------------------------------------------------------------------------------------------------------

/**删除控件数组中选定的成员**/

void __fastcall TfrmHill::DeleteHill()

{

if(HillListBox->ItemIndex>=0)            

{

  TImage *temImage = *(HillVec.begin() + HillListBox->ItemIndex);

  delete temImage;

  HillVec.erase( HillVec.begin() + HillListBox->ItemIndex);  //删除控件数组中指定的值

  HillListBox->Items->Delete(HillListBox->ItemIndex);

}

}

//--------------------------------------------------------------------------------------------------------------

/**清空控件数组**/

void __fastcall TfrmHill::ClearAll()

{

  vector <TImage *> :: iterator iter;          //定义迭带器

  for(iter = HillVec.begin();iter !=HillVec.end();iter++)   //遍历销毁对象

  {

     delete *iter;

  }

 

  HillVec.erase(HillVec.begin(),HillVec.end());          //清空vector

  HillListBox->Items->Clear();

}

好了,这就是我们的第三种方法了,这种方法相对而言是最好的,其强壮性好,可移植性强。但其一点不足就是在容器内进行操作的时候效率不佳。而STL的List在随机访问的时候效率又不好了。可见,世上万物,无一完美啊!你知道更好的方法吗?来告诉我。

上面的三种方法中,你可以看到,除TList只能存储指针外,DynamicArray和vector都是可以存储任意类型的容器,那我们为什么还非要存储指针呢?是这样的,如果我们存储了类,则实际上是存储了对象的副本,这时,当我们进行插入操作的时候,对象就会调用自己的拷贝构造函数以生成对象的副本。这样的效率就又降低了,因此我们以指针形式存储,这样的设计方式同样增加了程序处理的效率。

以上我结合控件数组的实现介绍了BCB中常用的三种容器。你可以在实际应用中考虑实际情况选择适合自己的容器使用。

本文中所有程序均在Windows2000+BCB5下调试通过。

  如对本文所谈技术有任何问题,欢迎跟我讨论,地址:[email protected]

北京联合大学信息学院  张琦

本文地址:http://com.8s8s.com/it/it29460.htm