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