程序員們經常有這樣一個誤區,即認爲產品級軟件的種種限制也適用於開發中的軟件。產品級的軟件要求可以快速的運行,而開發中的軟件則容許運行緩慢。產品級的軟件要節約適用資源,而開發中的軟件在使用資源時能夠比較奢侈。產品級的軟件不該向用戶暴露可能引發危險的操做,而開發中的軟件則能夠提供一些額外的、沒有安全網的操做。程序員
我曾參與編寫的一個程序中大量地使用了四重鏈表。鏈表的代碼是很容易出錯的,鏈表自己的結構很容易損壞。所以我給程序加了一個菜單項來檢測鏈表的完整性。編程
在調試模式下,Microsoft Word 在空閒循環中加入了一些代碼,它們每隔幾秒中就檢查一次 Document 對象的完整性。這樣既有助於快速檢測到數據的損壞,也方便了對錯誤的診斷。安全
應該在開發期間犧牲一些速度和對資源的使用,來換取一些可讓開發更順暢的內置工具。工具
你越早引入輔助調試的代碼,它可以提供的幫助也越大。一般,除非被某個錯誤反覆地糾纏,不然你是不肯意花精力去編寫一些調試輔助的代碼的。然而,若是你一遇到問題立刻就編寫或使用前一個項目中用過的某個調試助手的話,它就會自始至終在整個項目中幫助你。性能
應該以這麼一種方式來處理異常狀況:在開發階段讓它顯示出來,而在產品代碼運行時讓它能自我恢復。debug
下面列出一些可讓你進行進攻式編程的方法。版本控制
確保斷言語句使程序終止運行。不要讓程序員養成壞習慣,一碰到已知問題就按回車鍵把它跳過。讓問題引發的麻煩越大越好,這樣才能被修復。指針
徹底填充分配到的全部內存,這樣可讓你檢測到內存分配錯誤。調試
徹底填充已分配到的全部文件或流,這樣可讓你排查出文件格式錯誤。日誌
確保每個case語句中的default分支或者else分支都能產生嚴重錯誤(好比讓程序終止運行),或者至少讓這些錯誤不會被忽視。
在刪除一個對象以前把它填滿垃圾數據。
讓程序把它的錯誤日誌文件用電子郵件發給你,這樣你就能瞭解到在已發佈的軟件中還發生了那些錯誤——若是這對於你所開發的軟件使用的話。
有時候,最好的防守正式大膽進攻。在開發時慘痛地失敗,能讓你在發佈產品後不會敗的太慘。
若是你是寫程序給本身用,那麼把調試用的代碼都留在程序裏可能並沒有大礙。但若是是商用軟件,則此舉會使軟件的體積變大且速度變慢,從而給程序形成巨大的性能影響。要事先作好計劃,避免調試用的代碼和程序代碼糾纏不清。下面是一些能夠採用的方法。
使用相似ant和make這樣的版本控制工具好make工具
版本控制工具能夠從同一套源碼編譯出buto9ng版本的程序。在開發模式下,你可讓make工具把全部的調試代碼都包含進來一塊兒編譯。而在產品模式下,又可讓make工具把那些你不但願包含在商用版本中的調試代碼排除在外。
使用內置的預處理器
若是你所用的編程環境裏有一個預處理器——好比C++開發環境——你能夠用編譯器開關來包含或排除調試用的代碼。你既能夠直接使用預處理器,還能夠寫一個能與預處理器指令同時使用的宏。下面是一個直接使用預處理器的例子:
#define DEBUG #if defined(DEBUG) // debugging code #endif
這一用法能夠有幾種變化。好比說,除了能夠直接定義DEBUG之外,還能夠給他賦一個值,而後就能夠判斷其數值,而不只是去判斷它是否已經定義了。這麼作可讓你區分不一樣級別的調試代碼。你可能但願讓某些調試代碼永遠留在程序裏,這是你就能夠用相似#if DEBUG > 0 這樣的語句把這些代碼括起來。另外一些調試代碼可能只是針對一些特定的用途,你能夠用相似#if DEBUG == POINTER_ERROR這樣的語句把這些代碼括起來。在另一些地方,你可能想設置調試級別,這時就能夠寫相似#if DEBUG > LEVEL_A 這樣的語句。
若是你不喜歡讓#if defined() 一類語句散佈在代碼的各處,那麼能夠寫一個預處理器宏來完成一樣的任務。
#define DEBUG #if defined(DEBUG) #define DebugCode(code_fragment){code_fragment} #else #define DebugCode(code_fragment) #endif //根據是否認義DEBUG符號,可選擇是否編譯此處代碼 DebugCode(statemenmt 1; statement 2; ... statement n);
使用調試存根
不少狀況下,你能夠調用一段子程序進行調試檢查。在開發階段,該子程序可能要執行若干操做以後才能把控制權交還給其調用方代碼。而在產品代碼中,你能夠用一個存根子程序(stub routine)來替換這個複雜的子程序,而這段stub子程序要麼當即把控制權交換調用方,要麼是執行幾項快速的操做就返回。這種方法僅會帶來很小的性能損耗,而且比本身編寫預處理器要快一些。把開發版本和產品版本的stub子程序都保留起來,以便未來能夠隨時在二者之間來回切換。
你能夠先寫一個檢查傳入指針是否有效的子程序。
void DoSomething(SOME_TYPE* pointer) { //check parameters passed in CheckPointer(pointer); }
在開發階段,CheckPointer() 子程序會對傳入的指針進行全面檢查。這一檢查可能至關耗時,但必定要很是有效,好比說這樣:
void CheckPointer(void* pointer) { //執行第1項檢查——多是檢查它不爲NULL //執行第2項檢查——多是檢查它的地址是否合法 //執行第3項檢查——多是檢查它所指向的數據無缺無損 //... //執行第n項檢查——... }
當代碼準備穩當,即將要編譯爲產品時,你可能不但願這項指針檢查影響性能。這時你就能夠用下面這個子程代替前面那段代碼:
void CheckPointer(void* pointer) { //no code; just return to caller. }
防護式編程中存在這麼一種矛盾的觀念,即在開發階段你但願錯誤能引人注意——你寧願看它的臉色,也不想冒險去忽視他。但在產品發佈階段,你卻想讓錯誤儘量地偃旗息鼓,讓程序能十分穩妥地恢復或中止。下面給出一些指導性建議。
保留那些檢查重要錯誤的代碼
你須要肯定程序的哪些部分能夠承擔未檢測出錯誤而形成的後果,而哪些部分不能承擔。
去掉檢測細微錯誤的代碼
若是一個錯誤帶來的影響確實微乎其微的話,能夠把檢查它的代碼去掉。
去掉能夠致使程序硬性崩潰的代碼
當你的程序在開發階段檢測到了錯誤,你確定想讓它儘量地引人注意,以便能修復它。實現這一目的最好的方法一般是讓程序在檢測到錯誤後打印出一份調試信息,而後崩潰退出。這種方法甚至對於細微的錯誤也頗有用。
然而在生成產品的時候,軟件的用戶須要在程序崩潰以前有機會保存他們的工做成果,爲了讓程序給他們留出足夠長的保存時間,用戶甚至會忍受程序表現出的一些怪異行爲。相反,若是 程序中的一些代碼致使了用戶工做成果的丟失,那麼不管這些代碼對幫助調試程序並最終改善程序質量有多大的貢獻,用戶也不會心從存感激的。所以,若是你的程序裏存在着可能致使數據丟失的調試代碼,必定要把它們從最終軟件產品中去掉。
保留可讓程序穩妥地崩潰的代碼
若是你的程序裏有可以檢測出潛在嚴重錯誤的調試代碼,那麼應該保留那些能讓程序穩妥地崩潰的代碼。
爲你的技術支持人員記錄錯誤信息
能夠考慮在產品代碼中保留輔助調試用的代碼,但要改變他們的工做方式,以便與最終產品軟件相適應。
確認留在代碼中的錯誤消息是友好的
若是你在程序中留下了內部錯誤消息,請確認這些消息的用於對用戶而言是友好的。