许多C++程序员都使用标准模板库(STL),因为用它很容易实现数组、链表、映射以及其它容器。STL语言中“容器”指的是保存“数据集合”的对象。但是在有STL之前,已经有MFC了。在称为“MFC集合类”的一系列类中,MFC提供了自己的数组、链表、以及映射的实现途径。虽然在MFC中使用STL非常安全,但许多MFC程序员还是更喜欢用MFC集合类,一方面原因是更熟悉MFC,另一方面原因是不愿意链接2个独立的类库增加应用程序的exe的尺寸。
有了MFC集合类的帮助,你根本不必从头编写一个链表。下面的内容将介绍MFC集合类,并深入说明它们的使用和操作。
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,没有人会为你删除。不要以为集合类会为你干这种事。