調試策略
第一章 調試的過程
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++調試器調試
2. 在調試版中,內聯默認是被關閉的
3. 爲Debug版和Release版都創建調試符號,並將PDB文件存檔
4. 必須對調試版和發佈版都進行測試,不能假設他們運行起來是一樣的
調試技術
第八章 基本調試技術
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在失敗時拋出一個異常,而不是返回一個很容易被忽略的空指針
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對象會自己調用析構函數):
11. 在多個併發執行的線程中使用OutputDebugString函數輸出調試信息可能使線程的執行串行化
12. 當跟蹤一段可以被多個線程調用的代碼時,你不能假設你現在所處的上下文就是你剛纔所處的環境。可以通過觀察局部變量的變化情況或者調用堆棧來判斷上下文是否已經切換,還可以查看線程對話框來確定當前線程
13. 阻塞函數的超時時間是用真實時間來衡量的,而不是用有效CPU時間,中斷程序後的時間也會記入超時時間,所以調試時經常會引起正常運行時不大可能發生的超時錯誤
14. 在調試多線程代碼之前,先確定這個問題是否與版本、調試器、系統和處理器有關。從這些信息中你可以知道當看是使用調試器的時候應該先試些什麼
15. 在出現錯誤的地方,適用線程對話框查看是否其它線程正與出錯線程處理相同的數據
16. 使用dw( @TIB + 0x24 )來查看當前線程的ThreadID,就可以免於在線程對話框和代碼窗口間頻繁切換(在WinXP中不一定是這個值,待確定)
第十一章 COM調試
第十二章 非常規策略