VC代碼的編寫和調試---編寫易於調試的VC代碼

 

VC代碼的編寫和調試---編寫易於調試的VC代碼


一 程序的設計

  要避免錯誤,首先要從好的設計開始。對於程序的設計,需考慮到程序的兩個特性:

  1簡單性 

  大多數常見的錯誤來源於程序設計中不必要的複雜成分。一個好的設計應該反映問題本身的要求,而不必爲了刻意追求“滿足將來的需要”而添加不必要的特性。實際上,簡單優雅的設計比那些複雜的設計更能迎合未來的需求。

  2 耦合性 

  耦合(decoupling)性用來衡量不同對象之間的依賴程度。松耦合的程序易於理解和實現,易於測試和維護,且這種程序包含錯誤的可能性小,錯誤也較容易發現和清除。

二 編程風格

  編程風格是個人問題,有很大的隨意性。一個好的編程風格不僅讓代碼易理解,也易於調試。好的編程風格包括:

  1 清晰地書寫代碼

  如果沒有必要,儘量不要使用語言中的高級特性,因爲這些特性不易於理解和調試。使用大多數程序員都能理解的語言成分來書寫代碼不易犯錯且易於理解和維護。

  2 編寫結構良好的代碼

  當程序崩潰時所能得到的最基本的調試信息是源代碼文件、問題所在行的行號和一個調用棧(call stack)。調用棧是調試程序時最有幫助的部分,它提供錯誤出現的上下文,也就是帶參數的函數調用序列。你書寫的代碼結構越好,調用棧就能給你越多信息。

  3 使用良好的標識符

  一個好名字能使你的代碼更容易被理解和維護。流行的匈牙利命名法(Hungarian Notation)實際上是把標識符的意義和表示方法結合起來。現在,匈牙利命名法表現出不少的侷限性,匈牙利命名法過於看重前綴的作用,對一個變量的表達信息不完整,實際上並沒有傳遞多少有用信息,它使代碼難於閱讀,難以維護。一個好的命名傳統是指示出變量的作用域以便在需要的時候檢查它的定義,並明確地指出一個變量是全局的、局部的還是成員數據。依賴變量的定義比依賴匈牙利前綴更加有用和可靠。

  好的名字能夠用平常的語言概括出該標識符所代表的實體的含義。在選擇類、函數、變量的名字時可以考慮以下幾個原則:

    取簡單的描述性名字,好的名字能簡要地概括出這個標識符代表的含義。

    避免簡寫,簡寫使標識符難於閱讀和記憶,儘量使用混合大小寫的完整的單詞。

    避免相似性的文字,避免混淆。

    避免採用一般的或隨機產生的名字,而應採用有實際意義的名字。如欲從按鈕類派生位圖按鈕,取一個CBitmapButton,而不是CMyButton。

  4 用簡單的語句行

  在VC中,一行可寫多個語句。但調試是面向行的,過於複雜的行難於調試。因此,從調試的角度出發,每一個語句都應獨自成行。

  5 使用統一的排列

  統一的排列方式使類、變量的定義和語句更加明顯。

  6 用括號使書寫清晰

  你不一定能都記住各種運算符的優先級和結合律,而使用多餘的括號並不影響編譯後的代碼。因此,如果你不能確定是否需要括號時,請加上它。

  7 使用好的註釋

  用好的註釋能使你的代碼不易出錯,而且便於其他程序員閱讀,便於理解和維護。

三 編寫程序時應注意的問題

  1 充分利用VC++的特性

  可用下列技術來充分利用VC++的編譯器的特性:

  (1)用const代替#define來創建常量;

  (2)用enum代替#define來創建常量集合;

  (3)用內聯(inline)函數代替#define;

  這三種技術用C++而不是C預處理。使用預處理的問題在於編譯器對於預處理器所作的事情一無所知,因此無法用數據類型檢查錯誤和不一致的地方。預處理的名字不在符號表裏,因此也不能用調試工具來檢查預處理常量。相似地,預處理宏被編譯進去,不能用調試工具跟蹤。編譯器能充分了解const、enum和inline語句,從而能在編譯的時候對出現的問題發出警告。

  但預處理在很多調試代碼中起重要作用。調試代碼經常需要從非調試代碼裏面得到不同的行爲,而最有效的辦法就是讓預處理爲調試創建不同的代碼。

  (4)用new和delete代替malloc和free;

  在創建對象、類型的安全性和靈活性方面。使用new/delete比malloc/free要好。另外,new可被重載,提供了更大的靈活性。

  (5)用輸入輸出流(iostreams)代替stdio。

  使用C++輸入輸出流(<<和>>)而不使用C標準輸入輸出庫(printf/sprintf和scanf/sscanf),有利於安全性和擴展性。從調試的角度來看,標準輸入輸出函數的最大問題在於編譯器不能對控制流參數進行任何類型檢測,而輸入輸出流的任何問題都能在編譯時檢測出來。

  2 使用頭文件

  要在頭文件中聲明所有共享的外部符號,而且保留函數原型中的參數名。把所有的共享定義放在頭文件中,不要在.cpp文件裏面看到extern關鍵字。

  3 初始化變量

  在使用變量之前一定要把它們初始化。在初始化之前就使用變量肯定會產生錯誤。通常不需對對象進行初始化,對對數據成員應在構造函數中初始化。必須明確地爲在棧中和堆中分配的數組和數據結構進行初始化。對於對象,應該初始化每個需要初始化的數據成員。因爲變量的使用是由優化器來檢查的,所以檢測未初始化的本地變量,發佈版本要比調試版本要做得好。

  4 使用布爾表達式

C++的布爾類型:bool,值爲true和false,大小爲一個字節。

Windows程序通常用BOOL類型。定義如下:

Typedef int BOOL;

#define FALSE 0

#define TRUE 1

  在C++中,一個布爾表達式如果爲0則爲假,其他則爲真。因此,對布爾表達式應該檢查是否問假而不是檢查是否爲真。

  5 使用句柄和指針

  初始化一個指針時,要麼讓其指向一個有效的內存地址,要麼設爲0(空指針),避免指針指向無效地址。回收指針所指對象時要重新初始化這個指針,並且在指針被釋放前爲空時就對其進行處理。對句柄的處理跟指針一樣。

  6 用引用而不是指針做參數

  用指針做函數的參數可傳遞一個空指針,很靈活,但也很容易忘了對指針進行初始化。而引用是對象的別名,它必須和有效的對象相關聯,不存在空的和沒有初始化的引用。當在函數中收到一個引用參數時,可以肯定這是一個有效的對象。程序用引用做參數比用指針做參數更爲健壯。

  7 強制類型轉換(cast)

  進行數據類型的強制類型轉換時,將會調用相應的構造函數或轉換函數來創建一個新類型的臨時對象。對指針的正確類型轉換可消除一個編譯錯誤,但並沒改變指針。強制類型轉換破壞了編譯器進行類型檢查的功能,而這正是編譯器查找錯誤的最有效的機制。爲了保證安全性,每一個強制類型轉換都需要手工進行類型檢查。爲儘量避免強制類型轉換,你可以:避免使用多態數據類型;使用更加廣泛的基類;提供特殊的存取函數;讓編譯器隱式處理類型轉換等措施。

  8 使用構造函數和析構函數

  構造函數需要分配內存,創建資源或者打開文件,這些運算並不總是成功。構造函數沒有返回值,沒有直接顯示錯誤的方法。一個常見的方法(在很多MFC類中使用)是把對象創建分爲兩步:第一步,讓構造函數以一種不會出錯的方式初始化對象;第二步,讓某些初始化函數(如Init或Open)完成工作,這一步可能出錯。另一種方法是在構造函數中使用異常:第一步,以不會出錯的方式初始化對象;第二步,用可能在try段內出錯的代碼初始化對象;第三步,在catch代碼裏面處理異常。如果出現異常,就會在構造函數裏清除分配的資源,並且再次拋出異常。

  異常處理的一個關鍵細節就是在棧展開的過程中拋出的異常會終止整個應用程序。在處理異常時經常要調用析構函數,因此析構函數很容易出錯,一定要保證析構函數的異常在析構函數中得到處理。要保證基類的析構函數是虛函數。這樣,就算對象是一個指向基類的指針,也會調用派生類的析構函數。否則,就會引起資源泄漏(resource leak)。 



  
在VC程序中使用調試語句
 

  爲了更好地對程序調試,可以使用如下方法:使用斷言、使用跟蹤語句、使用異常和返回值。

一、斷言

1、基本概念

  斷言是一種讓錯誤在運行時候自我暴露的簡單有效實用的技術。它們幫助你較早較輕易地發現錯誤,使得整個調試過程效率更高。

  斷言是布爾調試語句,用來檢測在程序正常運行的時候某一個條件的值是否總爲真,它能讓錯誤在運行時刻暴露在程序員面前。使用斷言的最大好處在於,能在更解決錯誤的發源地的地方發現錯誤。斷言具有以下特徵:

.斷言是用來發現運行時刻錯誤的,發現的錯誤是關於程序實現方面的。

.斷言中的布爾表達式顯示的是某個對象或者狀態的有效性而不是正確性。

.斷言在條件編譯後只存在於調試版本中,而不是發佈版本里。

.斷言不能包含程序代碼。

.斷言是爲了給程序員而不是用戶提供信息。

  使用斷言最根本的好處是自動發現許多運行時產生的錯誤,但斷言不能發現所有錯誤。斷言檢查的是程序的有效性而不是正確性,可通過斷言把錯誤限制在一個有限的範圍內。當斷言爲假,激活調試器顯示出錯代碼時,可用Call Stack命令,通過檢查棧裏的調用上下文、少量相關參數的值以及輸出窗口中Debug表的內容,通常能檢查出導致斷言失敗的原因。_ASSERTE宏(屬於C運行時間庫)還能在斷言失敗時顯示出失效斷言。下面我們討論一下MFC庫中的斷言。

2、MFC庫中的斷言

(1) ASSERT(布爾表達式)

用MFC時最好選擇ASSERT宏,它的優點是即使出現了WM_QUIT消息也能顯示斷言失效消息框。

(2) VERIFY(布爾表達式)

VERIFY宏中的布爾表達式在發佈版本中被保留下來。VERIFY宏簡化了對函數返回值的檢查,一般用來檢查Windows API的返回值。由於VERIFY宏裏的布爾表達式在發佈版本里保留了下來,因此最好儘量不要使用這個宏以實現程序代碼和調試代碼的完全分離。

(3 )ASSERT_VALID(指向CObject派生類對象的指針)

ASSERT_VALID宏通過調用重載的AssertValid函數來確定指向CObject派生類對象的指針是否有效。無論你什麼時候從CObject派生類中得到一個對象,在對這個對象做任何操作之前都應該調用ASSERT_VALID宏。

(4) ASSERT_KINDOF(類名, 指向CObject派生類對象的指針)

這個宏用來驗證指向CObject派生類對象的指針是否從某個特殊類中派生,在調用它之前先調用ASSERT_VALID宏。只有在很特殊的場合下才用得到,如檢測編譯器可能錯過的對象類型問題。

此外,還有兩個沒有正式文件的ASSERT宏的變種:ASSERT_POINTER(指針,指針類型),ASSERT_NULL_OR_POINTER(指針,指針類型)。

3、什麼時候使用斷言

  把斷言看作一種簡單的製造柵欄的方法,這種柵欄能使錯誤在穿過自己時暴露。

.檢查函數的輸入

.檢查函數的輸出

.檢查對象的當前狀態

.堅持邏輯變量的合理性和一致性

.檢查類中的不變量

公有成員函數比私有和保護的成員函數需要更全面的斷言。

  不正確地使用斷言會導致錯誤。斷言應該檢測那些在程序正常運行的時候永遠都不可能出現的狀態。斷言是用來揭示錯誤的,而不是用來糾正運行時刻錯誤的。

4、斷言與防禦性編程(Defensive Programming)

  斷言在調試的時候向程序員揭示運行時刻錯誤(調試版本里),而防禦性編程使用戶在運行程序(發佈版本里)時,當出現意外情況時程序仍能繼續工作。實際上,防禦性的編程要求程序在檢測到意外時返回一個“安全”的值(比如布爾函數返回false,指針和句柄返回空值),一個錯誤代碼或者拋出一個異常來解決問題。特定的防禦性編程技術包括:處理無效函數參數和數據、出現問題的時候程序失敗、檢查臨界函數返回的錯誤代碼以及處理異常。需要防禦性編程的標準問題包括:錯誤的輸入數據、內存或者硬盤空間不夠、不能打開一個文件、外部設備不能訪問、網絡連接不上或者甚至在程序中還有錯誤,目的是保持程序的運行狀態。如果你的程序是防禦性的,別忘了使用斷言。如果你使用斷言,也別忘了防禦性編程。這兩種技術最好結合在一起使用。

二、跟蹤語句

1、基本概念

  跟蹤語句(trace statements)可使程序執行,並使程序員可對可變值進行查看。它們提供了一個用於觀察的程序,並且獨立於一個交互式的調試器,但是最具有特色的是它們常用於對調試器提供的信息進行補充。在VC中,跟蹤消息通常輸出到輸出窗口中的Debug標籤,也可以重新輸出到一個文件中。跟蹤語句的特性如下:

.跟蹤語句用於報告代碼中重要的運行事件。

.跟蹤語句的編譯通常是有條件的,並只存在於調試版本中,而在發佈版本中不被編譯。

.跟蹤語句不能包含程序代碼或對程序代碼有間接的影響作用。

.跟蹤語句的目的是向程序員提供信息,而不是向用戶。

跟蹤語句也是調試語句,它可以執行程序,並且在運行中程序員可以查看變量。跟蹤語句對於那些使用交互式調試器很難調試的程序是很有效的。

跟蹤語句和斷言的區別如下:

.跟蹤語句是無條件的,斷言是有條件的布爾語句。

.跟蹤語句用於顯示程序執行和變量值,不直接顯示bug,斷言用於顯示出bug。

.跟蹤語句將信息輸出到調試窗口或文件中,可被隨意地忽略,斷言打斷程序的執行。

2、MFC中的跟蹤語句

  在MFC中,你可以使用TRACE和AfxOutputDebugString宏、CObject::Dump虛擬函數和AfxDumpStack函數。TRACE宏由AfxDump實現,AfxDump由AfxOutputDebugString實現。AfxOutputDebugString宏和AfxDumpStack函數可以在所有版本中編譯,其他只能在調試版本中編譯。

(1)TRACE宏有以下形式:

_TRACE(reportType,format);

_TRACE0(reportType,format,arg1);

_TRACE1(reportType,format,arg1,arg2);

_TRACE2(reportType,format,arg1,arg2,arg3);

_TRACE3(reportType,format,arg1,arg2,arg3,arg4);

在MFC中,推薦使用TRACEn宏,當使用TRACE宏時需要使用_T宏來格式化參數以正確解決Unicode的校正,而TRACEn不需要。

MFC TRACE宏中的一個缺點是AfxTrace函數使用一個512字符固定大小的緩衝區,這使得它在跟蹤長字符串時是無用的。

(2)CObject::Dump

CObject類有一個轉儲(dump)虛擬函數,所有繼承CObject的類都可以通過重載這個函數,輸出它們的值。

3、Visual C++消息Pragma

消息Pragma實際上是一個編譯時的跟蹤語句,你可以使用它來警告在預處理過程中發現的潛在的編連(build)問題。典型的例子:

#if (WINVER>=0x0500)

#pragma message (“NOTE:WINVER has been defined as 0x0500 or greater.”)

#endif

  消息Pragma是非常有用的,尤其是在複雜編連中。然而,如果你要檢測一種特定的問題,而不是潛在的問題,使用#error預處理來代替打斷編譯會更直接一些。

  每當你的程序中有錯誤而你想得到更多信息的時候,你應該去查看一下跟蹤消息。由於VC輸出窗口的緩衝區是有大小限制的,因此如果跟蹤消息數據產生的速度超過輸出窗口處理的速度,那麼消息會塞滿緩衝區,導致數據丟失。避免這個問題的簡單方法是在輸出大量數據的代碼段如轉儲對象時,調用Sleep API函數。

三、異常

1、基本概念

  錯誤是一種條件,在這種條件下,如果不執行額外的處理,線程就不能正常地執行下去。異常是用於處理錯誤的。使用異常的一個很明顯的好處就是它們通過發出錯誤信號,可以讓程序代碼和錯誤處理代碼分開,而且不會讓程序忽略錯誤,你不用不斷地檢查函數的返回值,因此它們將程序代碼簡單化。另一個好處是它們不需要嚴格的編程作風。

異常的基本特性:

.異常是基於每個進程而提出並處理的。

.異常不能被線程忽略,必須被處理。

.未處理的異常會使進程結束,而不僅僅是結束線程。

.異常出來在釋放棧時會釋放所有的棧對象,避免了資源的漏洞。

.異常處理需要大量的額外操作,使得它不適於經常運行的代碼。

.可以拋出任何類型的異常對象,除了整數。

如果正確執行,異常處理有下面的特性:

.異常是不是正常的運行結果,是特殊情況。

.異常在返回值無效的情況下使用。

.異常是可靠的,不可能被忽略。

.異常簡化了錯誤處理,簡化了程序代碼,使錯誤處理更加方便。

  Visual C++的默認情況下,在調試版本中處理異常,而在發佈版本中並不進行處理。由於異常也是錯誤,Windows異常碼採用了同Windows錯誤碼一樣的位映射模式,爲一個32位的值,這些碼由Microsoft定義,任何異常碼的最高四位總是1100(二進制),即十六進制裏的0xC。

2、Windows結構異常和C++異常

  Windows結構異常作爲硬件異常(如訪問非法或被零除)或操作系統異常的結果被拋出,C++異常只能由throw語句拋出。Windows結構異常處理不能處理對象的解析,因此你應該在C++程序中一直使用C++異常。然而,C++異常不能處理硬件和操作系統異常,你的程序需要將結構異常轉化爲C++異常。C++異常並不直接從你的程序代碼中拋出而是從C++運行庫中拋出,因此你需要調用棧窗口來返回你的代碼。爲了正確處理硬件和操作系統異常,你可以創建自己的異常類並使用_set_se_translator函數安裝一個結構異常向C++異常的轉化器,但不要捕獲那些不能恢復所產生問題的轉化後的結構異常。

3、MFC中的異常

  在MFC中,所有的異常對象都是從CException基類(它有使用起來非常方便的GetErrorMessage和ReportError成員函數)中派生來的。大多數的MFC異常對象都是動態分配的,而且當它們被捕獲時,必須被刪除,而沒有被捕獲的MFC異常由MFC本身在AfxCallWndProc函數中捕獲並刪除。

4、異常的開銷

  當拋出C++異常時,函數調用鏈將從此回溯搜索,尋找可以處理拋出這類異常的處理器。若沒找到,進程結束。如果找到,調用棧將被釋放,所有的自動(局部)變量也將釋放,然後棧將被整理爲異常處理器的上下文相關設備。因此異常開銷由一個異常處理器目錄和一個活動的自動變量表(它需要額外的代碼、內存,而且不論異常是否拋出,都會運行),還得加上函數調用鏈的搜索、自動變量的解析和棧的調整(它只在拋出異常的時候需要執行)組成。

5、異常策略

(1)拋出時機

  拋出異常的時機應該是一個函數發現一個錯誤,如果沒有一些特殊的操作,該錯誤能阻止程序正常的運行,而這種操作它自己不能完成,或是在函數不可能有返回值的時候。

  使用異常處理更簡單,更可靠,更有效,可以創建更健壯的代碼。然而,應該只在意外的情況下使用異常處理。如果你認爲一個指針應該是空值,這種條件下就直接在代碼中檢查這個值,而不要使用異常。

(2)何時捕獲

對於這個問題,有一些可能的標準:

.當函數知道如何處理這個異常時。

.當這個函數可以合理地處理這個異常而高級的函數不知道如何處理時。

.當拋出異常可能使進程崩潰時。

.當函數可以繼續執行它的任務時。

.當需要整理分配好的資源時。

  異常處理的一個缺點是它可能導致資源的泄露。因此,防止資源泄露更應該是保持程序異常安全的一部分。棧釋放時會自動整理局部變量,但不包括動態分配的變量。可以使用智能(smart)指針來保護你的代碼在存在異常的情況下不會產生資源泄漏。

(3)怎樣捕獲

.非MFC的C++異常應該通過引用來捕獲。使用引用捕獲異常不需要刪除異常對象(因爲使用引用捕獲的異常會在棧中傳送),而且它保留了多態性(因此你捕獲的異常對象正是你拋出的異常對象)。

.MFC異常應該通過指針來捕獲。使用指針捕獲異常需要你刪除對象。因爲它們通常從堆中分配,當你處理完異常之後,需要調用Delete成員函數來刪除。你不可以使用省略捕獲處理器捕獲MFC異常,這會導致一個內存泄露。必須使用Delete成員函數刪除MFC異常,而不用delete,因爲一些MFC異常爲靜態對象創建。

在釋放棧的過程中拋出異常會導致進程的終止。釋放棧涉及到調用析構函數,異常可以阻止調用delete操作符,這樣會有資源泄漏,因此異常最好不要從析構函數中拋出。如果非要在析構函數裏拋出異常,必須妥善處理,避免資源泄漏。

6、異常與防禦性編程

  在異常發生時繼續執行程序,遠比執行一個正常的關閉動作要重要。如果可能,應該將精力集中在繼續執行程序,並在必須的情況下才正常地關閉程序。可能最根本的正常關閉是一個在崩潰時可以重新啓動自己的進程,這是Windows資源管理器使用的一種技術。

  如果一個與錯誤相關的C++異常是可預料的,如果它發生在非關鍵性的代碼中,如果它不是發生在程序啓動或結束過程中或一個不可恢復的結構異常的結果中,這個程序就可以從其中恢復。

  一旦你的程序可以從與錯誤相關的異常中恢復,應該先檢查程序的狀態和它的文檔。如果程序和文檔已經被破壞了,進程也應該終止運行。否則,程序需要通知客戶機確定動作的過程。如果客戶機同意執行下去,程序應該恢復錯誤並繼續執行。

四、返回值

  並不是在所以場合下都能使用異常,如在使用Windows API編程或帶有COM編程時並不使用異常。在異常不適合的時候,使用返回值是一個好的辦法。

返回值的基本特性:

.返回值可以指示正常和不正常的函數運行,但不能阻止線程的繼續運行。

.返回值很容易被忽略。

.返回值在典型情況下是一個整數,通常映射符合於一個預定義的值。

.返回值能高效地傳遞和接收。

因此,返回值最適合用於以下的情形:

.用於非錯誤的狀態信息

.用於大多數情況下可以隨意忽略而不會出問題的錯誤。

.用於更易於出現在循環中的錯誤。

.用於中間語言模塊如COM組件中的錯誤。



  
使用Visual C++調試器調試
 
一、調試版本與發佈版本

  有時程序能在調試版本運行但不能運行於發佈版本,反之也有可能。一般說來,一個發佈版本意味着某些類型的優化,而一個調試版本則沒有優化。下面我們來看看它們的區別:

1、特別針對調試版本的編譯選項

(1)/MDd,/MLd或者/MTd 

  調試版本的運行時刻庫有調試符號,使用了調試堆,調試堆的目的是發現內存破壞和內存泄漏,並且向用戶報告源代碼的哪個地方出了問題。特性:

.調試版本的運行時刻庫對內存的分配作了跟蹤,允許用戶檢查內存泄漏。

.在剛分配的內存裏寫上0xCD的字節模式,用0xCD來填充剛分配的內存,有助於發現數據未被初始化的錯誤。

.在被釋放的內存寫上0xDD的字節模式,有助於發現已被釋放的內存。

.在緩衝區的兩邊分配了四字節的保護數據,並用0xFD的字節模式作初始化,來檢查寫內存的上溢出和下溢出。

.在每個內存分配的地方對源代碼文件名和行號作了記錄,有助於用戶在源代碼中對內存分配進行定位。

(2)/Od

  這個選項用來關閉優化開關。因爲未被優化的代碼直接對應於源代碼,所以比優化後的代碼更容易讀懂。未被優化的代碼編譯和鏈接會更快,會有更短的調試周期。而由於優化,發佈版本不見得會比調試版本運行得好,優化代碼要求編譯器做一些假設,去除冗餘,但有時這個假設是錯誤的,並且去掉的冗餘也有可能隱藏錯誤。如發佈版本的幀指針(EBP寄存器)省略(FPO)隱藏了函數原型不匹配的錯誤;在同步異常模式(只能由throw語句拋出,編譯器默認,由/GX編譯選項設置)下,異常處理程序可能被優化掉,會阻止程序中的C++異常處理代碼安全地捕獲結構異常,在這種情況下,你必須使用異步異常模式(採取任何指令都會產生異常的機制,由/Eha編譯選項設置)。

(3)/D “_DEBUG”

  打開條件編譯調試代碼開關。只有這個符號被定義,調試代碼纔會被編譯,MFC使用_DEBUG符號來確定到底鏈接的是哪個版本的MFC類庫。在調試版本中,內聯默認情況下是被關閉的。

(4)/ZI

  創建編輯繼續(Edit and Continue)的程序數據庫。這個選項會打開/GF編譯選項,/GF編譯選項會消除重複字符串,並將字符串放到只讀內存。編輯繼續功能需要獲取存儲在PDB文件裏的特殊信息來使得代碼的修改對調試器有效。如果被修改文件對應的信息不在PDB文件裏,編輯繼續功能就不能進行,而且在調試過程中對代碼的任何修改都會出現下面的提示信息“One or more files are out of date or do not exist.”。

(5)/GZ

在調試版本中用來發現那些在發佈版本里才發現的錯誤。其作用如下:

.用0xCC模式初始化自動(本地)變量。

.在通過函數指針調用函數時,檢查棧指針,確認是否有調用規則不匹配。

.在函數最後檢查棧指針是否被改變。

(6)/Gm

  打開最小化重新鏈接開關,減少鏈接時間。

2、特別針對發佈版本的編譯選項

(1)/MD,/ML或者/MT

  使用發佈版本的運行時刻庫。

(2)/O1或者/O2 

  打開優化開關,使得程序會最小或說速度會最快,優化器還可能發現代碼中潛在的錯誤,而這些錯誤可能會被調試版本掩蓋。

(3)/D “NDEBUG” 

  關閉條件編譯調試代碼開關。

(4)/GF

  消除重複字符串並將它們放到只讀內存中以避免被錯誤地修改。

(5)/Zi

創建包含調試符號的程序數據庫。

  如果一個錯誤只發生在發佈版本里,除非你是個彙編高手,否則你需要調試符號來提示你到底程序出現了什麼問題,調試符號保存在程序的數據庫文件(PDB)中。Visual C++的AppWizard默認情況下沒有爲發佈版本創建調試符號。爲創建調試符號,打開工程設置對話框,選擇Win32 Release,在C/C++標籤裏選擇Common類,在調試信息裏,如果是發佈版本選擇Program Database,如果是調試版本選擇Program Database for Edit and Continue(編輯繼續選項與優化鏈接不相容,不適於發佈版本)。在Link標籤裏選擇Debug類,然後選擇Debug Info和Microsoft format選項,最好不要選擇Separate types選項,這樣所有的調試信息纔會被合併到單獨的一個PDB文件中。對於發佈版本,選擇Link標籤,在Project options對話框的最後加上“/OPT:REF”,這個選項使得不被引用的函數和數據不會出現在可執行文件中,避免了文件的無謂增大。對於調試版本不要使用這個選項,它會關閉增量鏈接(incremental linking)。

二、Visual C++編輯器的“設置”菜單

  當你打開或新建一個包含至少一個工程的Workspace後,Visual C++的Project菜單中的“Settings…”命令就變爲有效,選擇它或者按下熱鍵Alt+F7後,便可調出工程設置對話框,這裏面的選項將影響整個工程的建立和調試過程,因此很重要。

  在這個對話框中,左上方的下拉列表框用於選擇一種工程配置,包括有Win32 Debug、Win32 Release和All Configurations(指前兩種配置一起),某些選項在不同的工程配置中有不同的缺省值。左邊的樹形視圖給出了當前工程所有的文件及分類情況。下面我們就以Win32 Debug爲例來看看與工程有關的的四個主要選項卡的各自功能與含義(一共有十個選項卡):

1、 General選項卡

  這個選項卡比較簡單,從上向下的第一個選項用於更改使用MFC類庫的方式: DLL的方式或是靜態連接。我們可以在兩種方式之間進行切換。第二個選項用於指定在編譯連接過程中生成的中間文件和輸出文件的存放目錄,對於調試版本來說,缺省的目錄是工程下面的“Debug”子目錄。第三個選項用於指定是否允許每種工程配置都有自己的文件依賴關係(主要指頭文件),由於絕大多數工程的調試版本和發佈版本都具有相同的文件依賴關係,所以通常不需要更改該選項。

2、 Debug選項卡

  Debug選項卡中是一些與調試有關的選項,由於選項比較多,它們被分成了幾個類,我們可以從Category中選擇不同的類別,選項卡就會切換顯示出相應的選項。

  在General類別中,可以指定要調試的可執行文件名。另外三個選項可以指定用於調試的工作目錄,開始調試時給程序傳送的命令行參數,以及進行遠程調試時可執行文件的路徑。

3、C/C++選項卡

  C/C++選項卡控制着Visual C++的編譯器,其中的選項比較多。下面有一個Project Options編輯框,裏面列出的各種命令開關將會在開始編譯時作爲命令行參數傳送給Visual C++的編譯器。這些命令開關會跟隨其它選項改變而改變。

  在General類別中,Warning level用於指定編譯器顯示警告的級別,如果選中了Warnings as errors,那麼顯示的每一個警告都將會引起一個錯誤,這樣在編譯完畢後就無法啓動連接器來進行連接。Optimizations用於設置代碼優化方式,優化的目的主要有提高運行速度和減小程序體積兩種,但有時候這兩種目的是相互矛盾的。另外,在極少數情況下,不進行優化,程序能正常運行,打開了優化措施之後,程序卻會出現一些莫名其妙的問題。其實這多半是程序中有潛在的錯誤,關閉優化措施往往只是暫時解決問題。Debug info用於指定編譯器產生的調試信息的類型,爲了使用Visual C++的即編即調功能,必須在這裏選擇生成“Program Database for Edit and Continue”類型的調試信息。Preprocessor definitions是一些預先定義的宏名。

  C++ Language類別中的選項涉及到了C++語言的一些高級特性,包括有成員指針的表示方式、異常處理、運行時類型信息,一般情況下都不用改變它們。Code Generation類別中的選項涉及如何生成目標代碼,一般情況下保持缺省值即可。在Customize類別中,從上到下六個選項的含義分別爲:是否禁止使用Microsoft對C++的擴展;是否允許函數級別的連接;是否消除重複的字符串;是否允許進行最小化的重建;是否允許遞增編譯方式;是否允許編譯器在開始運行時向Output窗口中輸出自己的版本信息。

  在Listing Files類別中,我們可以指定編譯器生成瀏覽信息和列表文件(Listing file),前者可由瀏覽信息維護工具BSCMAKE生成瀏覽信息文件,後者則包含了C/C++源文件經過編譯後對應的彙編指令。Optimizations類別允許我們對優化措施進行更細微的控制,選擇了Customize後,便可以選擇進行哪幾項優化,在Inline function expansion中我們可以指定對內聯函數的擴展方式。Precompiled Headers類別中是關於預編譯頭文件的一些選項,一般情況下都不用更改。Preprocessor類別中是關於預處理的一些選擇。

4、Link 選項卡

  Link選項卡控制着Visual C++的連接器。在General類別中,可以指定輸出的文件名,以及一些在連接過程中需要使用的額外的庫文件或目標文件,下邊五個選項的含義分別爲:生成調試信息;忽略所有缺省的庫文件;允許遞增連接方式(這種方式可以加快連接的速度);生成MAP文件;允許進行性能分析。在Customize中選中Use program database允許使用程序數據庫。在Debug類別中,我們可以指定調試信息的類別是Microsoft的格式,還是COFF格式,或者兩種都有,選中Separate types後連接器會把調試信息分開放在PDB文件中,這樣連接起來會更快一些,但調試時速度卻會慢一些。Input類別中是一些與輸入庫文件有關的選項,我們可以在這裏指定使用或不使用某些庫文件或目標文件。Output類別中則是一些與最終輸出的可執行文件有關的選項,一般情況下都不用改變。

三、Visual C++調試工具

1、調試窗口

(1)觀察窗口(Watch)

  調試程序時,可使用觀察窗口監視變量和表達式。

(2)快速查看窗口(Quick watch)

  功能和觀察窗口差不多。

(3)變量窗口(Variables)

  變量窗口有三個標籤:Auto標籤顯示了當前語句和前一條語句用到的變量,Locals標籤顯示當前函數的局部變量,this標籤顯示了this指針執行的對象。

(4)寄存器窗口(Register)

  可以監視CPU的寄存器、標誌值以及浮點堆棧

(5)內存窗口(Memory)

  可顯示從一特定地址開始的虛擬內存。Address框允許你指定從哪個虛擬內存地址開始顯示。

(6)調用棧窗口(Call stack)

  可顯示引起當前源代碼語句執行的一系列函數調用,當前函數在堆棧的頂端。

(7)反彙編窗口(Disassembly)

  可查看編譯器生成的對應於源代碼的彙編指令。

2、調試符號

  程序數據庫文件(.pdb)包含了Visual C++調試器所需的調試信息和程序信息。調試信息包含了變量的名字和類型、函數原型、源代碼行號、類和結構的佈局、FPO調試信息(重建堆棧幀)以及進行增量鏈接所需的信息。對於設置了Program Database for Edit and Continue選項的程序,PDB還要包含執行編輯繼續功能所需的信息。

3、使用斷點

  斷點(BreakPoint)是運行你向調試器描述環境,並讓調試器設置好程序狀態的一種機制。如果沒有斷點,只有在程序裏一步一步跟蹤使用調試器。在Visual C++中,你可以設置三種類型的斷點:代碼定位斷點、數據斷點和消息斷點。

四、提高調試器的查錯能力

  儘量採用編譯時刻檢查而不是運行時刻檢查。

1、使用最高的編譯警告級別/W4

  象if(x=2)這樣的語句,默認的警告級別爲/W3時不顯示任何信息,但改成最高警告級別/W4時則會出現“waning C4706:assignment within conditional expression”的警告。/W4能給出一些/W3所不能給的警告。

2、在調試版本中使用/GZ編譯選項

  /GZ選項用來發現那些在發佈版本里才發現的錯誤,包括未被初始化的自動(局部)變量、堆棧錯誤、不正確的函數原型等。

3、使用#pragma warning編譯器指示

  你可以使用#pragma warning編譯器指示來禁止整個程序、特定的頭文件、特定的代碼文件或是特定的某一行代碼的特定警告,這看你把#pragma放在哪裏。

4、使用沒有警告的編譯法則/WX

  這個編譯選項把所有的警告當成錯誤來對待,只有在假警告被消除之後才能應用。有時編譯警告可能是合理的,處理編譯警告的核心是要發現錯誤,而不是抑制警告本身。這個法則對於大的程序開發小組來說很有幫助。最終目標是消除錯誤,而不是消除警告。

五、內存空間與分配

1、內存分配錯誤

  動態內存分配錯誤有兩種基本類型:內存錯誤和內存泄漏。

(1)內存錯誤

  當一個指針或者該指針所指向的內存單元成爲無效單元,或者內存中分配的數據結構被破壞時,就會造成內存錯誤。指針未被初始化,指針被初始化爲一個無效地址,指針被不小心錯誤地修改,在與指針相關聯的內存區域被釋放後使用該指針(這種指針被稱爲虛懸(dangling)指針),這些都會使指針變爲無效指針。當通過一個錯誤指針或者虛懸指針對內存進行寫入,或者將指針強制轉換爲不匹配的數據結構,又或者是寫數據越界,內存自身也會遭到破壞。刪除未被初始化的指針、刪除非堆指針、多次刪除同一指針或者覆蓋一個指針的內部數據結構,都會造成內存分配系統錯誤。

(2)內存泄漏

  內存泄漏在被動態分配的內存沒有被釋放時產生。有許多情況會導致內存泄漏,如沒有在程序的全部執行路徑中釋放內存,沒有在析構函數中釋放所有的內存等。一個程序在崩潰之前可運行的時間越長,則導致崩潰的原因與內存泄漏的關係越大。

  Windows會在程序結束的時候將泄漏的內存收回,因此內存泄漏是個暫時性的問題。但爲什麼必須消除內存泄露呢?首先,內存泄漏往往會導致系統資源的泄漏。動態分配內存往往不僅僅代表一塊存儲區域,還代表了某些類型的系統資源,如文件、窗口、設備上下文、GDI對象等。其次,高質量的程序和特定的服務器程序必須能夠無限地運行下去。最後,內存泄漏往往是其他程序錯誤或不良編程習慣的徵兆。

  導致內參泄漏的原因:忘記釋放內存;構造函數失敗;存在內存泄漏的析構函數;存在內存泄漏的異常處理程序;多個返回語句;使用錯誤形式的delete。

2、關於內存的初始化

  在調試版本里,堆裏未被初始化的內存被0xCD字節模式填充,堆裏釋放的內存被0xDD字節模式填充。堆棧裏被初始化的內存被0xCC字節模式填充。調試版本和發佈版本里,未被初始化的全局內存都被初始化爲0。

3、內存虛擬地址空間

  Windows使用一組固定的範圍來分割進程的4GB虛擬地址空間,因此有時可通過查看指針的返回值來判斷指針是否有效。

(1)Windows2000虛擬地址空間劃分

0~0XFFFF(64KB):不能用來檢測空指針賦值(訪問衝突)

0x10000(64KB)~0x7FFEFFFF(2GB-64KB):Win32進程私有的(非保留的),用於程序代碼和數據

0x7FFF0000(2GB-64KB)~0x7FFFFFFF(2GB):不能用來防止覆蓋OS分區(訪問衝突) 

0x800000000(2GB)~0xFFFFFFFF(4GB):爲操作系統保留,不可訪問(訪問衝突)

(2)Windows2000虛擬地址空間使用

0x00030000~0x0012FFFF:線程棧

0x00130000~0x003FFFFF:堆(有時堆位於此處)

0x00400000~0x005FFFFF:可執行代碼

0x00600000~0x0FFFFFFF:堆(有時堆位於此處)

0x10000000~0x5FFFFFFF:App DLLs、Msvcrt.dll、Mfc42.dll

0x77000000~0xFFFFFFFF:Advapi32.dll、Comctl32.dll、Gdi32.dll、Kernel32.dll、Ntdll.dll、Rpcrt4.dll、Shell32.dll、User32.dll

其中,0x00400000是所有版本的Windows能使用的最低基地址。

六、一些調試技術

1、調試死循環

  使用Debug菜單下的Break命令。在Windows2000中,如果程序有輸入請求,可以使用F12鍵中斷程序,然後檢查窗口的調用棧,或單步跟蹤代碼找到死循環的發生原因。

2、用Spy++調試與消息有關的問題

  調試消息的最好方案是使用Visual C++提供的Spy++工具。Spy++允許程序員查看窗口、消息、進程和線程。Spy++默認的消息輸出:第一欄顯示行號。第二欄顯示接受消息的句柄。第三欄中的“S”表示消息是用SendMessage發出的,“P”代表消息是由PostMessage發出的,“R”是消息句柄的返回值。第四欄給出解碼後的消息名,消息參數或返回值。

3、非常規方法

(1)重新編連你的應用程序

  當你的程序表現出異常的或意外的行爲,或者Visual C++編譯器因爲一個內部編譯器錯誤而失敗時,最好刪除工程中的Debug或Release文件夾,從頭開始重新進行編連。

(2)重新啓動Visual C++

  Visual C++有超強的能力,但編譯器的某些特性也會引起奇怪的錯誤。如果你的程序表現得很奇怪,你可是試着清除所有的斷點,關閉或隱藏觀察窗口,檢查工程設置對話框看最近做了什麼修改,直至重新啓動Visual C++以便消除由於Visual C++環境引起的異常行爲。

(3)重新啓動Windows

  當你發現Windows或者其他程序表現出異常的或出人意料的行爲時,就應該重新啓動Windows,以消除操作系統給調試帶來的干擾。 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章