無論一門語言有多么流行或多么優(yōu)秀,它總是存在一些問題,C語言也不例外。本章討論的重點是C語言本身存在的問題,作者煞費苦心的用一個太空任務和軟件的故事開頭,也用另一個太空任務和軟件的故事結(jié)尾,引人入勝。
關于這兩個故事,在這里不說,有興趣的朋友還是建議買這本書去看看,這本書用相當輕松的文字而又不失深沉地向我們道來C語言的各種特性與特別的用法。
書中提到一種分析編程語言缺陷的方法,讓我們能夠詳細的去分析各種編程語言的缺陷,即把所有的缺陷歸于3類:不該做的做了(多做之過)、該做的沒做(少做之過)、該做的做了但不合適(誤做之過),本章也是按照這樣一種分析方法來分析C語言本身存在的一些問題,由于C是一門神奇的語言,被許多平臺所選用,也被大家所學習,所以了解C語言是一件相當有必要的事情,本章就是從缺陷來了解C語言。
多做之過,就是語言中存在某些不應該存在的特性,包括容易出錯的switch語句、相鄰字符串常量自動連接和缺省全局作用域。
首先說說switch語句吧,這個語句在多條件的時候使用率還是相當高的,相比大量if語句,我還是比較傾向于它的。switch語句的一般形式如下:
switch(表達式)
{
case 常量表達式1:語句1; break;
....
case 常量表達式n:語句n; break;
default:語句;break;
}
每個case結(jié)構由3個部分組成,關鍵字case;其后的常量表達式;以及后面的冒號,當表達式的值與case后面的常量表達式匹配時,case后面的語句就會執(zhí)行,否則執(zhí)行default后面的語句,default都可以出現(xiàn)在case列表出現(xiàn)的任何位置,如果沒有default語句,那么switch語句就什么也不做,你不要指望它會提醒你它什么都沒做。在C語言中,幾乎從來不進行運行時錯誤檢查——對進行解引用操作的指針進行有效性檢查大概是唯一的例外,這是因為運行時檢查與C語言的設計理念相違背,按照C語言的理念,程序員應該知道自己在干什么,而且保證自己的所作所為是正確的。switch的另一個問題是它內(nèi)部的任何語句都可以加上標簽,并在執(zhí)行時跳轉(zhuǎn)到那里,作者給出了一個例子,那就是當你的default語句寫錯的時候,比如把l字母寫成了數(shù)字1,看起來很像對吧defau1t,不過功能可是大不相同,這意味著如果表達式不匹配任何常量表達式時它將什么也不干,因為沒有default語句啊,然而即使這樣,編譯器也無法檢查出錯誤來。當然switch語句里最大的問題還不是這個,而是它不會在每個case語句執(zhí)行完畢后自動跳出,如果你不使用break語句來跳出,它將一直執(zhí)行下去,在《C與指針》描述switch語句時有一句話我覺得非常合適,那就是case語句只是確認進入switch語句的入口,如果你不使用break語句,那么出口都是在swtich語句的右花括號那里,作者還舉了一個利用switch語句這種特性的例子,用來計算程序輸入中字符、單詞和行的個數(shù),有趣的是,這個例子正是需要switch語句一直執(zhí)行下去而不是遇到一個case就退出,有興趣的朋友可以參考《C與指針》第四章的內(nèi)容。當然,如果在這種情況下要使用switch語句的特性,那么一句注釋"FALL THRU"是必不可少的,它會告訴你,我就是要利用這個特性,我不需要break語句,不過,在絕大多數(shù)情況下是不會需要這種特性的。以上就是switch語句存在的三個主要問題。
其次,相鄰字符串常量的自動合并這個約定也會帶來一些問題。在printf的使用中,這是一個優(yōu)點,因為你不用擔心要輸出的字符串有多長,你可以放心的用雙引號包括每一行的內(nèi)容,反正它會自動合并,比方說
printf("A second favorite children's book"
"is Thoms the tank engine and the Naughty Enginedriver who"
"tied down thomas's boiler safety value");
這個printf語句會自動連接三個行,這可以使每一行的代碼看起來簡潔而又完整,不過,你該擔心的是下列情況:
char *available_resouces[] = {
"color monitor",
"big disk"
"Cray" /*少了一個逗號*/
"on-line drawing routhines",
...
在這種情況下我們都知道,由于數(shù)組大小的缺省,而少了逗號會使兩個字符串常量自動連接,所以在編譯器看來,這并不是一個錯誤,它也就不會提示你,而程序可能會莫名其妙的運行,打印"Crayon-line drawing routhines“或是修改其他變量,因為字符串數(shù)目比預期少了一個。
缺省可見性這個問題主要體現(xiàn)在全局函數(shù)的定義上,我們知道在聲明函數(shù)的時候,如果沒有任何關鍵字限制,那么會被自動定義為全局函數(shù),除非你加上static關鍵字,才能限制對這個函數(shù)的訪問。事實上,幾乎所有人都沒有在函數(shù)名前添加存儲類型說明符的習慣,所以絕大多數(shù)函數(shù)都是全局可見的,然而,根據(jù)實際經(jīng)驗,這個缺省的全局可見性多次被證明是個錯誤。軟件對象在大多數(shù)情況下應該缺省的采用有限可見性,當程序員需要讓它全局可見時,應該采用顯式的手段,原因在于這種大范圍的全局可見性會與C語言的另一個特性產(chǎn)生影響,也就是用戶編寫和庫函數(shù)同名的函數(shù)并取而代之的行為。這也說明了在C語言中,對信息可見性的選擇很有限,要么是extern,意味著整個庫的所有對象都可見,要么是static,對其他文件都不可見。
所謂”誤作之過“,就是語言中有誤導性質(zhì)或是不適當?shù)奶匦?,這些特性跟C語言的簡潔性有關,有些則與操作符的優(yōu)先級有關。
C語言存在的其中一個問題就是它太簡潔了,僅增加、修改或刪除一個字符就會使原先的程序變成另外一個仍然有效但全然不同的程序,這就意味著,如果你在一個小問題上出了一點問題,那么編譯器是不會檢查出來提示你的,因為你的程序仍然有效。當然,還造成一個問題,那就是很多符號同時具有好幾種意思,你要直到它到底是什么意思,還要根據(jù)上下文來,這一點尤其體現(xiàn)在作用域上。比方說static關鍵字就曾經(jīng)令我疑惑,它有時候表示靜態(tài)變量,有時候又表示內(nèi)部鏈接屬性,那么它到底代表什么呢?正確的答案是這樣的,在函數(shù)內(nèi)部,表示靜態(tài)變量,當表示函數(shù)時,代表內(nèi)部鏈接屬性。同樣的extern關鍵字也是這樣,在缺省可見性已經(jīng)提到,extern的外部鏈接屬性不應該作為缺省屬性。還有&操作符,既表示取地址操作符,又表示按位與操作,同樣*操作符也有多種含義,最明顯的、用法最多的操作符可能還是要數(shù)()操作符了,它們無處不在。一個符號所表達的意思越多,編譯器就越難檢測到這個符號在你的使用中所存在的異常情況。
另外在操作符的優(yōu)先級上,我完全能夠感同身受,初學C語言,甚至在學完C語言很久一段時間之內(nèi),我都沒有真正的完全搞清楚過操作符的優(yōu)先級,憑感覺用吧,一般來說結(jié)果都是錯的,不過用多了,可能也就會了。還記得->這個操作符在結(jié)構指針中的使用嗎,我們知道->這個操作符是對一個結(jié)構成員進行解引用,它所代表的意思p->f也就相當于(*p).f,不過千萬別忘了添加括號哦,因為”."操作符的優(yōu)先級大于"*",這個問題也是導致->操作符出現(xiàn)的原因之一,類似的還有很多,比如[]的優(yōu)先級高于*,int *p[]這個表達式呢代表p是一個元素為int指針的數(shù)組,而不是說p是個指向int數(shù)組的指針哦。不過在多年前,Dennis Ritchie解釋了這些不正常的情況是如何由于歷史的偶然原因而產(chǎn)生的,最大的原因還是,如果現(xiàn)在把它們更改過來的話,現(xiàn)有的大量代碼都可能出現(xiàn)問題。
最后,少做之過的特性就是語言應該提供但未提供的特性,如標準參數(shù)處理以及把lint程序錯誤的從編譯器中分離出來。
標準參數(shù)處理這個問題不管是在UNIX還是在C語言中都沒有得到好好的處理,因為參數(shù)與文件名,程序是分不清楚的。其中一個例子就是在在UNIX中創(chuàng)建一個文件,文件名以’-‘連字符開頭,然后卻發(fā)現(xiàn)無法用rm命令把連字符去掉,這就是它分不清文件名與參數(shù)的影響,書中還給出一個有趣的實例——關于在1990年以前給“用戶名的第二個字母是f的用戶”發(fā)郵件,那么他將收不到,進一步讓我們理解分不清參數(shù)與文件名的影響。
而lint程序,甚至現(xiàn)在好多使用C語言的人都沒有聽過,在早期的C語言中,語言設計者作出了明確的規(guī)定——把編譯器中所有的語義檢查措施全部分離出來,錯誤檢查由一個單獨的程序完成,這個程序被稱為“l(fā)int”,在省掉lint之后,編譯器可以做得更小,更快而且更簡單,所以理所當然的,它被去掉了,不過,所付出的代價是,代碼中悄悄混入了大量的Bug和不可靠的編碼風格,許多程序員缺省情況下在每次編譯中并不使用lint。在書中給出了一些實例,是一些程序員在寫代碼的過程中容易犯得錯誤而編譯器又檢查不出,如果使用lint程序,則可以全部檢查出來,所以作者大力推薦使用lint程序作為檢查。
下面,來介紹一下這個lint程序吧。
lint程序不但可以檢查出可移植性問題,而且可以檢查出那些雖然可移植并且完全合乎語法但卻很可能是錯誤的特性,lint程序會產(chǎn)生一系列程序員有必要從頭到尾仔細閱讀的診斷信息。
這是lint程序的系統(tǒng)版本:
UNIX系統(tǒng) 在UNIX系統(tǒng)中,可自動獲得lint,它是一個標準的UNIX工具。
Linux系統(tǒng) 在Linux各種發(fā)行版中,使用lint的版本是GNU下的Splint(前身是LClint)
Windows 在Windows系統(tǒng)中,從第三方獲得的lint工具的名稱是PC lint以及Splint
在這里,由于我使用的是Linux,所以介紹一下Linux中l(wèi)int的使用。
首先安裝splint工具:
sudo apt install splint
然后假定你要檢查的文件是main.c
splint main.c
其中main.c中代碼如下所示,使用了switch語句來測試:
#include <stdio.h>int main(void) { int x; scanf("%d",&x); switch(x){ case 3: printf("4\n"); case 4: printf("4\n"); } return 0; }
如果是直接gcc main.c,那么不會有任何提示,使用splint程序之后,它顯示了這些文本:
Splint 3.1.2 --- 03 May 2009main.c: (in function main) main.c:6:5: Return value (type int) ignored: scanf("%d", &x) Result returned by function call is not used. If this is intended, can cast result to (void) to eliminate message. (Use -retvalint to inhibit warning) main.c:10:10: Fall through case (no preceding break) Execution falls through from the previous case (use /*@fallthrough@*/ to mark fallthrough cases). (Use -casebreak to inhibit warning) Finished checking --- 2 code warnings
顯而易見的是,它給出了兩條提示,一條是說你的scanf語句的返回值并沒有用,另一條就是switch語句沒有break語句,并且提示你,如果確實不需要break語句,請用/*fallthrough*/把它注釋出來。
所以,多用lint程序來檢查你的程序吧,說不定會給你一個驚喜。