調試策略
第一章 調試的過程
1. 成功而高效的調試的關鍵是找到準確的錯誤資訊
2. 一旦找到一個錯誤,就可能找到更多。類似的代碼可能還有類似的錯誤
3. 從錯誤中學習如何預防將來會產生的錯誤
4. 對於新代碼,根本不需要執行測試來判斷它是否有錯誤
第二章 編寫便於調試的C++代碼
C++語言和編程風格
1. 在需要的時候使用語言的進階特性
2. 要寫出能被“人”理解的代碼,不僅是編譯器
3. 慎用匈牙利命名法
4. 每一個語句行都應該作為一個單獨的原子單位,這樣可以充分利用調試工具
5. 如果你不能確定是否需要括弧,那麼就需要括弧
6. 使用C++自身特性防止錯誤的方法:
用const代替#define來建立常量;
用enum代替#define來建立常量集合;
用inline函數代替#define宏;
用new和delete代替malloc和free;
用I/O Streams代替Standard I/O。
7. 在標頭檔重聲明所有的共用外部符號,並且保留函數原形中的參數名以便於理解
8. 經常建立Release版本來協助檢查未初始化的變數,因為變數的使用是由最佳化器來檢查的
9. 用Limits.h中的最大/小值作為整數資料型別的大小限制,浮點類型的最大/小值定義在Float.h中
10. 在VC++中預設情況下字元類型是有符號的[-128 ~ 127]
11. 不在迫不得已的情況下,不要使用強制類型轉換
dynamic_cast:用於多態類型的轉換(必需開啟編譯器的RTTI)
static_cast:用於非多態類型的轉換
const_cast:用於去除const、volatile和__unaligned(x64)屬性
reinterpret_cast:用於轉換不相容的資料類型,如指標和非指標
12. 在程式中仔細地使用const能夠在編譯的時候發現更多的錯誤
13. 不要再程式完成後再來添加const,要一開始就正確且嚴格地使用const
14. 如果每次迴圈都需要增加/減小迴圈變數,就是用for而不是while
15. 處理可能出錯的建構函式的方法:
一、在建構函式中進行不會出錯的基本初始化,在另一個函數(Init)中執行可能出錯的初始化操作。
二、使用異常機制捕捉初始化過程中的異常,然後釋放已經初始化的資源(也可以使用auto_ptr智能指標)並拋出異常
16. 如果在處理異常的時候調用了某個解構函式,並且該解構函式產生了未處理的異常,則程式將被終止。因為在棧展開的過程中拋出的異常會終止整個程式
17. 由於No.16所述原因,所以解構函式中的異常一定要在解構函式中得到處理
18. 如果一個類需要解構函式來釋放建構函式分配的資源,那麼要麼提供拷貝建構函式和賦值運算子,要麼使用private避免它們被自動添加
19. “大三法則”:如果一個類需要一個解構函式,或者一個拷貝建構函式,或者一個複製運算子,那麼它就三個都需要
Visual C++編譯器
1. 總是使用/W4警告層級
2. 在調試版中使用/GZ選項,可以協助發現在Release版裡才能發現的錯誤
3. 使用#pragma warnning來控制和調整特定的警告(具體參見VC++文檔),使用#pragma時最好給出明確的注釋
4. 在消除編譯警告時要仔細檢查原因,不是直接原因,而是隱藏的導致警告的根本原因。最終目標是消除錯誤,而不是消除警告
第三章 使用斷言
1. 斷言只能用於檢查有效性,而不是正確性
2. 保持斷言的簡單性。好的覆蓋
3. 在使用C語言執行階段程式庫中的斷言時,使用_ASSERTE而不是_ASSERT;使用MFC庫時使用ASSERT宏
4. 在ATL程式中使用ATLASSERT可以讓你使用自訂的斷言
5. VERIFY一般用來檢測Windows API的傳回值(建議不使用VERIFY宏,而使用ASSERT)
6. 在使用CObject類的衍生類別對象前調用ASSERT_VALID宏
7. 公有函數比私人和保護函數需要更全面的斷言
8. 給那些不是這麼顯而易見的斷言做出清晰的注釋
9. 任何垃圾輸入都不應該導致垃圾輸出
第四章 使用跟蹤語句
1. Windows API: OutputDebugString函數,可在調試/發布版本中運行,適合跟蹤啟動/結束
2. VC++的C運行時: _RPTn和_RPTFn系列函數
3. MFC中的TRACE(n):
AfxOutputDebugString宏: 調試版中等於_RPT0,發布版中等於OutputDebugString
CObject::Dump函數:
AfxDump全域對象:
AfxDumpStack函數:
4. ATL跟蹤語句: AtlTrace和AtlTrace2函數
第五章 使用異常和傳回值
1. 不要使用異常處理來屏蔽不可重獲得錯誤
2. 異常是基於每個線程而提出和處理的,但是未處理的異常會使整個進程結束
3. 異常處理需要大量的額外操作,因此它不適合用於經常啟動並執行代碼
4. 經常發生的情況很可能並不是真正的錯誤
5. 以下幾種情形適合使用傳回值(而不是異常):
用於指示非錯誤的狀態資訊
用於大多數情況下可以隨意忽略而不會出問題的錯誤
用於更易於出現在迴圈中的錯誤(異常需要更多額外開銷)
用於中繼語言模組中的錯誤,如COM
6. 必須使用/Eha調試器選項才能捕獲使用C++異常機制的作業系統異常
7. 可以使用_set_se_translator函數來為一個線程安裝一個把SE轉換到C++異常的轉換器
8. 只有浮點溢出、零除和存取違規這三類SE應該捕捉,其它的SE是不可恢複的
9. 使用SetUnhandledExceptionFilter函數安轉一個程式崩潰處理器
10. 在很少出現異常的情況下使用異常處理機制不會影響效能,反而有可能提升效能
11. 拋出異常的時機:當一個函數發生錯誤時,並且如果不採取一些特殊措施函數就不能繼續運行,而這些措施它自己又不能完成的情況下,應該拋出異常
12. 捕獲異常的時機:
當函數知道如何處理這個異常時
當這個函數知道如何處理這個異常,而進階函數不知道如何處理時
當拋出異常可能使進程崩潰時
當需要整理分配好的資源時
13. 在C++標準庫中的<stdexcept>中定義了用於異常報告的標準類
14. 對只可能意外發生的錯誤情況使用異常
15. MFC的異常應使用指標來捕獲並適用Delete成員函數釋放,因為它們通常在堆中分配;其它C++異常應使用引用來捕獲,也不需要手動釋放,因為引用捕獲的異常會在棧中傳遞且保持了多態性
16. 異常規範(不能和模板混合使用):
Function()——可以拋出任何異常
Function throw()——不能拋出異常
Function throw(CException)——只能拋出以CException為基類的異常
注意:不推薦使用異常規範,它可能由於未預料的異常而導致程式崩潰,且VC不支援
17. 可以使用_controlfp函數來讓浮點數拋出異常
調試工具
第六章 在Windows中調試
1. 可以在VC調試器的監視中輸入“@ERR”來顯示最新的LastError值,也可以使用“@ERR,hr”來顯示錯誤碼對應的文本描述資訊。還可以通過FormatMessage函數將錯誤碼轉換到文字格式設定
2. 建立對應檔以便於事後調試,建立時添加/MAPINFO:EXPORTS編譯選項用於輸出匯出序號
3. 如果崩潰不是發生在你的代碼內部,可以通過堆棧轉儲資訊得到返回到你的代碼的地址
4. 使用Undname命令列工具可以用於解析對應檔中的混合函數名
第七章 使用VC++調試器調試
1.
2. 在調試版中,內聯預設是被關閉的
3. 為Debug版和Release版都建立偵錯符號,並將PDB檔案存檔
4. 必須對調試版和發布版都進行測試,不能假設他們運行起來是一樣的
5.
調試技術
第八章 基本調試技術
1. 使用Break功能來調試死迴圈
2. 在監看式視窗中使用@CLK,d @CLK=0來觀察每一步的執行時間
3. 在EAX或EAX+EDX寄存器中查看函數的傳回值或返回資料的指標
4. 可以通過GdiSetBatchLimit函數來設定或關閉GDI的批處理,以便於調試繪圖代碼
5. 使用GetAsyncKeyState函數來調試WM_MOUSEMOVE訊息
6. 使用Spy++調試訊息有關的問題
7. 任何一個調用了FromHandle函數返回的對象都不能跨訊息儲存,它只是MFC臨時建立的一個對象,隨時可能被銷毀
第九章 記憶體調試
1. 記憶體泄露往往是其它程式錯誤和不良編程習慣的徵兆
2. 應該總是讓new在失敗時拋出一個異常,而不是返回一個很容易被忽略的null 指標
3.
4. 使用庫函數可以擁有更多的控制能力和更好的可移植性,但是使用MFC的函數會稍微方便一些
5. 通過定義_CRTDBG_MAP_ALLOC來將程式中的所有堆函數映射成其調試版,這樣可以獲得帶有源檔案和行號的調試資訊
第十章 調試多線程程式
1. 使用InterLocked系列加鎖函數實現對單個整數變數的安全執行緒操作
2. 當對鎖的競爭不多時,使用臨界區比鎖的效率要高,但是臨界區僅限於同一進程內部使用,且不允許設定逾時參數,也不能同時申請多個臨界區的所有權
3. 在WatiForSingleObject函數中使用適當的逾時參數可以有效地避免死結
4. 在使用互斥鎖時(不是臨界區),使用WaitForMultipleObjects函數是避免死結最有效方法
5. 處理與時序有關的問題的最好方法是促使潛在錯誤的發生
6. 使用volatile關鍵字防止在多線程程式中的由編譯器最佳化導致的變數訪問錯誤
7. 建立一個“自動鎖”類,利用其建構函式和解構函式來擷取和釋放鎖資源,這樣即使在有異常發生的情況下也能正確地釋放鎖資源
8. 盡量使用最高層的線程建立和清理函數,從而避免產生資源流失和未處理的異常。例如使用MFC架構開發時就是用MFC提供的AfxBeginThread和AfxEndThread函數,而不是用CRT的_beginthreadex和_endthreadex函數,以及WindowsAPI的CreateThread和ExitThread函數
9. 如果線程中需要使用MFC,那麼這個線程必須由MFC建立
10. 關閉一個MFC線程的正確方法一(CWinThread對象會自己調用解構函式):
正確方法二(不讓CWinThread對象自析構):
11. 在多個並發執行的線程中使用OutputDebugString函數輸出調試資訊可能使線程的執行序列化
12. 當跟蹤一段可以被多個線程調用的代碼時,你不能假設你現在所處的上下文就是你剛才所處的環境。可以通過觀察局部變數的變化情況或者呼叫堆疊來判斷上下文是否已經切換,還可以查看線程對話方塊來確定當前線程
13. 阻塞函數的逾時時間是用真即時間來衡量的,而不是用有效CPU時間,中斷程式後的時間也會記入逾時時間,所以調試時經常會引起正常運行時不大可能發生的逾時錯誤
14. 在調試多線程代碼之前,先確定這個問題是否與版本、調試器、系統和處理器有關。從這些資訊中你可以知道當看是使用調試器的時候應該先試些什麼
15. 在出現錯誤的地方,適用線程對話方塊查看是否其它線程正與出錯線程處理相同的資料
16. 使用dw( @TIB + 0x24 )來查看當前線程的ThreadID,就可以免於線上程對話方塊和代碼視窗間頻繁切換(在WinXP中不一定是這個值,待確定)
第十一章 COM調試
第十二章 非常規策略