編譯程序僅僅能查找出程序的語法錯誤,而對於“數組越界訪問”,“對空指針解引用”等錯誤,編譯程序是束手無策的。同時我們知道測試人員所使用的黑箱測試方法所能做的只是往程序裡填數據,並看它彈出什麼。這就決定了對程序錯誤的檢測可能需要點運氣。
假如編譯程序能夠檢測出“數組越界訪問”,“差一錯誤”,“空指針”等等錯誤,那麼編寫無錯代碼其實就要簡答多了。
所以我們需要一個思維轉變: 不要光依賴黑箱測試方法,還應該試著去模仿前面所講的假想編譯程序,來排除運氣對程序測試的影響,自動地抓住錯誤的每個機會。好的編譯程序應該能夠這樣: 可以把屢次出錯的合法的C習慣用法看成程序中的錯誤。這句話什麼意思呢? 一些C用法從語法上講是合法的,但是往往卻給程序帶來意想不到的錯誤。所以好的編譯程序應該提供支持:讓我們把這些用法當成錯誤。
舉個例子:
/* memcpy 復制一個內存塊 */ void* memcpy(void *pvTo,void *pvFrom, size_t size) { byte *pbTo=(byte *) pvTo; byte *pbFrom=(byte *)pvFrom; while(size-->0); *pbTo++=*pbFrom++; return pvTo; }
盡管在C語言中空語句本身是合法的,但是我們的確很少這樣使用,出現空語句時往往是由於程序員不小心導致的,而這樣的空語句也會導致隱藏很深的錯誤。所以當出現空語句時,如果編譯器把它認為是個錯誤,並自動給我們一個警告,這樣讓我們非常容易查找出錯誤。
當然如果我們的確要使用空語句時,那就用。但是最好使用NULL使其明顯可見。NULL只是個常量,所以編譯程序不會為NULL語句生成任何代碼。這樣,編譯程序只接受顯示的NULL語句,而把隱式的空語句(即只有一個分號)標示為錯誤。這就使得我們既可以明確地使用空語句,同時又可以指示出那些隱式的往往導致錯誤的空語句。
還有一種常見的問題就是無意的賦值。例如
if(ch=‘\t') ExpandTab();
但是有時為了代碼的簡單性,我們可能編寫出以下代碼
while(*pchTo++=*pchFrom++) NULL;
while((*pchTp++=*pchFrom)!='\0') NULL;
空語句,錯誤的賦值以及原型檢查等只是許多C編譯程序提供的選擇項中的一小部分內容,實際上還有更多的選擇項。這裡的要點是:用戶可以選擇的編譯程序警告設施可以就可能的錯誤向用戶發出警告信息。盡管有時為了這些警告設施,我們可能需要一些額外的工作,但是我們應該把這些警告設施看成一種無風險高償還的程序投資。
使用編譯程序所有的可選警告設施。
另一種檢查錯誤更詳細,更徹底的方法是使用lint。lint這個工具最初是用來掃描C源文件並對源程序中不可移植的部分提出警告,現在的lint實用程序變得更加嚴謹,lint可以檢測出雖然可移植並且完全合乎語法但很有可能是錯誤的特性。
使用lint來檢查出編譯程序漏掉的錯誤。
有時,似乎可以跳過一些設計用來避免程序出錯的步驟,例如單元測試,但是走捷徑之時,就是麻煩將至之日。
如果有單元測試,就進行單元測試。
總結: 當你寫程序時,要在心中時刻牢記著假想編譯程序這一概念,這樣就可以花費很少力氣利用每個機會抓住錯誤。要考慮編譯程序產生的錯誤,lint產生的錯誤以及單元測試失敗的原因。消除程序錯誤的最好方法是盡可能早,盡可能容易地發現錯誤,要尋求費力最小的自動差錯方法。
最後用作者在本章裡的一句引言結束這篇文章:
投資者與賭徒之間的區別在於投資者利用每一次機會,無論它是多麼小,去爭取利益;而賭徒只靠運氣。