爲了調試一個程序,首先必須使程序中包含調試信息。一般情況下,一個從AppWizard創建的工程中包含的Debug Configuration自動包含調試信息,但是是不是Debug版本並不是程序包含調試信息的決定因素,程序設計者可以在任意的Configuration中增加調試信息,包括Release版本。
爲了增加調試信息,可以按照下述步驟進行:
打開Project settings對話框(可以通過快捷鍵ALT+F7打開,也可以通過IDE菜單Project/Settings打開)
選擇C/C++頁,Category中選擇general ,則出現一個Debug Info下拉列表框,可供選擇的調試信息 方式包括:
命令行
Project settings
說明
無
None
沒有調試信息
/Zd
Line Numbers Only
目標文件或者可執行文件中只包含全局和導出符號以及代碼行信息,不包含符號調試信息
/Z7
C 7.0- Compatible
目標文件或者可執行文件中包含行號和所有符號調試信息,包括變量名及類型,函數及原型等
/Zi
Program Database
創建一個程序庫(PDB),包括類型信息和符號調試信息。
/ZI
Program Database for Edit and Continue
除了前面/Zi的功能外,這個選項允許對代碼進行調試過程中的修改和繼續執行。這個選項同時使#pragma設置的優化功能無效
選擇Link頁,選中複選框"Generate Debug Info",這個選項將使連接器把調試信息寫進可執行文件和DLL
如果C/C++頁中設置了Program Database以上的選項,則Link incrementally可以選擇。選中這個選項,將使程序可以在上一次編譯的基礎上被編譯(即增量編譯),而不必每次都從頭開始編譯。
調試方法:
1、使用 Assert(原則:儘量簡單) assert只在debug下生效,release下不會被編譯。
2、防禦性的編程
3、使用Trace
4、用GetLastError來檢測返回值,通過得到錯誤代碼來分析錯誤原因
5、把錯誤信息記錄到文件中
位置斷點(Location Breakpoint)
大家最常用的斷點是普通的位置斷點,在源程序的某一行按F9就設置了一個位置斷點。但對於很多問題,這種樸素的斷點作用有限。譬如下面這段代碼:
void CForDebugDlg::OnOK()
{
for (int i = 0; i < 1000; i++) //A
{
int k = i * 10 - 2; //B
SendTo(k); //C
int tmp = DoSome(i); //D
int j = i / tmp; //E
}
}
執行此函數,程序崩潰於E行,發現此時tmp爲0,假設tmp本不應該爲0,怎麼這個時候爲0呢?所以最好能夠跟蹤此次循環時DoSome函數是如何運行的,但由於是在循環體內,如果在E行設置斷點,可能需要按F5(GO)許多次。這樣手要不停的按,很痛苦。使用VC6斷點修飾條件就可以輕易解決此問題。步驟如下。
1 Ctrl+B打開斷點設置框,如下圖:
Figure 1設置高級位置斷點
2 然後選擇D行所在的斷點,然後點擊condition按鈕,在彈出對話框的最下面一個編輯框中輸入一個很大數目,具體視應用而定,這裏1000就夠了。
3 按F5重新運行程序,程序中斷。Ctrl+B打開斷點框,發現此斷點後跟隨一串說明:...487 times remaining。意思是還剩下487次沒有執行,那就是說執行到513(1000-487)次時候出錯的。因此,我們按步驟2所講,更改此斷點的skip次數,將1000改爲513。
4 再次重新運行程序,程序執行了513次循環,然後自動停在斷點處。這時,我們就可以仔細查看DoSome是如何返回0的。這樣,你就避免了手指的痛苦,節省了時間。
再看位置斷點其他修飾條件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,可以輸入一些條件,當這些條件滿足時,斷點才啓動。譬如,剛纔的程序,我們需要i爲100時程序停下來,我們就可以輸入在編輯框中輸入“i==100”。
另外,如果在此編輯框中如果只輸入變量名稱,則變量發生改變時,斷點纔會啓動。這對檢測一個變量何時被修改很方便,特別對一些大程序。
用好位置斷點的修飾條件,可以大大方便解決某些問題。
數據斷點(Data Breakpoint)
軟件調試過程中,有時會發現一些數據會莫名其妙的被修改掉(如一些數組的越界寫導致覆蓋了另外的變量),找出何處代碼導致這塊內存被更改是一件棘手的事情(如果沒有調試器的幫助)。恰當運用數據斷點可以快速幫你定位何時何處這個數據被修改。譬如下面一段程序:
#include "stdafx.h"
#include
int main(int argc, char* argv[])
{
char szName1[10];
char szName2[4];
strcpy(szName1,"shenzhen");
printf("%s/n", szName1); //A
strcpy(szName2, "vckbase"); //B
printf("%s/n", szName1);
printf("%s/n", szName2);
return 0;
}
這段程序的輸出是
szName1: shenzhen
szName1: ase
szName2: vckbase
szName1何時被修改呢?因爲沒有明顯的修改szName1代碼。我們可以首先在A行設置普通斷點,F5運行程序,程序停在A行。然後我們再設置一個數據斷點。如下圖:
Figure 2 數據斷點
F5繼續運行,程序停在B行,說明B處代碼修改了szName1。B處明明沒有修改szName1呀?但調試器指明是這一行,一般不會錯,所以還是靜下心來看看程序,哦,你發現了:szName2只有4個字節,而strcpy了7個字節,所以覆寫了szName1。
數據斷點不只是對變量改變有效,還可以設置變量是否等於某個值。譬如,你可以將Figure 2中紅圈處改爲條件”szName2[0]==''''y''''“,那麼當szName2第一個字符爲y時斷點就會啓動。
可以看出,數據斷點相對位置斷點一個很大的區別是不用明確指明在哪一行代碼設置斷點。
其他調試手段:系統提供一系列特殊的函數或者宏來處理Debug版本相關的信息,如下:
宏名/函數名
說明
TRACE
使用方法和printf完全一致,他在output框中輸出調試信息
ASSERT
它接收一個表達式,如果這個表達式爲TRUE,則無動作,否則中斷當前程序執行。對於系統中出現這個宏 導致的中斷,應該認爲你的函數調用未能滿足系統的調用此函數的前提條件。例如,對於一個還沒有創建的窗口調用SetWindowText等。
VERIFY
和ASSERT功能類似,所不同的是,在Release版本中,ASSERT不計算輸入的表達式的值,而VERIFY計算表達式的值。
值
Watch
VC支持查看變量、表達式和內存的值。所有這些觀察都必須是在斷點中斷的情況下進行。
觀看變量的值最簡單,當斷點到達時,把光標移動到這個變量上,停留一會就可以看到變量的值。
VC提供一種被成爲Watch的機制來觀看變量和表達式的值。在斷點狀態下,在變量上單擊右鍵,選擇Quick Watch, 就彈出一個對話框,顯示這個變量的值。
單擊Debug工具條上的Watch按鈕,就出現一個Watch視圖(Watch1,Watch2,Watch3,Watch4),在該視圖中輸入變量或者表達式,就可以觀察 變量或者表達式的值。注意:這個表達式不能有副作用,例如++運算符絕對禁止用於這個表達式中,因爲這個運算符將修改變量的值,導致 軟件的邏輯被破壞。
Memory
由於指針指向的數組,Watch只能顯示第一個元素的值。爲了顯示數組的後續內容,或者要顯示一片內存的內容,可以使用memory功能。在 Debug工具條上點memory按鈕,就彈出一個對話框,在其中輸入地址,就可以顯示該地址指向的內存的內容。
Varibles
Debug工具條上的Varibles按鈕彈出一個框,顯示所有當前執行上下文中可見的變量的值。特別是當前指令涉及的變量,以紅色顯示。
寄存器
Debug工具條上的Reigsters按鈕彈出一個框,顯示當前的所有寄存器的值。
--------------------------------------------------------------------------------
調試技巧:
1、VC++中F5進行調試運行
a)、在output Debug窗口中可以看到用TRACE打印的信息
b)、 Call Stack窗口中能看到程序的調用堆棧
2、當Debug版本運行時發生崩潰,選擇retry進行調試,通過看Call Stack分析出錯的位置及原因
3、使用映射文件調試
a)、創建映射文件:Project settings中link項,選中Generate mapfile,輸出程序代碼地址:/MAPINFO: LINES,得到引出序號:/MAPINFO: EXPORTS。
b)、程序發佈時,應該把所有模塊的映射文件都存檔。
c)、查看映射文件:見” 通過崩潰地址找出源代碼的出錯行”文件。
4、可以調試的Release版本
Project settings中C++項的Debug Info選擇爲Program Database,Link項的Debug中選擇Debug Info和Microsoft format。
5、查看API的錯誤碼,在watch窗口輸入@err可以查看或者@err,hr,其中”,hr”表示錯誤碼的說明。
6、Set Next Statement:該功能可以直接跳轉到指定的代碼行執行,一般用來測試異常處理的代碼。
7、調試內存變量的變化:當內存發生變化時停下來。???
進程控制
VC允許被中斷的程序繼續運行、單步運行和運行到指定光標處,分別對應快捷鍵F5、F10/F11和CTRL+F10。各個快捷鍵功能如下:
快捷鍵
說明
F5
調試/繼續運行
F10
單步,如果涉及到子函數,不進入子函數內部
F11
單步,如果涉及到子函數,進入子函數內部
CTRL+F10
運行到當前光標處。
F7
重建
F9
設置斷點/清除斷點
Ctrl+Shift+F9
清除所有斷點
Shift+F5
結束調試
Call Stack
調用堆棧反映了當前斷點處函數是被那些函數按照什麼順序調用的。單擊Debug工具條上的Call stack就顯示Call Stack對話框。在CallStack對話框中顯示了一個調用系列,最上面的是當前函數,往下依次是調用函數的上級函數。單擊這些函數名可以跳到對應的函數中去。
關注
一個好的程序員不應該把所有的判斷交給編譯器和調試器,應該在程序中自己加以程序保護和錯誤定位,具體措施包括:
對於所有有返回值的函數,都應該檢查返回值,除非你確信這個函數調用絕對不會出錯,或者不關心它是否出錯。
一些函數返回錯誤,需要用其他函數獲得錯誤的具體信息。例如accept返回INVALID_SOCKET表示accept失敗,爲了查明 具體的失敗原因,應該立刻用WSAGetLastError獲得錯誤碼,並針對性的解決問題。
有些函數通過異常機制拋出錯誤,應該用TRY-CATCH語句來檢查錯誤
程序員對於能處理的錯誤,應該自己在底層處理,對於不能處理的,應該報告給用戶讓他們決定怎麼處理。如果程序出了異常, 卻不對返回值和其他機制返回的錯誤信息進行判斷,只能是加大了找錯誤的難度。
另外:VC中要編制程序不應該一開始就寫cpp/h文件,而應該首先創建一個合適的工程。因爲只有這樣,VC才能選擇合適的編譯、連接 選項。對於加入到工程中的cpp文件,應該檢查是否在第一行顯式的包含stdafx.h頭文件,這是Microsoft Visual Studio爲了加快編譯 速度而設置的預編譯頭文件。在這個#include "stdafx.h"行前面的所有代碼將被忽略,所以其他頭文件應該在這一行後面被包含。
對於.c文件,由於不能包含stdafx.h,因此可以通過Project settings把它的預編譯頭設置爲“不使用”,方法是:
彈出Project settings對話框
選擇C/C++
Category選擇Precompilation Header
選擇不使用預編譯頭。
便於調試的代碼風格:
不用全局變量
所有變量都要初始化,成員變量在構造函數中初始化
儘量使用const