轉載自:http://blog.csdn.net/hopesophite/archive/2006/08/02/1010643.aspx
寫在前面的話:
這兩天看了《Writing Clean Code》,很受啟發,感覺值得再讀,于是整理了一點筆記,作為checklist,以備速查。
原書共8章,每章都舉一些例子,指出不足,再用通用的規則改寫,每章結束時會總結一下要點,其中覆蓋了比較重要的規則。附錄A是作者整理的編碼檢查表。本筆記前8章和原書前8章對應,列出了所有的規則,對比較特別或者比較難理解的規則還附上了書中的例子,偶爾加一兩句個人的想法。第9章是原書各章末尾要點的匯總。第10章是原書的編碼檢查表。
本筆記只作為原書的一個速查手冊,詳細的內容請看原書。
中譯本:
《編程精粹 ─── Microsoft 編寫優質無錯C 程序秘訣》Steve Maguire 著,姜靜波 佟金榮 譯,麥中凡 校,電子工業出版社
英文版:
《Writing Clean Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press
英文版原名:
《Writing Solid Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press
1 假想的編譯程序
1.1 使用編譯程序所有的可選警告設施
1.2 使用lint 來查出編譯程序漏掉的錯誤
1.3 如果有單元測試,就進行單元測試
1.4 Tips
C 的預處理程序也可能引起某些意想不到的結果。例如,宏UINT_MAX 定義在limit.h
中,但假如在程序中忘了include 這個頭文件,下面的偽指令就會無聲無息地失敗,
因為預處理程序會把預定義的UINT_MAX 替換成0:
#if UINT_MAX > 65535u
…
#endif
怎樣使預處理程序報告出這一錯誤?
2 構造自己的斷言
2.1 既要維護程序的交付版本,又要維護程序的調試版本
少用預處理程序,那樣會喧賓奪主,嘗試用斷言
2.2 斷言是進行調試檢查的簡單方法。要使用斷言捕捉不應該發生的非法情況。不要混淆非法情況與錯誤情況之間的區別,后者是在最終產品中必須處理的。
這是斷言和錯誤處理的區別
2.3 要使用斷言對函數參數進行確認
2.4 要從程序中刪去無定義的特性或者在程序中使用斷言來檢查出無定義特性的非法使用
這個對C/C++很適用
2.5 不要浪費別人的時間 ─── 詳細說明不清楚的斷言
森林中只標有“危險”,而沒指出具體是什么危險的指示牌將會被忽略。
2.6 斷言不是用來檢查錯誤的
當程序員剛開始使用斷言時,有時會錯誤地利用斷言去檢查真正地錯誤,而不去檢查非
法的況。看看在下面的函數strdup 中的兩個斷言:
char* strdup(char* str)
{
char* strNew;
ASSERT(str != NULL);
strNew = (char*)malloc(strlen(str)+1);
ASSERT(strNew != NULL);
strcpy(strNew, str);
return(strNew);
}
第一個斷言的用法是正確的,因為它被用來檢查在該程序正常工作時絕不應該發生的非
法情況。第二個斷言的用法相當不同,它所測試的是錯誤情況,是在其最終產品中肯定會出
現并且必須對其進行處理的錯誤情況。
2.7 用斷言消除所做的隱式假定,或者利用斷言檢查其正確性
Eg. 對于和機器相關的內存填充程序,不必也無法將其寫成可移植的。可以用條件編譯。但其中應該對某種機器的隱含假設做檢查。
2.8 利用斷言來檢查不可能發生的情況
壓縮程序的例子:正常情況和特殊情況,重復次數>=4或者就等于1
2.9 在進行防錯性程序設計時,不要隱瞞錯誤
2.10 要利用不同的算法對程序的結果進行確認
2.11 不要等待錯誤發生,要使用初始檢查程序
2.12 Tips
不要把真正需要執行的語句放在斷言里
3 為子系統設防
3.1 要消除隨機特性 ─── 使錯誤可再現
3.2 沖掉無用的信息,以免被錯誤地使用
分配內存時填充上非法值:eg. 68000 用0xA3,Intel X86系列用0xCC
釋放內存時立刻填上非法值
引申:這個和《代碼大全》中講的進攻式編程觀點類似
3.3 如果某件事甚少發生的話,設法使其經常發生
eg. 讓realloc函數中移動內存塊這種比較少發生的事情經常發生--自己包裝一個relloc。
3.4 保存調試信息到日志,以便進行更強的錯誤檢查
這里的日志信息相當于一個簿記功能的信息,寫到內存鏈表中。
p168代碼有錯:
if( pbiPrev == NULL )
pbiHead = pbi->pbiHead;
3.5 建立詳盡的子系統檢查并且經常地進行這些檢查--調試檢查
eg。利用簿記和‘是否被引用’的標志,檢查是否有內存泄漏和懸掛指針
3.6 仔細設計程序的測試代碼,任何選擇都應該經過考慮
eg. 先后順序是有講究的:先看500元的套裝,再看80元的毛衣
3.7 努力做到透明的一致性檢查
不要影響代碼的使用者的使用方式
3.8 不要把對交付版本的約束應用到相應的調試版本上
要用大小和速度來換取錯誤檢查能力
3.9 每一個設計都要考慮如何確認正確性
如果可能的話,把測試代碼放到所編寫的子系統中,而不要把它放到所編寫子系統的外層。不要等到進行了系統編碼時,才考慮其確認方法。在子系統設計的每一步,都要考慮“如何對這一實現進行詳盡的確認”這一問題。
引申:回憶高中時檢查結果:如果是解方程,則代入數值驗算就可;如果是計算題,換一個方法再算一遍。總之,要有方法確認其正確性。
3.10 “調試代碼時附加了額外信息的代碼,而不是不同的代碼”
加調試代碼時要保證產品代碼一定也要運行,這樣才能測試到真正的產品代碼。
3.11 在自己包裝的內存函數中加上允許注入錯誤的機制。
eg. 定義一個failure結構,在NewMemory中測試這個結構,如果為真,則返回false,表示內存分配失敗。
這樣,開發者和測試者都能利用這個機制,人為的注入錯誤。
4 對程序進行逐條跟蹤
4.1 代碼中不會自己生出錯誤來,錯誤是程序員編寫新代碼或者修改現有代碼的產物。如果你想發現代碼中的錯誤,沒有哪個辦法比在對代碼進行編譯時對其進行逐條跟蹤更好。
這個如果用個“完美的”編譯器就更好。
4.2 不要等到出了錯誤再對程序進行逐條的跟蹤
而是把對程序逐條跟蹤看成是一個必要的過程。這可以保證程序按你預想的方式工作。
引申:可以和代碼走查結合在一起。或者先進行代碼走查,再逐條跟蹤,共兩遍檢查代碼。
4.3 對每一條代碼路徑進行逐條的跟蹤
注意覆蓋率問題:語句覆蓋or分支覆蓋
4.4 當對代碼進行逐條跟蹤時,要密切注視數據流
這樣有助于發現以下錯誤:
上溢和下溢錯誤;
數據轉換錯誤;
差1 錯誤;
NULL 指針錯誤;
使用廢料內存單元錯誤(0xA3 類錯誤);
用 = 代替 == 的賦值錯誤;
運算優先級錯誤;
邏輯錯誤。
4.5 源級調試程序可能會隱瞞執行的細節,對關鍵部分的代碼要進行匯編指令級的逐條跟綜
對條件語句的各個子條件,不要一次越過,而要看每個子條件的值。
5 糖果機界面
作者以糖果機的糟糕的界面設計導致人犯錯講起,闡述界面設計應該指導程序員少犯錯誤。
5.1 要使用戶不容易忽視錯誤情況,不要在正常地返回值中隱藏錯誤代碼
作者以getchar函數為例:這個函數返回一個char或者是-1,由此要求使用getchar的程序員必須用int來接收getchar的返回值,但肯定會有很多程序員忘記這一點,由此可能會引發難以捕捉的錯誤。
作者設計了另一個函數界面來處理這種情況:int fGetChar(char*),返回值存入char*所指位置,而int返回flag,為true表示正確。這樣,由于劃分了正常的返回值和錯誤代碼,避免了getchar的返回值要用int接收的問題。
5.2 要不遺余力地尋找并消除函數界面中的缺陷
Eg. 下述代碼隱含著一個錯誤
pbBuf = (byte*)realloc( pbBuf, sizeNew );
if( pbBuf != NULL )
使用初始化這個更大的緩沖區
如果realloc分配內存時失敗,返回NULL,則pbBuf為NULL,它原來指向的內存將會丟失。
如果界面是flag fResizeMemory( void** ppv, size _t sizeNew )則好得多
5.3 不要編寫多種功能集于一身的函數,為了對參數進行更強的確認,要編寫功能單一的函數
以realloc為例,它接受的指針為NULL但size大于0時相當于malloc,指針不為NULL但size為0時相當于free。這樣realloc就混雜了malloc和free的功能,極其容易出錯。
5.4 不要模棱兩可,要明確地定義函數的參數
像realloc那樣靈活的參數不一定很好,要考慮程序員給出這樣的輸入參數可能是出于什么原因,如果沒有充分的理由,用斷言來禁止太靈活的輸入能減少錯誤。
5.5 返回值與錯誤處理:編寫函數使其在給定有效的輸入情況下不會失敗
返回錯誤碼不是唯一的處理錯誤的方式。Eg. Tolower函數在遇到輸入是小寫字母時,應該怎么辦?
如果返回-1,那么將遇到和getchar相同的問題:程序員要用int來存儲tolower的返回值。此時,tolower返回原字符也許是一個更好的方式。
5.6 使程序在調用點明了易懂:要避免布爾參數
通過檢查調用代碼,檢驗界面設計的合理性。
Eg. 以下兩個函數聲明會導致調用方式的不同:
void UnsignedToStr(unsigned u, char *strResult, flag fDecimal);
void UnsignedToStr(unsigned u, char* str, unsigned base);
前者的調用方式是:
UnsignedToStr(u, str, TRUE);
UnsignedToStr(u, str, FALSE);
這顯然不好。而后者是UnsignedToStr(u, str, BASE10)則好的多。
5.7 編寫注解突出可能的異常情況
用注釋寫出常見的錯誤用法和正確用法的例子。
5.8 小結
本章先給出一個界面不好的例子,再給出一般原則:要不遺余力的檢查界面的合理性。然后講功能要單一,輸入要有限制,輸出的正常返回值要與錯誤碼分開,用調用方式檢查界面,用注釋來指出異常情況。
6 風險事業
6.1 使用有嚴格定義的數據類型
可移植類型最值得注意之處是:它們只考慮了三種最通用的數制:壹的補碼、貳的補碼
和有符號的數值。
Char只有0~127嗎是可移植的
Unsigned char 是0~255,但signed char是-127~127 (沒有-128嗎)是可移植的
6.2 經常反問:“這個變量表達式會上溢或下溢嗎?”
Eg. 以下代碼會導致無窮循環,因為ch會上溢為0,導致不可能大于UCHAR_MAX。
unsigned char ch;
/* 首先將每個字符置為它自己 */
for (ch=0; ch <= UCHAR_MAX;ch++)
chToLower[ch] = ch;
eg. 以下代碼會下溢,導致無窮循環,因為size_t是無符號型,不可能小于0
size_t size = 100;
while (--size >= 0)
NULL;
6.3 盡可能精確地實現設計,近似地實現設計就可能出錯
6.4 一個“任務”應只實現一次(Implement "the task" just once).
一個原則:Strive to make everyfunction perform its task exactly
one time
static window * pwndRootChildren = NULL;
void AddChild( window * pwndParent, window * pwndNewBorn )
{
/* 新窗口可能只有子窗口 ⋯ */
ASSERT( pwndNewBorn->pwndSibling == NULL );
if( pwndParent == NULL )
{
/* 將窗口加入到頂層根列表 */
pwndNewBorn->pwndSibling = pwndRootChildren;
pwndRootChildren = pwndNewBorn;
}
else
{
/* 如果是父母的第一個孩子,那么開始一個鏈,
* 否則加到現存兄弟鏈的末尾處
*/
if( pwndParent -> pwndChild == NULL )
pwndParent -> pwndChild = pwndNewBorn;
else
{
window *pwnd = pwndParent -> pwndChild;
while( pwnd -> pwndSibling != NULL)
pwnd = pwnd -> pwndSibling;
pwnd -> pwndSibling = pwndNewBorn;
}
}
}
.
假如AddChild 是一個任務,要在現有窗口中增加子窗口,而上面的代碼具有三個單獨的插入過程。常識告訴我們如果有三段代碼而不是一段代碼來完成一個任務,則很可能有錯。這往往意味著這個實現中有例外情況。
其最終的改進見下一節。
6.5 避免無關緊要地if 語句
以指針為中心的樹的構建,可以不必為特殊情況編寫代碼:
void AddChild(window* pwndParent, window* pwndNewBorn )
{
window **ppwindNext;
/* 新窗口可能沒有兄弟窗口 ? */
ASSERT( pwndNewBorn -> pwndSibling == NULL );
/* 使用以指針為中心的算法
* 設置ppwndNext 指向pwndParent -> pwndChild
* 因為pwndParent -> pwndChild 是鏈中第一個“下一個兄弟指針”
一個“任務”應只實現一次
*/
ppwndNext = &pwndParent->pwndChild;
while( *ppwndNext != NULL )
ppwndNext = &( *ppwndNext )->pwndSibling;
*ppwndNext = pwndNewBorn;
}
由于沒有無關的if語句,使所有的程序都會經過同樣的路徑,因此這段代碼就會被測試的很充分。
6.6 避免使用嵌套的“?:“運算符
重新整理思路,甚至用查表法,都能簡化過程。
6.7 每種特殊情況只能處理一次
不要讓處理同一個特殊情況的代碼散布在多個地方
6.8 避免使用有風險的語言慣用語
這里舉了好幾個例子。
Eg. pchEnd = pch + size;
while( pch < pchEnd )
NULL;
如果pchEnd恰好查找到存儲器的結尾處,那么所指的位置就不存在了
Eg. 除以2和移位:移位有風險
Eg. while (--size >= 0) 和while(size-- > 0),前者有風險,后者卻沒有。
6.9 不能毫無必要地將不同類型地操作符混合使用,如果必須將不同類型地操作符混合使用,就用括號把它們隔離開來
6.10 避免調用返回錯誤的函數(Avoid calling functions that return errors)
這樣,就不會錯誤地處理或漏掉由其它人設計的函數所返回的錯誤條件。
如果自始至終程序反復處理同樣的錯誤條件,就將錯誤處理部分獨立出來。Eg. 單獨的錯誤處理子程序。
有時更好的方法是使錯誤根本不會發生。Eg. 窗口的rename函數可能要realloc,從而導致失敗,但通過分配超額的內存空間(都取名字長度的最大值),則這個使錯誤不會出現,從而避免了錯誤處理的代碼。
7 編碼中的假象
7.1 只引用屬于你自己的存儲空間
7.2 不能引用已釋放的存儲區
7.3 只有系統才能擁有空閑的存儲區,程序員不能擁有
決不要使用free以后的內存
7.4 不要把輸出內存用作工作區緩存
Don't use output memory as workspace buffers.
7.5 不要利用靜態(或全局)量存儲區傳遞數據
7.6 不要寫寄生函數
依賴于別的函數內部處理的函數叫寄生函數,被依賴的叫宿主函數。
宿主函數的實現一旦改變,寄生函數就不能正常工作。
Eg. ,FIG(FORTH Interest Group)公布的FORTH-77中有CMOVE, FILL等函數。如果用CMOVE實現FILL,則FILL就是寄生函數。如果CMOVE實現為一次拷貝4個字節,則FILL就失敗。
/* CMOVE ─── 用頭到頭的移動來轉移存儲 */
void CMOVE (byte *pbFrom,byte *pbTo,size_t size)
{
while(size-- > 0 )
*pbTo++ = *pbFrom++;
}
/* FILL 填充某一存儲域 */
void FILL (byte *pb,size_t size,byte b)
{
if(size>0)
{
*pb = b;
CMOVE(pb,pb+1,size-1);
}
}
7.7 不要濫用程序設計語言
用一把螺絲刀來播開油漆罐的蓋子,然后又用這把螺絲刀來攪拌油漆――這并不是正確的做法,之所以這樣做是因為當時這樣很方便,而且能夠解決問題。
程序設計語言也是如此。
Eg. 不要將比較的結果作為計算表達式的一部分
另外標準也會變。Eg. Forth-77和Forth-83中的布爾值定義
7.8 緊湊的C 代碼并不能保證得到高效的機器代碼
我的觀點是:如果你總是使用稀奇古怪的表達式,以便把C 代碼盡量寫在源代碼的一行上,從而達到最好的瑜伽狀態的話,你很可能患有可怕的“一行清”(one-line-itis)疾病(也稱為程序設計語言綜合癥)
7.9 為一般水平的程序員編寫代碼
8 剩下來的就是態度問題
8.1 錯誤幾乎不會“消失”
錯誤消失有三個原因:一是錯誤報告不對;二是錯誤已被別的程序員改正了;三是這個錯誤依然存在但沒有表現出來。
8.2 馬上修改錯誤,不要推遲到最后
l 不要通過把改正錯誤移置產品開發周期的最后階段來節省時間。修改一年前寫的代
碼比修改幾天前寫的代碼更難,實際上這是浪費時間。
l “一次性”地修改錯誤會帶來許多問題:早期發現的錯誤難以重現。
l 錯誤是一種負反饋,程序開發倒是快了,卻使程序員疏于檢查。如果規定只有把錯誤全部改正之后才能增加新特征的話,那么在整個產品開發期間都可以避免程序員的疏漏,他們將忙于修改錯誤。反之,如果允許程序員略過錯誤,那就使管理失控。
l 若把錯誤數保持在近乎于0 的數量上,就可以很容易地預言產品的完成時間。只需要估算一下完成 32 個特征所需的時間,而不需要估算完成32 個特征加上改正1742個錯誤所需的時間。更好的是,你總能處于可隨時交出已開發特征的有利地位。
8.3 修改錯誤要治本,不要治標
8.4 除非關系產品的成敗,否則不要整理代碼
整理代碼的問題在于程序員總不把改進的代碼作為新代碼處理,導致測試不夠
8.5 不要實現沒有戰略意義的特征
8.6 不設自由特征
對于程序員來說,增加自由特征可能不費事,但是對于特征來講,它不僅僅增多了代碼,還必須有人為該特征寫又檔,還必須有人來測試它。不要忘記還必須有人來修改該特征可能出現的錯誤。
8.7 不允許沒有必要的靈活性
Eg. realloc的參數
8.8 在找到正確的解法之前,不要一味地“試”,要花時間尋求正確的解
8.9 盡量編寫和測試小塊代碼。即使測試代碼會影響進度,也要堅持測試代碼
8.10 測試代碼的責任不在測試員身上,而是程序員自己的責任
開發人員和測試人員分別從內外開始測試,所以不是重復勞動。
8.11 不要責怪測試員發現了你的錯誤
8.12 建立自己優先級列表并堅持之
約克的優先級列表 | 吉爾的優先級列表 |
正確性 | 正確性 |
全局效率 | 可測試性 |
大小 | 全局效率 |
局部效率 | 可維護性/明晰性 |
個人方便性 | 一致性 |
可維護性/明晰性 | 大小 |
個人表達方式 | 局部效率 |
可測試性 | 個人表達方式 |
一致性 | 個人方便性 |
8.13 你必須養成經常詢問怎樣編寫代碼的習慣。
本書就是長期堅持詢問一些簡單問題所得的結果。
l 我怎樣才能自動檢測出錯誤?
l 我怎樣才能防止錯誤?
l 這種想法和習慣是幫助我編寫無錯代碼呢還是妨礙了我編寫無錯代碼?
9 本書各章要點匯總
書中每章結束時都小結了本章要點,這里匯總如下:
9.1 假想的編譯程序
l 消除程序錯誤的最好方法是盡可能早、盡可能容易地發現錯誤,要尋求費力最小的自動查錯方法。
l 努力減少程序員查錯所需的技巧。可以選擇的編譯程序或lint 警告設施并不要求程序員要有什么查錯的技巧。在另一個極端,高級的編碼方法雖然可以查出或減少錯誤,但它們也要求程序員要有較多的技巧,因為程序員必須學習這些高級的編碼方法。
9.2 自己設計并使用斷言
l 要同時維護交付和調試兩個版本。封裝交付的版本,應盡可能地使用調試版本進行自動查錯。
l 斷言是進行調試檢查的簡單方法。要使用斷言捕捉不應該發生的非法情況。不要混淆非法情況與錯誤情況之間的區別,后者是在最終產品中必須處理的。
l 使用斷言對函數的參數進行確認,并且在程序員使用了無定義的特性時向程序員報警。函數定義得越嚴格,確認其參數就越容易。
l 在編寫函數時,要進行反復的考查,并且自問:“我打算做哪些假定?”一旦確定了相應的假定,就要使用斷言對所做的假定進行檢驗,或者重新編寫代碼去掉相應的假定。另外,還要問:“這個程序中最可能出錯的是什么,怎樣才能自動地查出相應的錯誤?”努力編寫出能夠盡早查出錯誤的測試程序。
l 一般教科書都鼓勵程序員進行防錯性程序設計,但要記住這種編碼風格會隱瞞錯誤。當進行防錯性編碼時如果“不可能發生”的情況確實發生了,要使用斷言進行報警。
9.3 為子系統設防
l 考查所編寫的子系統,問自己:“在什么樣的情況下,程序員在使用這些子系統時會犯錯誤。”在子系統中加上相應的斷言和確認檢查代碼,以捕捉難于發現的錯誤和常見的錯誤。
l 如果不能使錯誤不斷重現,就無法排除它們。找出程序中可能引起隨機行為的因素,并將它們從程序的調試版本中清除。把目前尚“無定義”的內存單元置成了某個常量值,就可能產生這種錯誤。在這種情況下,如果程序在該單元被正確地定義為某個值之前引用了它的內容,那么每次執行這部分錯誤的代碼,都會得到同樣的錯誤結果。
l 如果所編寫的子系統釋放內存(或者其它的資源),并因此產生了“ 無用信息”,那么要把它攪亂,使它真的像無用信息。否則,這些被釋放了的數據就有可能仍被使用,而又不會被注意到。
l 類似地,如果在所編寫的子系統中某些事情可能發生,那么要為該子系統加上相應的調試代碼,使這些事情一定發生。這樣可以增大查出通常得不到執行的代碼中的錯誤的可能性。
l 盡力使所編寫的測試代碼甚至在程序員對其沒有感覺的情況下亦能起作用。最好的測試代碼是不用知道其存在也能起作用的測試代碼。
l 如果可能的話,把測試代碼放到所編寫的子系統中,而不要把它放到所編寫子系統的外層。不要等到進行了系統編碼時,才考慮其確認方法。在子系統設計的每一步,都要考慮“如何對這一實現進行詳盡的確認”這一問題。如果發現這一設計難于測試或者不可能對其進行測試,那么要認真地考慮另一種不同的設計,即使這意味著用大小或速度作代價去換取該系統的測試能力也要這么做。
l 在由于速度太慢或者占用的內存太多而拋棄一個確認測試程序之前,要三思而后行。切記,這些代碼并不是存在于程序的交付版本中。如果發現自己正在想:“這個測試程序太慢、太大了”,那么要馬上停下來問自己:“怎樣才能保留這個測試程序,并使它既快又小?”
9.4 對程序進行逐條跟蹤
l 代碼中不會自己生出錯誤來,錯誤是程序員編寫新代碼或者修改現有代碼的產物。如果你想發現代碼中的錯誤,沒有哪個辦法比在對代碼進行編譯時對其進行逐條跟蹤更好。
l 雖然直觀上你可能認為對代碼進行走查會花費大量的時間,但這是不對的。剛開始進行代碼的走查確實要多花一點時間,但當這一切習慣成自然之后并不會多花多少時間,你可以很快地走查一遍。
l 一定要對每一條代碼路徑進行逐條的跟蹤,至少要跟蹤一遍,尤其是對代碼中的錯誤處理部分。不要忘記 &&、|| 和?:這些運算符,它們每個都有兩條代碼路徑需要進行測試。
l 在某些情況下也許需要在匯編語言級對代碼進行逐條的跟蹤。盡管不必經常這樣做,但在必要的時候不要回避這種做法。
9.5 糖果機界面
l 最容易使用和理解的函數界面,是其中每個輸入和輸出參數都只代表一種類型數據的界面。把錯誤值和其它的專用值混在函數的輸入和輸出參數中,只會搞亂函數的界面。
l 設計函數的界面迫使程序員考慮所有重要細節(如錯誤情況的處理),不要使程序員能夠很容易地忽視或者忘記有關的細節。
l 老要想到程序員調用所編函數的方式,找出可能使程序員無意間引入錯誤的界面缺陷。尤其重要的是要爭取編出永遠成功的函數,使調用者不必進行相應的錯誤處理。
l 為了增加程序的可理解性從而減少錯誤,要保證所編函數的調用能夠被必須閱讀這些調用的程序員所理解。莫明其妙的數字和布爾參數都與這一目標背道而馳,因此應該予以消除。
l 分解多功能的函數。取更專門的函數名(如ShrinkMemory 而不是 realloc)不僅可以增進人們對程序的理解,而且使我們可以采用更加嚴格的斷言自動地檢查出調用錯誤。
l 為了向程序員展示出所編函數的適當調用方法,要在函數的界面中通過注解的方式詳細說明。要強調危險的方面。
9.6 風險事業
l 在選擇數據類型的時候要謹慎。雖然ANSI 標準要求所有的執行程序都要支持char, int,long 等類型,但是它并沒有具體定義這些類型。為了避免程序出錯,應該只按照ANSI 的標準選擇數據類型。
l 由于代碼可能會在不理想的硬件上運行,因此很可能算法是正確的而執行起來卻有錯。所以要經常詳細檢查計算結果和測試結果的數據類型范圍是否上溢或下溢。
l 在實現某個設計的時候,一定要嚴格按照設計去實現。如果在編寫代碼時只是近似地實現所提出的要求,那就很容易出錯。
l 每個函數應該只有一個嚴格定義的任務,不僅如此,完成每個任務也應只有一種途徑。假如不管輸入什么都能執行同樣的代碼,那就會大大降低那些不易被發現的錯誤所存在的概率。
l if 語句是個警告信號,說明代碼所做的工作可能比所需要的要多。努力消除代碼中每一個不必要的if 語句,經常反問自己:“怎樣改變設計從而刪掉這個特殊情況?”有時可能要改變數據結構,有時又要改變一下考察問題的方式,就象透鏡是凸的還是凹的問題一樣。
l 有時if 語句隱藏在while 和for 循環的控制表達式中。“?:”操作符是if 語句的另外一種形式。
l 曾惕有風險的語言慣用語,注意那些相近但更安全的慣用語。特別要警惕那些看上去象是好編碼的慣用語,因為這樣的實現對總體效率很少有顯著的影響,但卻增加了額外的風險性。
l 在寫表達式時,盡量不要把不同類型的操作符混合起來,如果必須混合使用,用括號把它們分隔開來。
l 特殊情況中的特殊情況是錯誤處理。如果有可能,應該盡量避免調用可能失敗的函數,假如必須調用返回錯誤的函數,將錯誤處理局部化以便所有的錯誤都匯集到一點,這將增加在錯誤處理代碼中發現錯誤的機會。
l 在某些情況下,取消一般的錯誤處理代碼是有可能的,但要保證所做的事情不會失敗。這就意味著在初始化時要對錯誤進行一次性處理或是從根本上改變設計。
9.7 編碼中的假象
l 如果你要用到的數據不是你自己所有的,那怕是臨時的,也不要對其執行寫操作。盡管你可能認為讀數據總是安全的,但是要記住,從映射到I/O 的存儲區讀數據,可能會對硬件造成危害。
l 每當釋放了存儲區人們還想引用它,但是要克制自己這么做。引用自由存儲區極易引起錯誤。
l 為了提高效率,向全局緩沖區或靜態緩沖傳遞數據也是很吸引人的,但是這是一條充滿風險的捷徑。假若你寫了一個函數,用來創建只給調用函數使用的數據,
那么就將數據返回給調用函數,或保證不意外地更改這個數據。
l 不要編寫依賴支持函數的某個特殊實現的函數。我們已經看到,FILL 例程不該象給出的那樣調用CMOVE,這種寫法只能作為壞程序設計的例子。
l 在進行程序設計的時候,要按照程序設計語言原來的本意清楚、準確地編寫代碼。避免使用有疑問的程序設計慣用語,即使語言標準恰好能保證它工作,也不要使用。請記住,標準也在改變。
l 如果能用C 語言有效地表示某個概念,那么類似地,相應的機器代碼也應該是有效的。邏輯上講似乎應該是這樣,可是事實上并非如此。因此在你將多行C代碼壓縮為一行代碼之前,一定要弄清楚經過這樣的更改以后,能否保證得到更好的機器代碼。
l 最后,不要象律師寫合同那樣來編寫代碼。如果一般水平的程序員不能閱讀和理解你的代碼,那就說明你的代碼太復雜了,使用簡單一點的語言。
9.8 剩下來的就是態度問題
l 錯誤既不會自己產生,也不會自己改正。如果你得到了一個錯誤報告,但這個錯誤不再出現了。不要假設測試員發生了幻覺,而要努力查找錯誤,甚至要恢復程序的老版本。
l 不能“以后”再修改錯誤。這是許多產品被取消的共同教訓。如果在你發現錯誤的時候就及時地更正了錯誤,那你的項目就不會遭受毀滅性的命運。當你的項目總是保持近似于0 個錯誤時,怎么可能會有一系列的錯誤呢?
l 當你跟蹤查到一個錯誤時,總要問一下自己,這個錯誤是否會是一個大錯誤的癥狀。當然,修改一個剛剛追蹤到的癥狀很容易,但是要努力找到真正的起因。
l 不要編寫沒有必要的代碼。讓你的競爭者去清理代碼,去實現“冷門”但無價值的特征,去實現自由特征。讓他們花大量的時間去修改由于這些無用代碼所引起的所有沒有必要的錯誤。
l 記住靈活與容易使用并不是一回事。在你設計函數和特征時,重點是使之容易使用;如果它們僅僅是靈活的,象realloc 函數和Excel 中的彩色格式特征那樣,那么就沒法使得代碼更加有用;相反地,使得發現錯誤變得更困難了。
l 不要受“試一試”某個方案以達到預期結果的影響。相反,應把花在嘗試方案上的時間用來尋找正確的解決方法。如果必要,與負責你操作系統的公司聯系,這比提出一個在將來可能會出問題的古怪實現要好。
l 代碼寫得盡量小以便于全面測試。在測試中不要馬虎。記住,如果你不測試你的代碼,就沒有人會測試你的代碼了。無論怎樣,你也不要期望測試組為你測試代碼。
l 最后,確定你們小組的優先級順序,并且遵循這個順序。如果你是約克,而項目需要吉爾,那么至少在工作方面你必須改變習慣。
10 本書附錄A 編碼檢查表
本附錄給出的問題列表,總結了本書的所有觀點。使用本表的最好辦法是花兩周時間評
審一下你的設計和編碼實現。先花幾分鐘時間看一看列表,一旦熟悉了這些問題,就可以靈
活自如地按它寫代碼了。此時,就可以把表放在一邊了。
一般問題
── 你是否為程序建立了DEBUG 版本?
── 你是否將發現的錯誤及時改正了?
─一 你是否堅持徹底測試代碼.即使耽誤了進度也在所不惜?
── 你是否依靠測試組為你測試代碼?
─一 你是否知道編碼的優先順序?
─一 你的編譯程序是否有可選的各種警告?
關于將更改歸并到主程序
─一 你是否將編譯程序的警告(包括可選的)都處理了?
── 你的代碼是否未用Lint
─一 你的代碼進行了單元測試嗎?
─一 你是否逐步通過了每一條編碼路徑以觀察數據流?
─一 你是否逐步通過了匯編語言層次上的所有關鍵代碼?
── 是否清理過了任何代碼?如果是,修改處經過徹底測試了嗎?
─一 文檔是否指出了使用你的代碼有危險之處?
── 程序維護人員是否能夠理解你的代碼?
每當實現了一個函數或子系統之時
─一 是否用斷言證實了函數參數的有效性?
─一 代碼中是否有未定義的或者無意義的代碼?
─一 代碼能否創建未定義的數據?
─一 有沒有難以理解的斷言?對它們作解釋了沒有?
─一 你在代碼中是否作過任何假設?
─一 是否使用斷言警告可能出現的非常情況?
─一 是否作過防御性程序設計?代碼是否隱藏了錯誤?
─一 是否用第二個算法來驗證第一個算法?
─一 是否有可用于確認代碼或數據的啟動(startup)檢查?
─一 代碼是否包含了隨機行為?能消除這些行為嗎?
── 你的代碼若產生了無用信息,你是否在DEBUG 代碼中也把它們置為無用信息?
── 代碼中是否有稀奇古怪的行為?
── 若代碼是子系統的一部分,那么你是否建立了一個子系統測試?
── 在你的設計和代碼中是否有任意情況?
── 即使程序員不感到需要,你也作完整性檢查嗎?
── 你是否因為排錯程序太大或太慢,而將有價值的DEBUG 測試拋置一邊?
── 是否使用了不可移植的數據類型?
─一 代碼中是否有變量或表達式產生上溢或下溢?
── 是否準確地實現了你的設計?還是非常近似地實現了你的設計?
── 代碼是否不止一次地解同一個問題?
── 是否企圖消除代碼中的每一個if 語句?
── 是否用過嵌套?:運算符?
── 是否已將專用代碼孤立出來?
── 是否用到了有風險的語言慣用語?
─一 是否不必要地將不同類型的運算符混用?
── 是否調用了返回錯誤的函數?你能消除這種調用嗎?
─一 是否引用了尚未分配的存儲空間?
─一 是否引用已經釋放了的存儲空間?
── 是否不必要地多用了輸出緩沖存儲?
── 是否向靜態或全局緩沖區傳送了數據?
── 你的函數是否依賴于另一個函數的內部細節?
── 是否使用了怪異的或有疑問的C 慣用語?
── 在代碼中是否有擠在一行的毛病?
── 代碼有不必要的靈活性嗎?你能消除它們嗎?
─一 你的代碼是經過多次“試著”求解的結果嗎?
─一 函數是否小并容易測試?
每當設計了一個函數或子系統后
─一 此特征是否符合產品的市場策略?
─一 錯誤代碼是否作為正常返回值的特殊情況而隱藏起來?
─一 是否評審了你的界面,它能保證難于出現誤操作嗎?
─一 是否具有多用途且面面俱到的函數?
─一 你是否有太靈活的(空空洞洞的)函數參數?
─一 當你的函數不再需要時,它是否返回一個錯誤條件?
─一 在調用點你的函數是出易讀?
─一 你的函數是否有布爾量輸入?
修改錯誤之時
── 錯誤無法消失,是否能找到錯誤的根源?
─一 是修改了錯誤的真正根源,還是僅僅修改了錯誤的癥狀?