?? 13.5 文檔對象數據的銷毀.txt
字號:
13.5 文檔對象數據的銷毀
本章上述內容實現的Graphic程序,還隱含著一個錯誤,當新建→個文檔對象時,或者打開一個文檔
對象時,先前文檔所保存的數據沒有被銷毀,這主要是指 CGraphicView 類OnLButtonUp函數中在堆
上為CGraph對象分配的內存(即下面這條語句調用分配的內存)沒有被釋放。
CGraph *pGraph~new CGraph(~nDrawType,m-ptOrigin,point);
當我們新建一個文檔時,程序文檔對象所保存的數據要被銷毀,然后再與一個新的文檔相關聯。然
而對于在堆上分配的內存,必須由程序員自己去釋放。我們可以看一下 CDocument類的
OnNewDocument函數的實現,其源代碼位于 DOCCORE.CPP中。
例13-29
BOOL CDocument::OnNewDocument()
{
if(工sModified())
TRACEO ("Warning: OnNewDocument replaces an unsaved document.\丑 11 ) ;
DeleteContents() ;
m_strPathName.Empty(); 11 no path name yet
SetModifiedFlag(FALSE); 11 make clean
return TRUE;
可以看到, CDocument類OnNewDocument函數的默認實現是調用CDocument類的另一個成員函數
DeleteContents,以確保這個文檔是空的,然后標記該新文檔是干凈的。同樣的,當單擊【文件\
打開】菜單命令后,程序框架會調用OnOpenDocument函數,其默認實現是打開指定的文件,調用
DeleteContents函數以確保這個文檔是空的。也就是說,不管是新建文檔,還是打開一個己有文檔,
都是先刪除文檔數據,因為本例是單文檔應用類型的程序,它只有一個文檔對象,該對象將被重復
使用,所以應該在該文檔對象被再次使用之前去刪除和這個文檔對象相關聯的所有數據。因為文件
打開和文件新建都會調用 DeleteContents函數,所以在這個函數中釋放文檔對象在堆上分配的內存
是比較合適的。 DeleteContents畫數是一個虛函數,主要是由框架調用,用來刪除文檔的數據,同
時并不銷
毀CDocument對象本身a它是在文檔將要被銷毀之前被調用,它
使用之前被調用,以確保文檔是空的。對單文檔應用程序來說,這一點特別重要,因為僅僅使用一
個文檔,無論用戶是創建,還是打開另一個文檔,該文檔對象都是被重復使用的。所以在文檔對象
被重復使用之前,應釋放己分配的內存。
對Graphic程序來說,在DeleteContents函數被調用時就應該釋放在堆上分配的CGraph
創.~ I 519
第13
這個對象的內存。于是,我們為 CGraphicDoc類增加虛函數: DeleteContents的重載,然后在其中
添加如例 13-30所示代碼。
1J~ 13-30
1. void CGraphicDoc::DeleteContents()
2. {
3. // TODO: Add your specialized code here and/or call the base class
4. int nCount i
5. nCount=m_obArray.GetSize() i
6. for(int i=Oii<m一obArray.GetSize()ii++)
7. {
8. delete m一obArray.GetAt(i) i
9. m_obArray.RemoveAt(i) i
10. }
11.
12. CDocument::DeleteContents();
13. }
在上述如例 13-30所示代碼中,首先定義了一個整型變量: nCount,保存m_obArray數組中的元素個
數。因為先前利用new操作符分配的內存,所以必須利用delete函數釋放該內存。上述例 13-30所示
代碼中通過for循環遍歷m_obArray數組中的每一個元素,并利用 CObArray類的 GetAt成員函數取出
指定索引的元素。因為在CObArray數組中保存的元素都是指針,所以利用delete函數將刪除這個指
針所指向的堆內存。讀者一定要注意,雖然這時刪除了這個指針所指向的堆內存,但是對于m_obArray
數組所保存的元素來說,其內存并沒有被刪除,也就是說,它所保存的指針值還是存在的,所以我
們需要把數組所保存的元素,即 CGraph指針值刪除掉。 CObArray類中有-個成員函數: RemoveAt,
可以刪除指定索引處的元素,其原型聲明如下所示:
void RemoveAt( int nlndex, int nCount = 1 )i
該函數的第一個參數(nIndex)設定索引,第二個參數 (nCount)指定要移走的元素數目。因此,在如
例 13-30所示代碼中,在釋放堆內存之后,就調用RemoveAt函數刪除指定索引i處的元素。
我們在上述例 13-30所示代碼中 for循環處設置一個斷點,調試運行程序,利用相應繪圖菜單命令
繪制一些圖形,例如繪制三條直線,即這時會產生三個圖形對象(/IP CGraph 對象)。我們知道,當
新建文件、打開文件,或者關閉程序時,也就是說,當文檔對象被銷毀時,都會去調用文檔類對象(本
例即CGraphicDoc對象)的DeleteContents函數。因此,我們執行關閉 Graphic程序的操作,程序將
進入上述例 13-30所示的 DeleteContents函數中。讀者將可以看到這時 nCount變量的值等于 3,
這個數值是正確的,因為此時 m_obArray數組中確實是保存了三個元素。然后單步執行程序,這時
索引 i是 0,也就是釋放索引0位置處的元素所在的堆內存(第8行代碼),然后移走元素本身(第9行
代碼),這是第一次循環。繼續運行,進入第二次循環,此時i為1,所以刪除索引為 1的元素所
.
520 I ~~勢
指向的堆內存(第 8行代碼〉、并移走元素本身(第 9行代碼)。繼續運行,接下來應釋放索引為 2
元素所指的堆內存,但是發現程序直接退出了,并沒有進入第 3次循環。
這里之所有沒有成功刪除內存,主要是因為利用 RemoveAt函數刪除元素時出現了問題,該函數是在
數組指定的索引位置處開始移走一個或多個元素,在這個過程中,它會下移在這個元素之上的所有
元素,并減少這個數組的上界。也就是說. RemoveAt函數的調用會導致數組中剩余元素的重新排列。
例如,數組有三個元素,當移走索引為 O的元素時,原先索引為 1和 2的元素都會下移,即索引 1
的元素現在索引為 O.索引 2的元素現在索引為 1。因此在上述代碼中,第二次進入循環時, i已經
變成 1了,于是它刪除的實際是原先索引為 2的那個元素,即數組中的最后一個元素,但是刪除這
個元素之后,還漏掉了一個元素,即原索引為1,現索引為 O的那個元素。也就說它循環兩次之后就
出現問題了。這是我們經常容易犯的錯誤,把判斷元素數目的代碼放置在條件判斷的位置(上述代碼
中 for循環的條件判斷語句: i< m_obArray.GetSizeO).對于剛才這種寫法,因為刪除元素后,它的
大小就會發生變化. GetSizeO函數的返回值也在不斷變化,原先是 3個元素,移走一個后變成 2個。
所以第三次循環時,索引 i是 2.而 GetSize函數的返回值是1,所以循環結束。而這樣的一種調用,
程序員根本發現不了它有問題。當我們不斷地新建文檔和打開另一個文檔時,實際上就隱含的有內
存泄漏的發生,因為有些對象的內存沒有被釋放。如果我們對 RemoveAt函數調用的機制不太了解的
話,所編寫的代碼就會存在內存泄漏的隱患,因此在這里需要修改代碼,在釋放元素所保存的指針
所指向的堆內存之后(第 8行代碼).先不刪除這個元素本身,然后在 for循環結束之后,也就是所有
元素所指向的堆內存都被刪除之后,再刪除這些元素。為了刪除 m_obArray數組中的所有元素,并
不需要再進行一次循環,逐一刪除,因為 CObArray類中還有一個成員函數: RemoveAll.用于從這個
數組中移除所有元素。也就是說,在上述 for循環之后,可以調用 m_obArray對象的 RemoveAll函
數,刪除其所有元素。這樣的話,程序就不會出現問題了。修改后的代碼如例 13-31所示。
例 13-31
void CGraphicDoc::DeleteContents()
// TODO: Add your specialized code here and/or call the base class int nCount;
nCount=m一obArray.GetSize();
for(int i=O;i<nCount;i++)
delete m_obArray.GetAt(i);
//m_obArray.RemoveAt(土) ;
m_obArray.RemoveAll();
CDocument::DeleteContents();
m
這時,讀者可以再次調試運行 Graphic程序,同樣也繪制三條直線,然后執行關閉程
13
序的操作,程序將進入如例 13-31所示 CGraphicDoc類的 DeleteContents函數中,繼續調試程序,
將會看到 DeleteContents函數中的 for循環確實執行了三次,把程序中分配的堆內存都釋放掉了,
最后移走了數組中的所有元素。
另外,我們還可以采用另一種實現方式,可以從索引最大的元素開始刪除,這時的實現代碼如例
13-32所示。
例 13-32
void CGraphicDoc: :DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
int nCount;
nCount=m一obArray.GetSize() ;
while(nCount--)
{
delete m_obArray.GetAt(nCount);
m一 obArray.RemoveAt(nCount);
CDocument: :DeleteContents();
在上述例 13-32代碼中,得到數組元素大小后,進行 while循環,在此循環中,刪除當前索引位置
處元素保存的指針所指向的堆內存。當然,這時就可以調用 RemoveAt函數刪除數組索引值最大的那
個元素。我們可以分析一下這時程序的調用流程,首先 nCount等于 3,進入 while循環,先判斷條
件為真,然后 nCount值減 1。于是移走 m_obArray數組中索引為 2的那個元素保存的指針所指向的
堆內存,并刪除元素本身:再進入 while語句,先判斷 nCount值為 2,條件為真,繼續循環, nCount
值減 1變成1,釋放索引 1位置處的元素保存的指針所指向的堆內存,并刪除該元素本身:接下來,
因為 nCount值為1, while語旬的條件仍為真,繼續循環, nCount值減 1變為 0,釋放索引 0位置
處的元素保存的指針所指向的堆內存,并刪除該元素本身:這時,因為 nCount值為 0, while條件
為假,所以 while循環終止。可見,當繪制三條直線時,上述 while循環確實執行了三次。讀者可
以自行調試運行 Graphic程序,測試這段代碼。
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -