許多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,沒有人會為你刪除。不要以為集合類會為你幹這種事。