分享

MFC中的集合类

 学海无涯GL 2013-09-17

STL中的集合类数组、列表、关联,MFC中也有相似的类,下面列出MFC中的一些使用方法。

实际上集合类就是数据结构中的顺序存储结构和链表存储结构。它的优点就是建立这样的存储结构简单,并且把相关的操作集中成函数,方便用户的调用。比如顺序存储用到数组,如果要在数组中的固定位置添加一项,普通的方法就是要用一个for循环,有点烦琐。而在集合类中只要用一个add函数就完成了,相对简单一点。

 

数据结构是软件设计中的一个重要组成部分。因此我们有必要搞清楚集合类的相关问题。

 

1.3种类型

集合类中有3种大的类型:

(1)         Array: 数组,可以动态的改变大小,有索引值和最大下标等。

(2)         List:双向链表,无索引,链表有头尾,插入元素要比数组快。

(3)         Map:是一种映射,俗名“字典”,是一种关联式数组。(在此不做讨论)

 

2.各自特点:

类型

是否有序

插入元素速度

搜索元素速度

索引

Array

List

 

3.MFC中的集合类

是否使用模板

是否支持串行化

是否支持倾印

CArray

CTypedPtrArray

可能

CByteArray

CDWordArray

CObArray

CPtrArray

CStringArray

CWordArray

CUIntArray

CList

CTypedPtrList

可能

COblist

CStringList

 

 

4.几点说明

(1) 有些集合类是从c++模板支持的,如CArray,CList,使用时必须指出所要收集对象的类型,比如int,char,CPoint等,可以参考一下c++模板的知识。

 

(2)在表中有2个“可能“,因为CTypedPtrArray,CTypedPtrList这些类在使用时要指定基类,如果基类可以串行化,那么它就可以串行化。

 

(3)在深入浅出MFC中,作者说CUIntArray是可以串行化的,但我在做项目时证明它是不可以串行化的,希望读者注意

如一个对象类:

        Void  CStudent::Serialize(CArchive &ar)

 {

                   ……

        m_array.Serialize(ar);//这里m_array是CUIntArray的对象

     }

可是运行后发现并没有成功。因此只好采用for循环:

        Void  CStudent::Serialize(CArchive &ar)

   {

                   If(ar.IsStoring( ))//存储

                   {

                            ar<<m_array.GetSize();

                            for(int i=0;i<m_array.GetSize();i++)

               ar<<m_array.GetAt(i)

       }

       …..//读取略

}

 

5.常用函数举例

(1) 数组

         如int 数组:

                  CArray<int,int>  m_intArray;

                   m_intArray.Add(15);   // 添加一个元素

                   CArray<CPoint,CPoint> pArray;

                   pArray.Add(CPoint(10,10));

 

       添加元素

注意,此时开始并没有分配数组的存储空间,但是add可以动态分配空间。如果可以预计数组大小,可以先用SetSize()来分配空间,因为如果频繁使用add,会产生内存碎片。SetSize可以增加数组元素,也可以减少,但是在减少时,并不会自动缩小保存数组数据的缓冲区,还是先调用removeAt先把元素删掉。

 

推荐使用:SetAtGrow(int index,ARG_TYPE newElement ),它与Add相比,就是可以利用它修改数组中的数组;而如果用add,那么必须先RemoveAll

 

       获得元素个数和最大下标

GetSize():可以返回数组中元素的个数。

GetUpperBound():返回数组中的最大下标,一般加1就和GetSize()相等。

 

       获得成员值

一般可以用GetAt(),有时可能要强制类型转化。如:

CObArray  array;

  Cline *pline=new Cline(100,100,200,200);//Cline为直线类,用起点和终点坐标初始化

 array.Add(pline);//此处存储的只是一个地址

 Cline *p=(Cline*)array.GetAt(0);//必须强制类型转换

 

注意这里必须使用new动态创建内存空间。如果是局部变量,等到函数结束,就不能够再通过数组来引用这块内存了,因为已经析构。

当然也可以不用类型转化。此时可以用集合类CTypedPtrArray,例如


  CTypedPtrArray<CobArray,Cline*) array ;//表示array是CobArray对象,专门存储Cline*指针

  Cline *pline=new Cline(100,100,200,200);//Cline为直线类,用起点和终点坐标初始化

  array.Add(pline);

  Cline *p=array.GetAt(0);//不用强制类型转换

 

       修改成员值

 

一般可以使用函数SetAt(),但是在修改之前,这个元素的内存空间必须已经分配,比如下面这样写是错误的:

  CUIntArray  Array;

  Array.SetAt(0,10) ;//想把第一项修改为10;

可以这样写来修改元素的值:

   CUIntArray  Array;

  Array.Add(5) ;

  Array.SetAt(0,10) ;//把第一项5修改为10;

另外用数组成员引用符号[]来表示也可以来修改或者是获得元素值。

   CUIntArray  Array;

  Array.Add(5) ;

Array[0]=5 ;//把第一项5修改为10;

同样 这样写是错误的:

  CUIntArray  Array

  Array.Add(5) ;

  Array.GetAt(0)=5 ;//想把第一项5修改为10;

     删除元素

RemoveAt():删除一个元素,删除后,数组中的索引号会自动改变。

RemoveAll():删除所有元素。

注意如果要同时删除几个元素,必须从后面删除起。

比如:

CUIntArray  Array;

    Array.Add(5) ;

Array.Add(10);

Array.Add(13);

Array.RemoveAt(0);

Array.RemoveAt(1);

本来是要删除第1个元素和第二个元素,但结果却是把第一个元素和第三个元素删除了,因为当调用Array.RemoveAt(0);删除第一个元素后,索引号开始变化,元素10成为索引要从1变为0,而元素13的索引号从2变为1。解决这个问题可以从后面开始删除:

CUIntArray  Array;

    Array.Add(5) ;

Array.Add(10);

Array.Add(13);

Array.RemoveAt(1);

Array.RemoveAt(0);

只是改变了顺序,效果却大不相同哦!

 

 

 

(2) 链表

 

       添加成员

Clist<int,int> m_intlist ;

 m_intlist.AddTail(36) ;//在尾部添加

m_intlist.AddHead(34) ;//在头部添加

 

       遍历

POSITION pos=m_intlist.GetHeadPosition() ;

While(pos !=NULL)

{

           int i=m_intlist.GetNext(pos) ;

}//数组中有索引号,要方便一些。

 

其他用法数组差不多,不作介绍。

 

 

6. 动态创建的含义

 

举例说明:

还是有个直线类,现在要串行化保存。

 

         添加元素

Cline *line =new Cline(100,100,200,200);

m_array.Add(line); //CObArray m_array;  类中的成员变量

 

          

在文档类中保存和读取

 Void CMyDocument::Serialize(CArchive & ar)

{

         m_array.Serialize(ar);

}

 

 

实际上在m_array.Serialize(ar);的内部实现用到了for循环,一个一个元素的存储和读取的。

前面已经说过line必须是动态分配空间的,所以存储是可以理解的。但是如果关闭这个程序,所有内存全部释放,此时再次运行程序,打开文件,肯定会调用m_array.Serialize(ar);,在内部又是采用for循环一个一个读取,但是这里并我并没有动态分配空间,它从磁盘读取的内容(Cline)到底存在哪里啊?

 

在深入浅出MFC中给出了很好的说明。在调用CArchive中这个重载符号”>>”,也就是读取时,内部会再次调用一系列的函数,有一个是CreateObject(),它的代码就是

CObject *Cline ::CreateObject()

{

return new Cline;

}

 


许多C++程序员都使用标准模板库(STL),因为用它很容易实现数组、链表、映射以及其它容器。STL语言中“容器”指的是保存“数据集合”的对象。但是在有STL之前,已经有MFC了。在称为“MFC集合类”的一系列类中,MFC提供了自己的数组、链表、以及映射的实现途径。虽然在MFC中使用STL非常安全,但许多MFC程序员还是更喜欢用MFC集合类,一方面原因是更熟悉MFC,另一方面原因是不愿意链接2个独立的类库增加应用程序的exe的尺寸。
STL中的集合类跨平台好,有较多的泛型算法。

1、数组

      C和C++的一个最大缺陷是数组不进行边界检查,如下代码,它反映了C和c++应用程序中最常见的一种错误:

 

此代码出错是由于for循环中的最后一次迭代赋值超出了数组的范围。在运行时会产生非法存取错误。

   C++程序员经常通过编写数组类并在类内部进行边界检查来解决此问题。下面给出的数组类有Get和Set函数,用了检查传递给它们的下标,如果传递来的下标无效就进行断言处理:

 
这样就会避免非法存取错误的发生。

1.1MFC数组类

      你不必亲自编写数组类,MFC已经提供了各种各样的数组,首先是一般的CArray类,它实际上是一个模板类,利用它可以创建任何数据类型的“类型安全数组”。在头文件Afxtempl.h中定义了CArray。其次是非模板化的数组类,分别为保存特定类型的数组而设计。这些类在Afxcoll.h中定义,下面说明了非模板化的数组类以及它们所保存的数据类型:

CByteArray          8位字节(BYTE)

CWordArray        16位字节(WORD)

CDWordArray      32位双字节(DWORD)

CUIntArray          无符号整型(UINT)

CStringArray        CString

CObArray             CObject指针

CPtrArray             void指针

      只要学会使用这些数组类中的一种,也就会用其它数组类了,因为它们共享公共的一组成员函数。下例声明一个包含10个UINT的数组并用数字1-10对它进行了初始化:

 
在这两个例子中,都是用SetSize来指定数组包含10个元素;重载[]运算符调用数组的SetAt函数,该函数将 值 复制到 数组中 指定位置处的 元素 中;如果数组边界非法,程序将执行断言处理。边界检查内置在SatAt代码中:ASSERT(nIndex>=0 && nIndex<=m_nSize);在MFC源程序文件Afxcoll.inl中可以看到此代码。

      可以使用InsertAt函数在不覆盖已有数组项的情况下给数组插入元素项。与SetAt不同,SetAt只是给已存在的数组元素赋值,InsertAt还要给新的元素分配空间,通过把插入点后面的元素向后移动来完成。InsertAt是那些便于使用的函数之一,它们在新的元素项添加到数组中时指定增加数组尺寸。使用[]运算符将调用GetAt函数,该函数将从数组中的指定位置取回一个值(当然要进行边界检查)。如果愿意可以直接调用GetAt而不是通过[]运算符。

      要确定数组包含元素的个数,可以调用数组的GetSize函数,还可以调用GetUpperBound返回数组的上界 下标,因为下标从0开始,所以其值为数组元素总数减1。

      MFC的数组类为从数组中删除元素提供了2个函数:RemoveAt和RemoveAll。RemoveAt从数组中删除一个及一个以上的元素,并将被删元素后面的所有元素前移。RemoveAll清空整个数组。两个函数都将调整数组的上界 从而反映出被删除的元素项个数。如果被删除的元素是指针,它并不删除指针所指的对象。如果数组是CPtrArray或CObArray类型的,要清空数组并删除指针所指的对象就应该写成:

 
如果对地址保存在指针数组中的对象删除失败,就会导致内存泄露。

1.2动态调整MFC数组大小

      除了可以边界检查外,MFC数组类还支持 动态调整大小。由于 为保存数组元素 而分配的内存可以“根据 元素 的 添加或删除 而”增大或缩小。所以没有必要事先预见动态调整尺寸的数组具有多少元素。

      一种动态增大MFC数组的方法是调用SetSize。可以在任何需要的时候调用SetSize来分配额外的内存。假设开始的时候给数组设置了10个元素项,后来却发现需要20个,这时只要第二次调用SetSize给额外的项分配空间即可。用此方法调整数组大小时,原来的项仍旧保持它们的值。因此,在调用SetSize之后新项需要明确的初始化。

      另一种增大数组的方法是调用SetAtGrow而不是SetAt来添加元素项。例如:CUIntArray array;array.SetAt(0,1);此代码会执行断言处理。因为数组大小为0,SetAt不会自动增大数组来容纳新的元素。但是,将SetAt更改为SetAtGrow后,程序将顺利执行。与SetAt不同,SetAtGrow会在必要时自动增大数组的内存分配空间。Add函数也是这样,他将元素添加到数组的末尾。其它可以自动增大数组来容纳新的元素项的函数还包括:InsertAt、Append(将一个数组附加给另一个数组)以及Copy(将一个数组复制到另一个数组)

      由于每当数组尺寸增加时都要分配新的内存,所以太频繁的增大数组会对操作产生不好的影响并有可能导致产生内存碎片。如下代码:

CUIntArray array;

for(int i=0;i<100000;i++)

      array.Add(i+1);

这些语句看上去非常正确,但它们效率却不高,要申请分配成千上万个独立的内存。这也正是MFC让你在SetSize中可选的第二个参数指定“增加量”的原因。下面的代码更有效的初始化了一个数组,它告诉MFC在需要申请更多的内存时,每次分配10000个UINT的内存空间。

CUIntArray array;

array.SetSize(0,10000);

for(int i=0;i<100000;i++)

      array.Add(i+1);

当然,要是预先给100000个元素分配空间,那么程序的效率会更高一些。但事先不可能预见到数组要保存的元素的数量。如果能预见到能给数组增加许多元素却不能确定到底需要多少空间,那么指定大的增加量是有益的。如果你没有指定增加量,MFC会通过“基于数组尺寸 得到的 简单公式”为你选择一个值。数组越大,增加量也越大。如果指定数组尺寸为0,并且根本没有调用SetSize,那么默认增加量为4项。

      同样一个用来增大数组的SetSize函数也可以用来减少数组元素。但是,当它减少数组时,SetSize并不会自动缩小保存数组数据的缓冲区,除非调用FreeExtra函数之后。

1.3用CArray创建“类型安全”数组类

      CUIntArray、CStringArray、以及其它MFC数组类都是针对特定数据类型的。如果假设需要一个其它数据类型的数组,例如CPoint对象的数组,由于不存在CPointArray类,所以必须从CArray类中自己创建了。CArray是一个模板类,用它可以为任意的 数据类型 创建 “类型安全”数组类。

      为了明了起见,下面用一个自己声明的CPoint类来创建CPoint类型的“类型安全”数组,并对类进行实例化。

CArray<CPoint,CPoint&> array;

模板中的第一个参数指定了保存在数组中的数据类型,第二个参数指定“类型在参数列表中”的表示方法。

      使用CArray和其它基于模板的MFC集合类工作的时候,在创建的类中包含默认的构造函数很重要,因为MFC在类似的InsertAt这样的函数被调用时会使用类的默认构造函数来创建新的元素项。

      有了可以随意处理的CArray,如果愿意的话,你可以不使用项CUIntArray这样的老式MFC数组类而只使用模板。下面语句用typedef定义了一个CUIntArray数据类型,功能与MFC的CUIntArray等价:typedef CArray<UINT,UINT> CUIntArray;最终选择哪个类取决于你自己。但是MFC资料中却建议尽可能的使用模板类,因为这样做可以与现代C++程序设计惯例保持一致。

2、链表

      Insert和RemoveAt函数使得给数组添加和删除元素非常方便。但这种插入和删除的简便方法也是有代价的:如果在数组的中间插入或删除元素,数组高端元素就会在内存中向上或向下移动。在用此方法处理大型的数组时,这种操作付出的代价是十分昂贵的。

2.1MFC链表类

      MFC的模板类CList实现了一般的链表,用它可以自定义处理任何数据类型。MFC还提供了下面列出的处理特定数据类型的非模板链表类。这些类主要用于与MFC旧版本兼容,在现代的MFC应用程序中并不经常使用。

    类名             数据类型

CObList          CObject指针

CPtrList          void指针

CStringList     CString

MFC链表是双向链接的,便于前后移动操作。链表中的位置由抽象数值POSITION标识。对于链表,POSITION实际上是指向CNode数据结构的指针(该结构代表了链表中的链表项)。CNode包含3个字段:1、一个指向链表中下一个CNode结构的指针2、一个指向链表中上一个CNode结构的指针3、链表项的数据。无论是在链表头还是链表尾,或是在POSITION指定的任何位置,插入操作都是快速高效的。还可以对链表进行查询操作,但是由于查询涉及到顺序遍历链表并逐个检查链表项,所以要是链表很大的话会占用很多时间。

      AddTail函数在链表结尾处添加一个链表项。要给链表头部添加链表项可以使用AddHead函数。在链表头或尾删除链表项同样简单,只要调用RemoveHead或RemoveTail即可。RemoveAll函数一下删除所有的链表项。

      例如:每次给CStringList添加一个字符串时,MFC都会将字符串 复制 给CString并在相应的CNode结构中保存它。因此,用来初始化 链表 的字符串 超出 创建链表时设定的范围 是完全可以接受的。

      一旦链表创建成功,就可以使用GetNext和GetPrev函数通过迭代在链表中前后移动了。两个函数都接受“表示链表中当前位置的POSITION值”并返回该位置处的链表项。两者都要更新POSITION值来引用下一个或上一个链表项。可以使用GetHeadPosition或GetTailPosition来检索链表中链表头或者链表尾的POSITION。如果只是希望得到链表头或链表尾的链表项,可以使用GetHead或GetTail函数,由于位置已经隐含在调用中了,所以它们都不需要输入POSITION值。

      如果给定标识特别链表项的POSITION值,就可以使用链表的At函数来检索、修改、或删除它:

CSting str=list.GetAt(pos);//Retrieve the item

list.SetAt(pos,"florida state");//Chage it

list.RemoveAt(pos);//Delete it

还可以使用InsertBefor或InsertAfter在链表中插入链表项:

list.InsertBefore(pos,"Florida state");

list.InsertAfter(pos,"Florida state");

链表的特性决定了这样进行插入和删除操作效率非常高。

      MFC的链表类还包含这样2个成员函数,可以用来执行查找操作。FindIndex接受从0开始的索引号并返回 链表 中相应位置处的 链表项的POSITION值。Find查找与指定输入匹配的链表项,并返回它的POSITION。对于字符串链表它比较字符串,对于指针链表它比较指针,但并不寻找和比较指针所指的链表项。要在字符串链表中查找“Tennessee”只需调用一个函数:

POSITION pos=list.Find(“Tennessee”);默认状态下,Find从头至尾查找链表。如果愿意,可以在第二个参数指定查找的起始点。

      可以用GetCount函数了解链表中元素的个数。如果GetCount返回0,说明链表是空的,而检测空链表的最好方法是调用IsEmpty。

2.2用CList创建“类型安全”链表类

      可以利用MFC的CList类为所选的任何数据类型创建 类型安全 的链表类。如:CList<CPoint,CPoint&> list;与CArray一样,第一个参数指定了数据类型,第二个参数指定了参数列表中链表项的传递方式(通过引用)。

      如果在CList中使用了类而不是原始数据类型并且调用链表了Find函数,除非下列条件之一成立,否则程序不会得到编译:

@类具有重载了的==运算符,执行与相似对象的比较

@用特殊类型的版本覆盖了模板函数CompareElements,执行对2个实例的比较。

第一种方法更常用,在MFC类如CPoint和CString中已经为你实现了。如果自己亲自编写一个类,就必须进行运算符重载

 

 

 
覆盖CompareElements消除了对重载运算符的需要。

4、类型指针类

      名字中带有Ptr和Ob的MFC集合类(如:CPtrArray、CObArray、CPtrList、CObList)可以方便的 实现 保存一般指针(void)的容器和 保存 指向MFC对象指针(由CObject派生类创建的对象)的容器。使用Ptr和Ob类的问题出在它们太一般了。通常要求许多强制类型的转换,这对于许多C++程序员而言是令人生厌的,而且也是糟糕的编程风格。

      MFC的“类型指针类”用来以安全的方式处理指针集合,它为 保存指针 而不 危害类型安全,提供了一种简便的解决方法。如下“类型安全指针类”:

         类名                            说明

CTypedPtrArray             管理指针数组

CTypedPtrList                管理指针链表

CTypedPtrMap               管理 使用指针做为项目或关键字 的映射表

      假设你编写了一个绘图程序,并且创建了一个名为CLine的类来代表屏幕上绘制的线段。每次用户绘制一条线就创建一个新的CLine对象。如果需要一个地方来保存CLine指针,而且希望能够在集合的任何位置添加和删除指针都不会造成操作冲突,所以你决定使用链表,因为CLine是从CObject派生来的,所以CObList好像是个自然的选择。

      CObList可以完成任务,但是每次从链表中检索到一个CLine指针,都必须将它强制转换为CLine*,因为CObList返回的是CObject指针。CTypedList是一个很好的选择,它不需要类型强制转换,代码如下:

CTypedPtrList<COblist,CLine*> list;

当你使用GetNext检索一个CLine指针时,得到的就是一个CLine指针而不需要强制转换,这就是类型安全。

      CTypedPtrList和其它“类型安全指针类”一样要从“第一个模板参数指定的类”中派生实现。

所有保存指针的MFC集合类,它们从数组、链表或映射表删除指针,但绝不会删除指针所指的项目。因此,在清空一个CLine指针链表之前,也有必要删除CLines:

POSITION pos=list.GetHeadPosition();

while(pos!=NULL)

      delete list.GetNext(pos);

list.RemoveAll();

记住:如果你不删除CLines,没有人会为你删除。不要以为集合类会为你干这种事。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多