在開發程式的過程中,經常需要尋找程式中的錯誤,這就需要利用調試工具來幫
助你進行程式的調試,當然目前有許多調試工具,而整合在VC中的調試工具以其
強大的功能,一定使你愛不釋手。下面我們先來介紹VC中的調試工具的使用。
1 VC調試工具
1.1 調試環境的建立
在VC中每當建立一個工程(Project)時,VC都會自動建立兩個版本:Release
版本,和Debug版本,正如其字面意思所說的,Release版本是當程式完成後,準備
發行時用來編譯的版本,而Debug版本是用在開發過程中進行調試時所用的版本。
DEBUG 版本當中,包含著MICROSOFT 格式的調試資訊,不進行任何代碼最佳化
,而在RELEASE 版本對可執行程式的二進位代碼進行了最佳化,但是其中不包含任
何的調試資訊。
在建立立的工程中,你所看到是DEBUG版本,若要選擇RELEASE版本,可以選擇菜
單PROJECT中的SETTING命令,這時螢幕上面彈出PROJECT SETTEING 對話方塊,在
SETTING FOR下拉式清單中選擇RELEASE,按OK退出,4.1。
圖4.1
在偵錯工具的時候必須使用DEBUG版本,我們可以在Project Setting 對話方塊的
C/C++頁中設定調試選項。
圖4.2
各個選項的含意如下:
· Program Database表示產生一個儲存程式資訊的資料檔案(.PDB),它包含
了類型資訊和符號化的調試資訊;
· Line Numbers Only表示程式經過編譯和連結產生的.OBJ或.EXE檔案僅僅
包含全域和外部符號以及行號資訊;
· C7 Compatible表示產生一個.OBJ或.EXE檔案行號資訊以及符號化的調試
資訊;
· None表示不產生任何調試資訊。
1.2調試的一般過程
調試,說到底就是在程式的運行過程的某一階段觀測程式的狀態,而在一般情況
下程式是連續啟動並執行,所以我們必須使程式在某一地點停下來。所以我們所做的
第一項工作就是設立斷點。其次,再運行程式,當程式在設立斷點處停下來時,再
利用各種工具觀察程式的狀態。程式在斷點停下來後,有時我們需要按我們的要
求控製程序的運行,以進一步觀測程式的流向,所以下面我們依次來介紹斷點的
設定,如何控製程序的運行以及各種觀察工具的利用。
1.3 如何設定斷點
在VC中,你可以設定多種類型的斷點,我們可以根據斷點起作用的方式把這些斷
點分為三類:1、與位置有關的斷點;2、與邏輯條件有關的斷點3、與WINDOWS消
息有關的斷點下面我們分別介紹這三類斷點。
首先我們介紹與位置有關的斷點。
1、 最簡單的是設定一般位置斷點,你只要把游標移到你要設斷點的位置,
當然這一行必須包含一條有效語句的;然後按工具條上的add/remove
breakpoint 按鈕或按快速鍵F9;這時你將會在螢幕上看到在這一行的左邊出現
一個紅色的圓點表示這二設 立了一個斷點。
圖4.3
2 、有的時候你可能並不需要程式每次運行到這兒都停下來,而是在滿足一定條
件的情況下才停下來,這時你就需要設定一種與位置有關的邏輯斷點。要設定這
種斷點我們只需要從EDIT 菜單中選中breakpoint命令,這時Breakpoint對話方塊
將會出現在螢幕上。選中Breakpoint對話方塊中的LOCATION標籤,使LOCATION 頁
面彈出,4.4
圖4.4
單擊condition按鈕,彈出Breakpoint對話方塊,在Expression編輯框中寫出你
的邏輯運算式,如X>=3或a+b>25,最後按OK返回。
圖4.5
這種斷點主要是由其位置發生作用的,但也結合了邏輯條件,使之更靈活。
3、有時我們需要更深入地偵錯工具,我們需要進入程式的彙編代碼,因此我們
需要在在彙編代碼上設立斷點:要設立這種斷點我們只需從View菜單中選Debug
window命令 ,
圖4.6
再選Disassembly子命令,這時彙編視窗將會出現在螢幕上。
圖4.7
在圖4.7中的彙編視窗中你將看到對應於來源程式的彙編代碼,其中來源程式是用黑
體字顯示,下面是且對應的彙編代碼。要設立斷點,我們只需將游標移到你想設
斷點處然後點擊工具條上的Insert/Remove Breakpoints 按鈕,此後你將會看到
一個紅圓點出現在該彙編代碼的右邊。
圖4.8
上面所講的斷點主要是由於其位置發揮作用的,即當程式運行到設立斷點的地方
時程式將會停下來。但有時我們設立只與邏輯條件有關的斷點,而與位置無關。
所以下面介紹一下與邏輯條件有關的斷點。
(1)邏輯條件觸發斷點的設定:
l 從EDIT 菜單中選中breakpoint命令,這時螢幕上將會出現Breakpoint對話
框。
圖4.9
l 選中Breakpoint對話方塊中的DATA標籤,對應的頁面將會彈出
圖4.10
l 在圖4.10的DATA頁面中的Expression編輯框中寫出你的邏輯運算式,如
(X==3);
圖4.11
l 最後按OK返回。
其他幾種斷點的設定的方法都與之類似。我們一一加以說明。
(2)監視運算式發生變化斷點:
l 從EDIT 菜單中選中breakpoint命令,這時螢幕上將會出現Breakpoint對話
框。
l 選中Breakpoint對話方塊中的DATA標籤,對應的頁面將會彈出
l 在Expression編輯框中寫出你需要監視的運算式
l 最後按OK鍵返回。
(3)監視數組發生變化的斷點:
l 從EDIT 菜單中選中breakpoint命令,這時螢幕上將會 出現Breakpoint對
話框。
l 選中Breakpoint對話方塊中的DATA標籤,對應的頁面將會彈出
l 在Expression編輯框中寫出你需要監視數組名;
l 在Number of Elements 編輯框輸入你需要監視數組元素的個數;
l 按OK鍵返回。
(4)監視由指標指向的數組發生變化的斷點:
l 從EDIT 菜單中選中breakpoint命令,這時在螢幕上將會出現Breakpoint對
話框。
l 選中Breakpoint對話方塊中的DATA標籤;
l 在Expression編輯框中輸入形如*pointname,其中*pointname為指標變數名
;
l 在Number of Elements 編輯框輸入你需要監視數組元素的個數;
l 按OK鍵返回。
(5)監視外部變數發生變化的斷點:
l 從EDIT 菜單中選中breakpoint命令這時螢幕上將會出現Breakpoint對話方塊
;
l 選中Breakpoint對話方塊中的DATA標籤;
l 在Expression編輯框中輸入變數名;
l 點擊在Expression編輯框的右邊的下拉鍵頭;
l 選取Advanced選項,這時Advanced Breakpoint 對話方塊出現;
l 在context框中輸入對應的函數名和(如果需要的話)檔案名稱;
l 按OK鍵關閉Advanced Breakpoint 對話方塊。
l 按OK鍵關閉Breakpoints 對話方塊。
(6)在講了位置斷點和邏輯斷點之後我們再講一下與WINDOWS訊息有關的斷點。
注意:此類斷點只能工作在x86 或 Pentium 系統上。
l 從EDIT 菜單中選中breakpoint命令,這時螢幕上將會出現Breakpoint對話
框;
l 選中Breakpoint對話方塊中的MESSAGE標籤,對應的頁面將會彈出;
l 在Break At WndProc 編輯框中輸入Windows 函數的名稱;
l 在Set One Breakpoint From Each Message To Watch 下拉式清單方塊中選擇
對應的訊息;
l 按OK 返回。
1.4 控製程序的運行
上面我們講了如何設定各類斷點,下面我們來介紹如何控製程序的運行。當我們
從菜單Build到子功能表Start Debuging 選擇Go 程式開始運行在Debug狀態下,程
序會由於斷點而停頓下來後,可以看到有一個小箭頭,它指向即將執行的代碼。
圖4.12
隨後,我們就可以按要求來控製程序的運行:其中有四條命令:Step over,
step Into , Step Out ,Run to Cursor。
圖4.13
在圖4.13中:
Step over 的功能是運行當前箭頭指向的代碼(只運行一條代碼)。
Step Into的功能是如果當前箭頭所指的代碼是一個函數的調用,則用Step Into
進入該函數進行逐步執行。
Step Out的功能是如當前箭頭所指向的代碼是在某一函數內,用它使程式運行至
函數返回處。
Run to Cursor的功能是使程式運行至游標所指的代碼處。
1.5 查看工具的使用
調試過程中最重要的是要觀察程式在運行過程中的狀態,這樣我們才能找出
程式的錯誤之處。這裡所說的狀態包括各變數的值,寄存中的值,記憶體中的值,
堆棧中的值 ,為此我們需要利用各種工具來協助我們察看程式的狀態。
¨ 彈出式調試資訊泡泡(Data Tips Pop_up Information)。
當程式在斷點停下來後,要觀察一個變數或運算式的值的最容易的方法是利用調
試資訊泡泡。要看一個變數的值,只需在來源程式視窗中,將滑鼠放到該變數上,
你將會看到一個資訊泡泡彈出,其中顯示出該變數的值。
圖4.14
要查看一個運算式的值,先選中該運算式,仍後將滑鼠放到選中的運算式上,同
樣會看到一個資訊泡泡彈出以顯示該運算式的值4.15所示。
圖4.15
¨ 變數視窗(VARIABLE WINDOW)。
在VIEW 菜單,Debug window選 Variables window; 變數視窗將出現在螢幕上。
其中顯示著變數名及其對應的值。你將會看到在變數觀察視窗的下部有三個標籤
:AUTO ,LOCAL,THIS 選中不同的標籤,不同類型的變數將會顯示在該視窗中。
圖4.16
¨ 觀察視窗(WATCH WINDOW):
在VIEW 菜單,選擇Debug window 命令,Watch window 子命令。這時變數視窗
將出現在螢幕上。
圖4.17
在圖4.17的觀察視窗中雙擊Name欄的某一空行,輸入你要查看的變數名或運算式
。
圖4.18
斷行符號後你將會看到對應的值。觀察視窗可有多頁,分別對應於標籤
Watch1,Watch2,Watch3等等。假如你輸入的運算式是一個結構或是一個對象,你
可以用滑鼠點取運算式右邊的形如 + ,以進一步觀察其中的成員變數的值
4.19。
圖4.19
¨ 快速查看變數對話方塊(quick watch);
在快速查看變數對話方塊中你可以象利用觀察視窗一樣來查看變數或運算式的值。
但我們還可以利用它來該變運行過程中的變數,具體操作如下:
(1) 在Debug 菜單,選擇Quick Watch命令,這時螢幕上將會出現Quick
Watch 對話方塊;
圖4.20
(2) 在Expression 編輯框中輸入變數名,按斷行符號;
圖4.21
(3)在Current Value 格子中將出現變數名及其當前對應的值4.22:
圖4.22
(4)如要改變該變數的值只需雙擊該變數對應的Name 欄,輸入你要改變的
值;
(5)如要把該變數加入到觀察視窗中,點擊Add watch 按鈕;
(6)點擊Close 按鈕返回;
¨ 我們還可以直接查看記憶體中的值
(1)從View菜單中選取Debug windows 及Memory 子命令。Memory Window 出現
;
圖4.23
(2)在Address 編輯框中輸入你要查看的記憶體位址,斷行符號。對應記憶體位址中的
值將顯示在Memory window 的視窗中。
圖4.24
¨ 在調試過程中,有時我們需要查看或改寄存器中的值。我們只需:
(1)從View 菜單中選取Debug window 及 Registers 子選項。Registers
視窗出現。在Registers 視窗中,資訊以 Register = Value 的形式顯示,其中
Register 代表寄存器的名字,Value 代表寄存器中的值。
圖4.25
(2)如果你要修改某一個寄存器的值,用TAB鍵,或滑鼠將游標移到你想改
變的值的右邊,然後輸入你想要的值。斷行符號返回。
在寄存器中,有一類特殊的寄存器稱為標誌寄存器,其中有八個標誌位:
OV是溢出標誌;
UP是方向標誌;
EI是中斷使能標誌;
Sign 是符號標誌,
Zero是零標誌。
Parity是奇偶較驗標誌。
Carry 是進位標誌。
2 進階調試技術
前面我們講了調試工具的使用,利用它可以就進行常規的調試,即使程式在某處
停下來,再觀察程式的當前壯態。而且這些工具在且它調試器中也有。但我們知
道我們知道在VC程式的開發過程中,光有這些工具是不夠的。為了更快更好地開
發程式,我們還需要利用更進階的調試工具。我們知道,在利用VC開發過程中,
利用MFC將會極大地方便應用程式的開發,所以開發人員往往是利用MFC來開發應
用程式,正是這個原因Microsoft公司在MFC中提供了一些特性來協助你進行程式
的調試。
我們知道在MFC中,絕大多數類都是從一個叫做Cobject的類繼承過來的,雖然這
是一個虛基類,但它定義了許多成員函數,其中許多成員函數是用來支援程式的
調試的,如Dump ,Assertvalid 等成員函數。另外他們都支援如TRACE,ASSERT等
宏,並支援記憶體漏洞的檢查等等。我們知道,為了支援調試,類庫肯定在在效能
上有所損失,為此Microsoft 公司提供了兩個不同的版本的類庫:Win32 Debug
版本和Win32 Release版本。在前面我們已經提到,每當我們建立一個工程時,
我們也有對應的兩個版本。在你的DEBUG 版本的工程中,編譯器串連DEBUG 版本
的MFC類庫;在你的RELEASE 版本的工程中編譯器串連RELEASE版本的MFC 類庫以
獲得儘可能快的速度。下面我們來介紹這些工具的利用。
2.1 TRACE 宏的利用
TRACE 宏有點象我們以前在C語言中用的Printf函數,使程式在運行過程中
輸出一些調試資訊,使我們能瞭解程式的一些狀態。但有一點不同的是:TRACE
宏只有在調試狀態下才有所輸出,而以前用的Printf 函數在任何情況下都有輸
出。和Printf 函數一樣,TRACE函數可以接受多個參數如:
int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement\n" );
TRACE( "The value of x is %d\n", x );
TRACE( "x = %d and y = %d\n", x, y );
TRACE( "x = %d and y = %x and z = %f\n", x, y, z );
要注意的是TRACE宏只對Debug 版本的工程產生作用,在Release 版本的工程中
,TRACE宏將被忽略。
2.2 ASSERT宏的利用
在開發過程中我們可以假設只要程式運行正確,某一條件肯定成立。如不成立
,那麼我們可以斷言程式肯定出錯。在這種情況下我們可以利用ASSERT來設定斷
言。ASSERT宏的參數是一個邏輯運算式,在程式運行過程中,若該邏輯運算式為
真,則不會發生任何動作,若此運算式為假,系統將彈出一個對話方塊警告你,並
停止程式的執行。同時要求你作出選擇:Abort,Ignore,Retry。若你選擇
Abort,系統將停止程式的執行;若你選擇Ignore 系統將忽略該錯誤,並繼續執
行程式;若你選擇Retry ,系統將重新計算該運算式,並啟用調試器。同TRACE
宏一樣,ASSERT宏只DEBUG版本中起作用,在RELEASE版本中ASSERT宏將被忽略。
2.3 ASSERT_VALID宏的利用以及類的AssertValid()成員函的重載
ASSERT_VALID宏用來在運行時檢查一個對象的內部合法性,比如說現在 有
一個學生對象,我們知道每個學生的年齡一定大於零,若年齡小於零,則該學生
對象肯定有問題。事實上,ASSERT_VALID宏就是轉化為對象的成員函數
AssertValid()的調用,只是這種方法更安全。它的參數是一個對象指標,通過
這個指標來調用它的AssertValid()成員函數。
與此相配套,每當我們建立從Cobject類繼承而來的一個新的類時,我們可以重
載該成員函數,以執行特定的合法性檢查。
2.4對象的DUMP函數的利用
Dump 函數用來按指定的格式輸出一個對象的成員變數,來協助你診斷一個
對象的內部情況。與AssertValid成員函數一樣,Dump也是Cobject 類的成員函
數。Dump函數的參數是一個CdumpContext對象,你可以象利用流一樣往向這個對
象中輸入資料。當你建立一個Cobject繼承而來的 新類時,你可以按如下步驟重
載你自己的Dump函數:
(1) 調用基類的Dump函數,以輸出基類的內容;
(2) 向Cdumpcontest對象輸出該類的資料.
例如,典型的Dump函數定義如下:
#ifdef _DEBUG
void CPerson::Dump( CDumpContext& dc ) const
{
// call base class function first
CObject::Dump( dc );
// now do the stuff for our specific class
dc << "last name: " << m_lastName << "\n"
<< "first name: " << m_firstName << "\n";
}
#endif
你可能已經注意到整個函數的定義都包含在#ifdef _DEBUG 和#endif中,這
使得Dump成員函數只在DEBUG版本中發生作用,而對RELEASE版本不發生作用。
3 記憶體漏洞的檢查
也許你已經知道,在C++和C語言中指標問題也就是記憶體申請與釋放是一個令人頭
疼的事情,假如你申請了記憶體,但沒有釋放,並且你的程式需要長時間地運行,
那麼,系統的資源將逐漸減少,當系統的資源全部被用完時,系統將會崩潰。所
以在開發程式的過程中一定要保證資源的完全釋放。下面我們來介紹記憶體漏洞的
檢查。
也許你會問,系統是怎樣支援記憶體漏洞的檢查的?其實在你的Debug版本中所有
的有關記憶體配置的函數都是被重載過的,具體過程是這樣的,當你的程式申請內
存時,它首先調用一般的記憶體配置函數分配一塊稍大的記憶體塊。在這一記憶體塊中
分為四個小塊:Heap Information, buffer , User memory block, buffer。第
一塊為有關堆的資訊,比如,申請該記憶體的地點(檔案名稱,行號),此記憶體塊的類
型(如整型,浮點,或某一類的對象)等等。第二塊是一個緩衝區,用於截獲使用者
對其申請記憶體使用量越界的情況。第三塊是真正給使用者的記憶體,返回的指標也是指
向這兒。第四塊也是一個緩衝區,作用同第二塊。
當你申請的記憶體均被記錄在案後,要檢查記憶體漏洞就比較容易了,粗略地說,假
如你要檢查某一程式段是否有記憶體漏洞,你只需在這一程式 段的開始要求系統
為你做一個記憶體使用量情況的映象,記錄下程式開始時的記憶體使用量情況,然後在程
序段的末尾再使系統為你做一次記憶體映象,比較兩次映象,以檢查是否有沒釋放
的記憶體,假如有未釋放的記憶體,根據這一塊中有關分配情況的資訊來告訴使用者在
那兒申請的記憶體沒釋放。
具體地講檢查記憶體漏洞需要以下幾個步驟:
l 在你所檢測的程式段的開始處建立一個CmemoryState對象,調用其成員函
數Checkpoint,以取得當前記憶體使用量情況的快照;
l 在你所檢測的程式段的末尾處再建立一個CmemoryState 對象,調用其成員
函數Checkpoint ,以取得當前記憶體使用量情況的快照;
l 再建立第三個CmemoryState 對象,調用其成員函數Difference,把第一個
CmemoryState對象和第二個CmemeoryState對象作為其參數.,如果兩次記憶體快照
不相同,則該函數返回非零,說明此程式 段中有記憶體漏洞。下面我們來看一個
典型的例子:
// Declare the variables needed
#ifdef _DEBUG
CMemoryState oldMemState, newMemState, diffMemState;
OldMemState.Checkpoint();
#endif
// do your memory allocations and deallocations...
CString s = "This is a frame variable";
// the next object is a heap object
CPerson* p = new CPerson( "Smith", "Alan", "581_0215" );
#ifdef _DEBUG
newMemState.Checkpoint();
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!\n" );
}
#endif