c語言陷阱和缺陷_第1頁
c語言陷阱和缺陷_第2頁
c語言陷阱和缺陷_第3頁
c語言陷阱和缺陷_第4頁
已閱讀5頁,還剩49頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領

文檔簡介

C語言陷阱和缺陷[譯序]那些自認為已經(jīng)“學完”c語言的人,請你們仔細讀閱讀這篇文章吧。路還長,很多東西要學。我也是……[概述]C語言像一把雕刻刀,鋒利,并且在技師手中非常有用。和任何鋒利的工具ー樣,C會傷到那些不能掌握它的人。本文介紹C語言傷害粗心的人的方法,以及如何避免傷害。[內(nèi)容]0簡介!詞法缺陷.1=不是==2&和|不是&&和!|1.3多字符記號4例外1.5字符串和字符2句法缺陷1理解聲明2運算符并不總是具有你所想象的優(yōu)先級3看看這些分號!4switch語句5函數(shù)調(diào)用6懸掛else問題3鏈接1你必須自己檢查外部類型4語義缺陷1表達式求值順序4.2&&、||和!運算符3下標從零開始4.4C并不總是轉(zhuǎn)換實參4.5指針不是數(shù)組6避免提喻法空指針不是空字符串整數(shù)溢出9移位運算符5庫函數(shù)1getc()返回整數(shù)5.2緩沖輸出和內(nèi)存分配6預處理器1宏不是函數(shù)6.2宏不是類型定義7可移植性缺陷1ー個名字中都有什么?ー個整數(shù)有多大?字符是帶符號的還是無符號的?右移位是帶符號的還是無符號的?除法如何舍入?ー個隨機數(shù)有多大?7大小寫轉(zhuǎn)換先釋放,再重新分配可移植性問題的ー個實例8這里是空閑空間參考腳注0簡介C語言及其典型實現(xiàn)被設計為能被專家們?nèi)菀椎厥褂?。這門語言簡潔并附有表達カ。但有一些限制可以保護那些浮躁的人。一個浮躁的人可以從這些條款中獲得一些幫助。在本文中,我們將會看ー看這些未可知的益處。這是由于它的未可知,我們無法為其進行完全的分類。不過,我們?nèi)匀煌ㄟ^研究為了一個C程序的運行所需要做的事來做到這些。我們假設讀者對c語言至少有個粗淺的了解。第一部分研究了當程序被劃分為記號時會發(fā)生的問題。第二部分繼續(xù)研究了當程序的記號被編譯器組合為聲明、表達式和語句時會出現(xiàn)的問題。第三部分研究了由多個部分組成、分別編譯并綁定到ー起的c程序。第四部分處理了概念上的誤解:當ー個程序具體執(zhí)行時會發(fā)生的事情。第五部分研究了我們的程序和它們所使用的常用庫之間的關系。在第六部分中,我們注意到了我們所寫的程序也不并不是我們所運行的程序;預處理器將首先運行。最后,第七部分討論了可移植性問題:一個能在ー個實現(xiàn)中運行的程序無法在另ー個實現(xiàn)中運行的原因。!詞法缺陷編譯器的第一個部分常被稱為詞法分析器(lexicalanalyzer)。詞法分析器檢查組成程序的字符序列,并將它們劃分為記號(token)ー個記號是ー個有一個或多個字符的序列,它在語言被編譯時具有一個(相關地)統(tǒng)ー的意義。在C中,例如,記號ー》的意義和組成它的每個獨立的字符具有明顯的區(qū)別,而且其意義獨立于ー》出現(xiàn)的上下文環(huán)境。另外一個例子,考慮下面的語句:if(x>big)big=x;該語句中的每一個分離的字符都被劃分為ー個記號,除了關鍵字if和標識符big的兩個實例。事實上,C程序被兩次劃分為記號。首先是預處理器讀取程序。它必須對程序進行記號劃分以發(fā)現(xiàn)標識宏的標識符。它必須通過對每個宏進行求值來替換宏調(diào)用。最后,經(jīng)過宏替換的程序又被匯集成字符流送給編譯器。編譯器再第二次將這個流劃分為記號。在這ー節(jié)中,我們將探索對記號的意義的普遍的誤解以及記號和組成它們的字符之間的關系。稍后我們將談到預處理器。1.1=不是==從Algol派生出來的語言,如Pascal和Ada,用:=表示賦值而用=表示比較。而C語言則是用=表示賦值而用==表示比較。這是因為賦值的頻率要高于比較,因此為其分配更短的符號。此外,C還將賦值視為ー個運算符,因此可以很容易地寫出多重賦值(如a=b=c),并且可以將賦值嵌入到ー個大的表達式中。這種便捷導致了一個潛在的問題:可能將需要比較的地方寫成賦值。因此,下面的語句好像看起來是要檢查x是否等于y:if(x=y)foo();而實際上是將x設置為y的值并檢查結果是否非零。在考慮下面的ー個希望跳過空格、制表符和換行符的循環(huán):while(c==''||c='\t'IIc=='\n')c=getc(f);在與‘'t’進行比較的地方程序員錯誤地使用=代替了==。這個“比較”實際上是將'\ピ賦給c,然后判斷c的(新的)值是否為零。因為、ピ不為零,這個“比較”將一直為真,因此這個循環(huán)會吃盡整個文件。這之后會發(fā)生什么取決于特定的實現(xiàn)是否允許一個程序讀取超過文件尾部的部分。如果允許,這個循環(huán)會一直運行。ー些C編譯器會對形如el=e2的條件給出ー個警告以提醒用戶。當你趨勢需要先對一個變量進行賦值之后再檢查變量是否非零時,為了在這種編譯器中避免警告信息,應考慮顯式給出比較符。換句話說,將:if(x=y)foo();改寫為:if((x=y)!=0)foo();這樣可以清晰地表示你的意圖。1.2&和|不是&&和!|容易將==錯寫為=是因為很多其他語言使用=表示比較運算。其他容易寫錯的運算符還有&和&&,或I和丨丨,這主要是因為C語言中的&和|運算符于其他語言中具有類似功能的運算符大為不同。我們將在第4節(jié)中貼近地觀察這些運算符。1.3多字符記號ー些C記號,如/、?和=只有一個字符。而其他ー些C記號,如/?和==,以及標識符,具有多個字符。當c編譯器遇到緊連在ー起的/和?時,它必須能夠決定是將這兩個字符識別為兩個分離的記號還是ー個單獨的記號。c語言參考手冊說明了如何決定:“如果輸入流到ー個給定的字符串為止已經(jīng)被識別為記號,則應該包含下ー個字符以組成能夠構成記號的最長的字符串”。因此,如果/是ー個記號的第一個字符,并且/后面緊隨了一個?,則這兩個字符構成了注釋的開始,不管其他上下文環(huán)境。下面的語句看起來像是將y的值設置為x的值除以p所指向的值:y=x/*p/*p指向除數(shù)?/;實際上,/?開始了一個注釋,因此編譯器簡單地吞噬程序文本,直到*/的出現(xiàn)。換句話說,這條語句僅僅把y的值設置為x的值,而根本沒有看到P。將這條語句重寫為:y=x/*p/*p指向除數(shù)?/;或者干脆是y=x/(*p)/*p指向除數(shù)?/;它就可以做注釋所暗示的除法了。這種模棱兩可的寫法在其他環(huán)境中就會引起麻煩。例如,老版本的C使用=+表示現(xiàn)在版本中的+=。這樣的編譯器會將a=-l;視為a=-1;或a-a-1;這會讓打算寫的程序員感到吃驚。另一方面,這種老版本的C編譯器會將a=/*b;斷句為a-/*b;盡管/?看起來像ー個注釋。4例外組合賦值運算符如+=實際上是兩個記號。因此,a+/*strange*/=1和a+=1是ー個意思。看起來像ー個單獨的記號而實際上是多個記號的只有這ー個特例。特別地,p->a是不合法的。它和p->a不是同義詞。另一方面,有些老式編譯器還是將=+視為ー個單獨的記號并且和+=是同義詞。1.5字符串和字符單引號和雙引號在C中的意義完全不同,在一些混亂的上下文中它們會導致奇怪的結果而不是錯誤消息。包圍在單引號中的一個字符只是書寫整數(shù)的另ー種方法。這個整數(shù)是給定的字符在實現(xiàn)的對照序列中的ー個對應的值。因此,在ー個ASCII實現(xiàn)中,‘a(chǎn)'和0141或97表示完全相同的東西。而ー個包圍在雙引號中的字符串,只是書寫ー個有雙引號之間的字符和一個附加的二進制值為零的字符所初始化的ー個無名數(shù)組的指針的ー種簡短方法。線面的兩個程序片斷是等價的:printf("Helloworld\n");charhell。ロ={'H','e','I','I',‘〇','','w',‘〇','r','I','d',''n',0};printf(hello);使用一個指針來代替一個整數(shù)通常會得到一個警告消息(反之亦然),使用雙引號來代替單引號也會得到ー個警告消息(反之亦然)。但對于不檢查參數(shù)類型的編譯器卻除外。因此,用printf('\n');來代替printf(〃、n〃);通常會在運行時得到奇怪的結果。由于ー個整數(shù)通常足夠大,以至于能夠放下多個字符,一些C編譯器允許在一個字符常量中存放多個字符。這意味著用‘yes’代替''yes〃將不會被發(fā)現(xiàn)。后者意味著“分別包含y、e、s和一個空字符的四個連續(xù)存貯器區(qū)域中的第一個的地址”,而前者意味著“在ー些實現(xiàn)定義的樣式中表示由字符y、e、s聯(lián)合構成的ー個整數(shù)”。這兩者之間的任何一致性都純屬巧合。2句法缺陷要理解C語言程序,僅了解構成它的記號是不夠的。還要理解這些記號是如何構成聲明、表達式、語句和程序的。盡管這些構成通常都是定義良好的,但這些定義有時候是有悖于直覺的或混亂的。在這ー節(jié)中,我們將著眼于ー些不明顯句法構造。1理解聲明我曾經(jīng)和一些人聊過天,他們那時在書寫在ー個小型的微處理器上單機運行的C程序。當這臺機器的開關打開的時候,硬件會調(diào)用地址為0處的子程序。為了模仿電源打開的情形,我們要設計一條C語句來顯式地調(diào)用這個子程序。經(jīng)過ー些思考,我們寫出了下面的語句:(*(void(*)())0)();這樣的表達式會令C程序員心驚膽戰(zhàn)。但是,并不需要這樣,因為他們可以在一個簡單的規(guī)則的幫助下很容易地構造它:以你使用的方式聲明它。每個C變量聲明都具有兩個部分:ー個類型和一組具有特定格式的期望用來對該類型求值的表達式。最簡單的表達式就是ー個變量:floatf,g;說明表達式f和g——在求值的時候——具有類型floato由于待求值的時表達式,因此可以自由地使用圓括號:float((f));者表示((f))求值為float并且因此,通過推斷,f也是ー個float。同樣的邏輯用在函數(shù)和指針類型。例如:floatff();表示表達式ff0是一個float,因此ff是ー個返回ー個float的函數(shù)。類似地,float*pf;表示?pf是ー個float并且因此pf是ーIs?指向ー個float的指針。這些形式的組合聲明對表達式是ー樣的。因此,float*g(),(*h)();表示?g()和(*h)()都是float表達式。由于〇比*綁定得更緊密,*g()和?(g())表示同樣的東西:g是ー個返回指float指針的函數(shù),而h是ー個指向返回float的函數(shù)的指針。當我們知道如何聲明一個給定類型的變量以后,就能夠很容易地寫出ー個類型的模型(casD:只要刪除變量名和分號并將所有的東西包圍在ー對圓括號中即可。因此,由于float*g();聲明g是ー個返回float指針的函數(shù),所以(float*())就是它的模型。有了這些知識的武裝,我們現(xiàn)在可以準備解決(*(void(*)())0)()了。我們可以將它分為兩個部分進行分析。首先,假設我們有一個變量fp,它包含了一個函數(shù)指針,并且我們希望調(diào)用fp所指向的函數(shù)。可以這樣寫:(*fp)0;如果fp是ー個指向函數(shù)的指針,貝リ?fp就是函數(shù)本身,因此(*fp)()是調(diào)用它的ー種方法。(*fp)中的括號是必須的,否則這個表達式將會被分析為?(fp())。我們現(xiàn)在要找ー個適當?shù)谋磉_式來替換fpo這個問題就是我們的第二步分析。如果c可以讀入并理解類型,我們可以寫:(*0)0;但這樣并不行,因為?運算符要求必須有一個指針作為他的操作數(shù)。另外,這個操作數(shù)必須是ー個指向函數(shù)的指針,以保證?的結果可以被調(diào)用。因此,我們需要將。轉(zhuǎn)換為ー個可以描述“指向一個返回void的函數(shù)的指針”的類型。如果fp是ー個指向返回void的函數(shù)的指針,貝リ(*fp)()是一個void值,并且它的聲明將會是這樣的:void(*fp)();因此,我們需要寫:void(*fp)();(*fp)0;來聲明一個啞變量。一旦我們知道了如何聲明該變量,我們也就知道了如何將一個常數(shù)轉(zhuǎn)換為該類型:只要從變量的聲明中去掉名字即可。因此,我們像下面這樣將。轉(zhuǎn)換為ー個“指向返回void的函數(shù)的指針”:(void(*)())0接下來,我們用(void(*)())0來替換fp:(*(void(*)())0)();結尾處的分號用于將這個表達式轉(zhuǎn)換為ー個語句。在這里,我們就解決了這個問題時沒有使用typedef聲明。通過使用它,我們可以更清晰地解決這個問題:typedefvoid(*funcptr)();(*(funcptr)0)();2運算符并不總是具有你所想象的優(yōu)先級假設有一個聲明了的常量FLAG是ー個整數(shù),其二進制表示中的某ー位被置位(換句話說,它是2的某次幕),并且你希望測試ー個整型變量flags該位是否被置位。通常的寫法是:if(flags&FLAG)...其意義對于很多C程序員都是很明確的:if語句測試括號中的表達式求值的結果是否為〇〇出于清晰的目的我們可以將它寫得更明確:if(flags&FLAG!=0)...這個語句現(xiàn)在更容易理解了。但它仍然是錯的,因為!=比&綁定得更緊密,因此它被分析為:if(flags&(FLAG!=0))...這(偶爾)是可以的,如FLAG是1或。(!)的時候,但對于其他2的裏是不行的[2]。假設你有兩個整型變量,h和1,它們的值在。和15(含。和15)之間,并且你希望將r設置為8位值,其低位為1,高位為h。ー種自然的寫法是:r=hくく4+1;不幸的是,這是錯誤的。加法比移位綁定得更緊密,因此這個例子等價于:r=hくく(4+1);正確的方法有兩種:r=(h?4)+1;r=hくく4I1;避免這種問題的ー個方法是將所有的東西都用括號括起來,但表達式中的括號過度就會難以理解,因此最好還是是記住C中的優(yōu)先級。不幸的是,這有15個,太困難了。然而,通過將它們分組可以變得容易。綁定得最緊密的運算符并不是真正的運算符:下標、函數(shù)調(diào)用和結構選擇。這些都與左邊相關聯(lián)。接下來是一元運算符。它們具有真正的運算符中的最高優(yōu)先級。由于函數(shù)調(diào)用比一元運算符綁定得更緊密,你必須寫(*p)()來調(diào)用P指向的函數(shù):*P()表示P是ー個返回一個指針的函數(shù)。轉(zhuǎn)換是一元運算符,并且和其他一元運算符具有相同的優(yōu)先級。一元運算符是右結合的,因此?P++表示*(p++),而不是(*p)++〇在接下來是真正的二元運算符。其中數(shù)學運算符具有最高的優(yōu)先級,然后是移位運算符、關系運算符、邏輯運算符、賦值運算符,最后是條件運算符。需要記住的兩個重要的東西是:所有的邏輯運算符具有比所有關系運算符都低的優(yōu)先級。一位運算符比關系運算符綁定得更緊密,但又不如數(shù)學運算符。在這些運算符類別中,有一些奇怪的地方。乘法、除法和求余具有相同的優(yōu)先級,加法和減法具有相同的優(yōu)先級,以及移位運算符具有相同的優(yōu)先級。還有就是六個關系運算符并不具有相同的優(yōu)先級:==和!=的優(yōu)先級比其他關系運算符要低。這就允許我們判斷a和b是否具有與c和d相同的順序,例如:aくb==cくd在邏輯運算符中,沒有任何兩個具有相同的優(yōu)先級。按位運算符比所有順序運算符綁定得都緊密,每種與運算符都比相應的或運算符綁定得更緊密,并且按位異或〇運算符介于按位與和按位或之間。三元運算符的優(yōu)先級比我們提到過的所有運算符的優(yōu)先級都低。這可以保證選擇表達式中包含的關系運算符的邏輯組合特性,如:z=a<b&&b<c?d:e這個例子還說明了賦值運算符具有比條件運算符更低的優(yōu)先級是有意義的。另外,所有的復合賦值運算符具有相同的優(yōu)先級并且是自右至左結合的,因此a=b=c和b=c;a=b;是等價的。具有最低優(yōu)先級的是逗號運算符。這很容易理解,因為逗號通常在需要表達式而不是語句的時候用來替代分號。賦值是另一種運算符,通常具有混合的優(yōu)先級。例如,考慮下面這個用于復制文件的循環(huán):while(c=getc(in)!=EOF)putc(c,out);這個while循環(huán)中的表達式看起來像是c被賦以getc(in)的值,接下來判斷是否等于EOF以結束循環(huán)。不幸的是,賦值的優(yōu)先級比任何比較操作都低,因此c的值將會是getc(in)和EOF比較的結果,并且會被拋棄。因此,“復制”得到的文件將是ー個由值為1的字節(jié)流組成的文件。上面這個例子正確的寫法并不難:while((c=getc(in))!=EOF)putc(c,out);然而,這種錯誤在很多復雜的表達式中卻很難被發(fā)現(xiàn)。例如,隨UNIX系統(tǒng)一同發(fā)布的lint程序通常帶有下面的錯誤行:if(((t=BTYPE(ptl->aty)==STRTY)||t==UNIONTY){這條語句希望給t賦ー個值,然后看t是否與STRTY或UNIONTY相等。而實際的效果卻大不相同[3]。C中的邏輯運算符的優(yōu)先級具有歷史原因。B—C的前輩——具有和C中的&和|運算符對應的邏輯運算符。盡管它們的定義是按位的,但編譯器在條件判斷上下文中將它們視為和&&和丨丨ー樣。當在c中將它們分開后,優(yōu)先級的改變是很危險的[4]。3看看這些分號!C中的一個多余的分號通常會帶來一點點不同:或者是ー個空語句,無任何效果;或者編譯器可能提出ー個診斷消息,可以方便除去掉它。ー個重要的區(qū)別是在必須跟有一個語句的if和while語句中。考慮下面的例子:if(x[i]>big);big=x[i];這不會發(fā)生編譯錯誤,但這段程序的意義與:if(x[i]>big)big=x[i];就大不相同了。第一個程序段等價于:if(x[i]>big){}big=x[i];也就是等價于:big=x[i];(除非x、i或big是帶有副作用的宏)。另ー個因分號引起巨大不同的地方是函數(shù)定義前面的結構聲明的末尾[譯注:這句話不太好聽,看例子就明白了]。考慮下面的程序片段:structfoo{intx;f(){在緊挨著f的第一個}后面丟失了一個分號。它的效果是聲明了一個函數(shù)f,返回值類型是structfoo?這個結構成了函數(shù)聲明的?一部分。如果這里出現(xiàn)了分號,則f將被定義為具有默認的整型返回值[5]〇4switch語句通常C中的switch語句中的case段可以進入下ー個。例如,考慮下面的C和Pascal程序片斷:switch(color){printf("red");break;printf("yellow");break;printf("blue");break;)casecolorofwrite('red));write('yellow);write('blue);end這兩個程序片斷都作相同的事情:根據(jù)變量color的值是1、2還是3打印red、yellow或blue(沒有新行符)。這兩個程序片斷非常相似,只有一點不同:Pascal程序中沒有C中相應的break語句。C中的case標簽是真正的標簽:控制流程可以無限制地進入到ー個case標簽中??纯戳硪环N形式,假設C程序段看起來更像Pascal:switch(color){printf("red");printf("yellow");printf("blue");ェ并且假設color的值是2〇則該程序?qū)⒋蛴ellowblue,因為控制自然地轉(zhuǎn)入到下ー個printf()的調(diào)用。這既是C語言switch語句的優(yōu)點又是它的弱點。說它是弱點,是因為很容易忘記ー個break語句,從而導致程序出現(xiàn)隱晦的異常行為。說它是優(yōu)點,是因為通過故意去掉break語句,可以很容易實現(xiàn)其他方法難以實現(xiàn)的控制結構。尤其是在ー個大型的switch語句中,我們經(jīng)常發(fā)現(xiàn)對ー個case的處理可以簡化其他ー些特殊的處理。例如,設想有一個程序是一臺假想的機器的翻譯器。這樣的ー個程序可能包含ー個switch語句來處理各種操作碼。在這樣一臺機器上,通常減法在對其第二個運算數(shù)進行變號后就變成和加法一樣了。因此,最好可以寫出這樣的語句:caseSUBTRACT:opnd2=-opnd2;/*nobreak;*/caseADD:另外一個例子,考慮編譯器通過跳過空白字符來查找ー個記號。這里,我們將空格、制表符和新行符視為是相同的,除了新行符還要引起行計數(shù)器的增長外:case'\n':linecount++;/*nobreak*/case'\t':case'':2.5函數(shù)調(diào)用和其他程序設計語言不同,C要求一個函數(shù)調(diào)用必須有一個參數(shù)列表,但可以沒有參數(shù)。因此,如果f是ー個函數(shù),f();就是對該函數(shù)進行調(diào)用的語句,而f;什么也不做。它會作為函數(shù)地址被求值,但不會調(diào)用它[6]。2.6懸掛else問題在討論任何語法缺陷時我們都不會忘記提到這個問題。盡管這ー問題不是C語言所獨有的,但它仍然傷害著那些有著多年經(jīng)驗的C程序員〇考慮下面的程序片斷:if(x==0)if(y==0)error();else{z=x+y;f(&z);寫這段程序的程序員的目的明顯是將情況分為兩種:*=0和ス!=0。在第一種情況中,程序段什么都不做,除非y=0時調(diào)用error。。第二種情況中,程序設置z=x+y并以z的地址作為參數(shù)調(diào)用f()o然而,這段程序的實際效果卻大為不同。其原因是ー個else總是與其最近的if相關聯(lián)。如果我們希望這段程序能夠按照實際的情況運行,應該這樣寫:if(x==0){if(y==0)error();else{z=x+y;f(&z);換句話說,當X!=0發(fā)生時什么也不做。如果要達到第一個例子的效果,應該寫:if(x==0){if(y==0)error();else{z=z+y;f(&z);3鏈接ー個C程序可能有很多部分組成,它們被分別編譯,并由一個通常稱為鏈接器、鏈接編輯器或加載器的程序綁定到一起。由于編譯器一次通常只能看到ー個文件,因此它無法檢測到需要程序的多個源文件的內(nèi)容才能發(fā)現(xiàn)的錯誤。在這ー節(jié)中,我們將看到ー些這種類型的錯誤。有一些C實現(xiàn),但不是所有的,帶有一個稱為lint的程序來捕獲這些錯誤。如果具有一個這樣的程序,那么無論怎樣地強調(diào)它的重要性都不過分。3.1你必須自己檢查外部類型假設你有一個C程序,被劃分為兩個文件。其中一個包含如下聲明:intn;而令一Is?包含如下聲明:longn;這不是一個有效的C程序,因為一些外部名稱在兩個文件中被聲明為不同的類型。然而,很多實現(xiàn)檢測不到這個錯誤,因為編譯器在編譯其中一個文件時并不知道另一個文件的內(nèi)容。因此,檢查類型的工作只能由鏈接器(或ー些工具程序如lint)來完成;如果操作系統(tǒng)的鏈接器不能識別數(shù)據(jù)類型,C編譯器也沒法過多地強制它。那么,這個程序運行時實際會發(fā)生什么?這有很多可能性:實現(xiàn)足夠聰明,能夠檢測到類型沖突。則我們會得到ー個診斷消息,說明n在兩個文件中具有不同的類型。你所使用的實現(xiàn)將int和long視為相同的類型。典型的情況是機器可以自然地進行32位運算。在這種情況下你的程序或許能夠工作,好象你兩次都將變量聲明為long(或int)。但這種程序的工作純屬偶然。n的兩個實例需要不同的存儲,它們以某種方式共享存儲區(qū),即對其中一個的賦值對另ー個也有效。這可能發(fā)生,例如,編譯器可以將int安排在long的低位。不論這是基于系統(tǒng)的還是基于機器的,這種程序的運行同樣是偶然。n的兩個實例以另ー種方式共享存儲區(qū),即對其中一個賦值的效果是對另ー個賦以不同的值。在這種情況下,程序可能失敗。這種情況發(fā)生的里一個例子出奇地頻繁。程序的某ー個文件包含下面的聲明:charfilename[]="etc/passwd”;而另一個文件包含這樣的聲明:char*filename;盡管在某些環(huán)境中數(shù)組和指針的行為非常相似,但它們是不同的。在第一個聲明中,filename是一個字符數(shù)組的名字。盡管使用數(shù)組的名字可以產(chǎn)生數(shù)組第一個元素的指針,但這個指針只有在需要的時候ォ產(chǎn)生并且不會持續(xù)。在第二個聲明中,filename是一個指針的名字。這個指針可以指向程序員讓它指向的任何地方。如果程序員沒有給它賦ー個值,它將具有一個默認的〇值(null)[譯注:實際上,在C中一個為初始化的指針通常具有一個隨機的值,這是很危險的!]。這兩個聲明以不同的方式使用存儲區(qū),他們不可能共存。避免這種類型沖突的ー個方法是使用像lint這樣的工具(如果可以的話)。為了在ー個程序的不同編譯單元之間檢查類型沖突,ー些程序需要一次看到其所有部分。典型的編譯器無法完成,但lint可以。避免該問題的另ー種方法是將外部聲明放到包含文件中。這時,ー個外部對象的類型僅出現(xiàn)一次[7]〇4語義缺陷ー個句子可以是精確拼寫的并且沒有語法錯誤,但仍然沒有意義。在這ー節(jié)中,我們將會看到ー些程序的寫法會使得它們看起來是ー個意思,但實際上是另一種完全不同的意思。我們還要討論ー些表面上看起來合理但實際上會產(chǎn)生未定義結果的環(huán)境。我們這里討論的東西并不保證能夠在所有的C實現(xiàn)中工作。我們暫且忘記這些能夠在ー些實現(xiàn)中工作但可能不能在另ー些實現(xiàn)中工作的東西,直到第7節(jié)討論可以執(zhí)行問題為止。4.1表達式求值順序ー些C運算符以ー種已知的、特定的順序?qū)ζ洳僮鲾?shù)進行求值。但另ー些不能。例如,考慮下面的表達式:aくb&&cくdC語言定義規(guī)定a<b首先被求值。如果a確實小于b,c<d必須緊接著被求值以計算整個表達式的值。但如果a大于或等于b,則c<d根本不會被求值。要對a<b求值,編譯器對a和b的求值就會有一個先后。但在ー些機器上,它們也許是并行進行的。C中只有四個運算符&&、丨|、?:和,指定了求值順序。&&和丨|最先對左邊的操作數(shù)進行求值,而右邊的操作數(shù)只有在需要的時候オ進行求值。而?:運算符中的三個操作數(shù):a、b和c,最先對a進行求值,之后僅對b或c中的ー個進行求值,這取決于a的值。,運算符首先對左邊的操作數(shù)進行求值,然后拋棄它的值,對右邊的操作數(shù)進行求值[8]〇C中所有其它的運算符對操作數(shù)的求值順序都是未定義的。事實上,賦值運算符不對求值順序做出任何保證。出于這個原因,下面這種將數(shù)組x中的前n個元素復制到數(shù)組y中的方法是不可行的:i=0;while(i<n)y[i]=x[i++];其中的問題是y[i]的地址并不保證在i增長之前被求值。在某些實現(xiàn)中,這是可能的;但在另ー些實現(xiàn)中卻不可能。另ー種情況出于同樣的原因會失敗:i=0;while(i<n)y[i++]=x[i];而下面的代碼是可以工作的:i=0;while(i<n){y[i]=x[i];i++;當然,這可以簡寫為:for(i=0;i<n;i++)y[i]=x[i];4.2&&、II和!運算符C中有兩種邏輯運算符,在某些情況下是可以交換的:按位運算符&、I和1以及邏輯運算符&&、丨|和!。一個程序員如果用某一類運算符替換相應的另ー類運算符會得到某些奇怪的效果:程序可能會正確地工作,但這純屬偶然。&&、丨丨和!運算符將它們的參數(shù)視為僅有“真”或“假”,通常約定。代表“假”而其它的任意值都代表“真”。這些運算符返回1表示“真”而返回〇表示“假”,而且&&和丨I運算符當可以通過左邊的操作數(shù)確定其返回值時,就不會對右邊的操作數(shù)進行求值。因此!10是零,因為10非零;10&&12是1,因為10和12都非零;101112也是1,因為10非零。另外,最后一個表達式中的12不會被求值,10IIf()中的f()也不會被求值。考慮下面這段用于在ー個表中查找ー個特定元素的程序:i=0;while(i<tabsize&&tab[i]!=x)i++;這段循環(huán)背后的意思是如果i等于tabsize時循環(huán)結束,元素未被找到。否則,i包含了元素的索引。假設這個例子中的&&不小心被替換為了&,這個循環(huán)可能仍然能夠エ作,但只有兩種幸運的情況可以使它停下來。首先,這兩個操作都是當條件為假時返回〇,當條件為真時返回1。只要x和y都是1或0,x&y和x&&y都具有相同的值。然而,如果當使用了出了1之外的非零值表示“真”時互換了這兩個運算符,這個循環(huán)將不會工作。其次,由于數(shù)組元素不會改變,因此越過數(shù)組最后ー個元素進ー個位置時是無害的,循環(huán)會幸運地停下來。失誤的程序會越過數(shù)組的結尾,因為&不像&&,總是會對所有的操作數(shù)進行求值。因此循環(huán)的最后ー次獲取tab[i]時i的值已經(jīng)等于tabsize了。如果tabsize是tab中元素的數(shù)量,則會取到tab中不存在的ー個值。.3下標從零開始在很多語言中,具有n個元素的數(shù)組其元素的號碼和它的下標是從1到n嚴格對應的。但在C中不是這樣。ー個具有n個元素的C數(shù)組中沒有下標為n的元素,其中的元素的下標是從。到n-1〇因此從其它語言轉(zhuǎn)到C語言的程序員應該特別小心地使用數(shù)組:inti,a[10];for(i=1;i<=10;i++)a[i]=0;這個例子的目的是要將a中的每個元素都設置為。,但沒有期望的效果。因為for語句中的比較iく10被替換成了iく=10,a中的一個編號為10的并不存在的元素被設置為了〇,這樣內(nèi)存中a后面的一個字被破壞了。如果編譯該程序的編譯器按照降序地址為用戶變量分配內(nèi)存,則a后面就是i。將i設置為零會導致該循環(huán)陷入ー個無限循環(huán)。4.4C并不總是轉(zhuǎn)換實參下面的程序段由于兩個原因會失敗:doubles;=sqrt(2);printf(,/%g\n/,,s);第一個原因是sqrt()需要一個double值作為它的參數(shù),但沒有得到。第二個原因是它返回一個double值但沒有這樣聲名。改正的方法只有一個:doubles,sqrt();=sqrt(2.0);printf(“/g\n",s);C中有兩個簡單的規(guī)則控制著函數(shù)參數(shù)的轉(zhuǎn)換:(1)比int短的整型被轉(zhuǎn)換為int;(2)比double短的浮點類型被轉(zhuǎn)換為double。所有的其它值不被轉(zhuǎn)換。確保函數(shù)參數(shù)類型的正確行使程序員的責任。因此,ー個程序員如果想使用如sqrt()這樣接受ーI"*double類型參數(shù)的函數(shù),就必須僅傳遞給它float或double類型的參數(shù)。常數(shù)2是一個int,因此其類型是錯誤的。當ー個函數(shù)的值被用在表達式中時,其值會被自動地轉(zhuǎn)換為適當?shù)念愋?。然?為了完成這個自動轉(zhuǎn)換,編譯器必須知道該函數(shù)實際返回的類型。沒有更進一步聲名的函數(shù)被假設返回int,因此聲名這樣的函數(shù)并不是必須的。然而,sqrt()返回double,因此在成功使用它之前必須要聲名。實際上,C實現(xiàn)通常允許ー個文件包含include語句來包含如sqrt()這些庫函數(shù)的聲名,但是對那些自己寫函數(shù)的程序員來說,書寫聲名也是必要的——或者說,對那些書寫非凡的C程序的人來說是有必要的。這里有一個更加壯觀的例子:main(){inti;charc;for(i=0;i<5;i++){scanf("%d",&c);printf('%d”,i);)printf('\n");表面上看,這個程序從標準輸入中讀取五個整數(shù)并向標準輸出寫入〇1234〇實際上,它并不總是這么做。譬如在一些編譯器中,它的輸出為000001234。為什么?因為c的聲名是char而不是into當你令scanf()去讀取ー個整數(shù)時,它需要一個指向ー個整數(shù)的指針。但這里它得到的是一個字符的指針。但scanf()并不知道它沒有得到它所需要的:它將輸入看作是一個指向整數(shù)的指針并將一個整數(shù)存貯到那里。由于整數(shù)占用比字符更多的內(nèi)存,這樣做會影響到c附近的內(nèi)存。c附近確切是什么是編譯器的事:在這種情況下這有可能是i的低位。因此,每當向c中讀入一個值,i就被置零。當程序最后到達文件結尾時,scanf()不再嘗試向c中放入新值,iオ可以正常地增長,直到循環(huán)結束。4.5指針不是數(shù)組C程序通常將一個字符串轉(zhuǎn)換為ー個以空字符結尾的字符數(shù)組。假設我們有兩個這樣的字符串s和t,并且我們想要將它們連接為一個單獨的字符串ro我們通常使用庫函數(shù)strcpyO和strcat()來完成。下面這種明顯的方法并不會工作:char*r;strcpy(r,s);strcat(r,t);這是因為r沒有被初始化為指向任何地方。盡管r可能潛在地表示某ー塊內(nèi)存,但這并不存在,直到你分配它。讓我們再試試,為r分配ー些內(nèi)存:charr[100];strcpy(r,s);strcat(r,t);這只有在s和t所指向的字符串不很大的時候才能夠工作。不幸的是,C要求我們?yōu)閿?shù)組指定的大小是ー個常數(shù),因此無法確定r是否足夠大。然而,很多C實現(xiàn)帶有一個叫做mallocO的庫函數(shù),它接受ー個數(shù)字并分配這么多的內(nèi)存。通常還有一個函數(shù)成為strlenO,可以告訴我們一個字符串中有多少個字符:因此,我們可以寫:char*r,*malloc();r=malloc(strlen(s)+strlen(t));strcpy(r,s);strcat(r,t);然而這個例子會因為兩個原因而失敗。首先,mallocO可能會耗盡內(nèi)存,而這個事件僅通過靜靜地返回一個空指針來表示。其次,更重要的是,mallocO并沒有分配足夠的內(nèi)存。一個字符串是以一個空字符結束的。而strlenO函數(shù)返回其字符串參數(shù)中所包含字符的數(shù)量,但不包括結尾的空字符。因此,如果strlen(s)是n,則s需要n+1個字符來盛放它。因此我們需要為r分配額外的ー個字符。再加上檢查mallocO是否成功,我們得到:char*r,*malloc();r=malloc(strlen(s)+strlen(t)+1);if(!r){complain();exit(1);)strcpy(r,s);strcat(r,t);6避免提喻法提喻法(Synecdoche,sin-ECK-duh-key)是ー種文學手法,有點類似于明喻或暗喻,在牛津英文詞典中解釋如下:“amorecomprehensivetermisusedforalesscomprehensiveorviceversa;aswholeforpartorpartforwhole,genusforspeciesorspeciesforgenus,etc.(將全面的單位用作不全面的單位,或反之;如整體對局部或局部對整體、一般對特殊或特殊對一般,等等。)”這可以精確地描述C中通常將指針誤以為是其指向的數(shù)據(jù)的錯誤。正將常會在字符串中發(fā)生。例如:char*p,*q;P二xyz;盡管認為p的值是xyz有時是有用的,但這并不是真的,理解這一點非常重要。P的值是指向一個有四個字符的數(shù)組中第0個元素的指針,這四個字符是’X’、’y’、’z’和‘、?!?。因此,如果我們現(xiàn)在執(zhí)行:q=P;P和q會指向同一塊內(nèi)存。內(nèi)存中的字符沒有因為賦值而被復制。這種情況看起來是這樣的:要記住的是,復制ー個指針并不能復制它所指向的東西。因此,如果之后我們執(zhí)行:q[l]='Y';q所指向的內(nèi)存包含字符串xYz。p也是,因為p和q指向相同的內(nèi)存??罩羔槻皇强兆址畬⒁粋€整數(shù)轉(zhuǎn)換為ー個指針的結果是實現(xiàn)相關的(implementation-dependent),除了一個例外。這個例外是常數(shù)〇,它可以保證被轉(zhuǎn)換為ー個與其它任何有效指針都不相等的指針。這個值通常類似這樣定義:^defineNULL0但其效果是相同的。要記住的一個重要的事情是,當用〇作為指針時它決不能被解除引用。換句話說,當你將〇賦給ー個指針變量后,你就不能訪問它所指向的內(nèi)存。不能這樣寫:if(p==(char*)0)...也不能這樣寫:if(strcmp(p,(char*)0)==0)…因為strcmpO總是通過其參數(shù)來查看內(nèi)存地址的。如果P是一個空指針,這樣寫也是無效的:printf(p);或printf('如“,p);整數(shù)溢出C語言關于整數(shù)操作的上溢或下溢定義得非常明確。只要有一次操作數(shù)是無符號的,結果就是無符號的,并且以2n為模,其中n為字長。如果兩個操作數(shù)都是帶符號的,則結果是未定義的。例如,假設a和b是兩個非負整型變量,你希望測試a+b是否溢出。ー個明顯的辦法是這樣的:if(a+b<0)complain();通常,這是不會工作的。一旦a+b發(fā)生了溢出,對于結果的任何賭注都是沒有意義的。例如,在某些機器上,ー個加法運算會將一個內(nèi)部寄存器設置為四種狀態(tài):正、負、零或溢出。在這樣的機器上,編譯器有權將上面的例子實現(xiàn)為首先將a和b加在ー起,然后檢查內(nèi)部寄存器狀態(tài)是否為負。如果該運算溢出,內(nèi)部寄存器將處于溢出狀態(tài),這個測試會失敗。使這個特殊的測試能夠成功的一個正確的方法是依賴于無符號算術的良好定義,既要在有符號和無符號之間進行轉(zhuǎn)換:if((int)((unsigned)a+(unsigned)b)<0)complain();9移位運算符兩個原因會令使用移位運算符的人感到煩惱:在右移運算中,空出的位是用〇填充還是用符號位填充?移位的數(shù)量允許使用哪些數(shù)?第一個問題的答案很簡單,但有時是實現(xiàn)相關的。如果要進行移位的操作數(shù)是無符號的,會移入。。如果操作數(shù)是帶符號的,則實現(xiàn)有權決定是移入〇還是移入符號位。如果在ー個右移操作中你很關心空位,那么用unsigned來聲明變量。這樣你就有權假設空位被設置為〇。第二個問題的答案同樣簡單:如果待移位的數(shù)長度為n,則移位的數(shù)量必須大于等于。并且嚴格地小于n。因此,在一次單獨的操作中不可能將所有的位從變量中移出。例如,如果ー個int是32位,且n是ー個int,寫nくく31和nくく。是合法的,但n?32和n?T是不合法的。注意,即使實現(xiàn)將符號為移入空位,對ー個帶符號整數(shù)的右移運算和除以2的某次哥也不是等價的。為了證明這一點,考慮(-1)?1的值,這是不可能為。的。[譯注:(-1)/2的結果是。。]5庫函數(shù)每個有用的C程序都會用到庫函數(shù),因為沒有辦法把輸入和輸出內(nèi)建到語言中去。在這ー節(jié)中,我們將會看到ー些廣泛使用的庫函數(shù)在某種情況下會出現(xiàn)的ー些非預期行為。1getc()返回整數(shù)考慮下面的程序:ttinclude<stdio.h>main(){charc;while((c=getchar())!=EOF)putchar(c);)這段程序看起來好像要講標準輸入復制到標準輸出。實際上,它并不完全會做這些。原因是c被聲明為字符而不是整數(shù)。這意味著它將不能接收可能出現(xiàn)的所有字符包括EOF〇因此這里有兩種可能性。有時ー些合法的輸入字符會導致c攜帶和EOF相同的值,有時又會使c無法存放EOF值。在前ー種情況下,程序會在文件的中間停止復制。在后一種情況下,程序會陷入一個無限循環(huán)。實際上,還存在著第三種可能:程序會偶然地正確工作。C語言參考手冊嚴格地定義了表達式((c=getchar())!=EOF)的結果。其6.1節(jié)中聲明:當ー個較長的整數(shù)被轉(zhuǎn)換為ー個較短的整數(shù)或ー個char時,它會被截去左側(cè):超出的位被簡單地丟棄。7.14節(jié)聲明:存在著很多賦值運算符,它們都是從右至左結合的。它們都需要一個左值作為左側(cè)的操作數(shù),而賦值表達式的類型就是其左側(cè)的操作數(shù)的類型。其值就是已經(jīng)付過值的左操作數(shù)的值。這兩個條款的組合效果就是必須通過丟棄getchar()的結果的高位,將其截短為字符,之后這個被截短的值再與EOF進行比較。作為這個比較的一部分,c必須被擴展為ー個整數(shù),或者采取將左側(cè)的位用〇填充,或者適當?shù)夭扇》枖U展。然而,ー些編譯器并沒有正確地實現(xiàn)這個表達式。它們確實將getchar()的值的低幾位賦給c。但在c和EOF的比較中,它們卻使用了getchar()的值!這樣做的編譯器會使這個事例程序看起來能夠“正確地”工作。5.2緩沖輸出和內(nèi)存分配當ー個程序產(chǎn)生輸出時,能夠立即看到它有多重要?這取決于程序。例如,終端上顯示輸出并要求人們坐在終端前面回答ー個問題,人們能夠看到輸出以知道該輸入什么就顯得至關重要了。另ー方面,如果輸出到ー個文件中,并最終被發(fā)送到ー個行式打印機,只有所有的輸出最終能夠到達那里是重要的。立即安排輸出的顯示通常比將其暫時保存在一大塊ー起輸出要昂貴得多。因此,c實現(xiàn)通常允許程序員控制產(chǎn)生多少輸出后在實際地寫出它們。這個控制通常約定為ー個稱為setbufO的庫函數(shù)。如果buf是ー個具有適當大小的字符數(shù)組,則setbuf(stdout,buf);將告訴I/O庫寫入到stdout中的輸出要以buf作為一個輸出緩沖,并且等到buf滿了或程序員直接調(diào)用fflushO再實際寫出。緩沖區(qū)的合適的大小在くstdio.h>中定義為BUFSIZo因此,下面的程序解釋了通過使用setbufO來講標準輸入復制到標準輸出:#include<stdio.h>main(){intc;charbuf[BUFSIZ];setbuf(stdout,buf);while((c=getchar())!=EOF)putchar(c);)不幸的是,這個程序是錯誤的,因為ー個細微的原因。要知道毛病出在哪,我們需要知道緩沖區(qū)最后一次刷新是在什么時候。答案:主程序完成之后,作為庫在將控制交回到操作系統(tǒng)之前所執(zhí)行的清理的一部分。在這ー時刻,緩沖區(qū)已經(jīng)被釋放了!有兩種方法可以避免這ー問題。首先,是用靜態(tài)緩沖區(qū),或者將其顯式地聲明為靜態(tài):staticcharbuf[BUFSIZ];或者將整個聲明移到主函數(shù)之外。另ー種可能的方法是動態(tài)地分配緩沖區(qū)并且從不釋放它:char*malloc();setbuf(stdout,malloc(BUFSIZ));注意在后一種情況中,不必檢查malloc()的返回值,因為如果它失敗了,會返回一個空指針。而setbuf()可以接受ー個空指針作為其第二個參數(shù),這將使得stdout變成非緩沖的。這會運行得很慢,但它是可以運行的。6預處理器運行的程序并不是我們所寫的程序:因為C預處理器首先對其進行了轉(zhuǎn)換。出于兩個主要原因(和很多次要原因),預處理器為我們提供了一些簡化的途徑。首先,我們希望可以通過改變一個數(shù)字并重新編譯程序來改變ー個特殊量(如表的大小)的所有實例[9]。其次,我們可能希望定義ー些東西,它們看起來象函數(shù)但沒有函數(shù)調(diào)用所需的運行開銷。例如,putcharO和getcharO通常實現(xiàn)為宏以避免對每ー個字符的輸入輸出都要進行函數(shù)調(diào)用。6.1宏不是函數(shù)由于宏可以象函數(shù)那樣出現(xiàn),有些程序員有時就會將它們視為等價的。因此,看下面的定義:^definemax(a,b)((a)>(b)?(a):(b))注意宏體中所有的括號。它們是為了防止出現(xiàn)a和b是帶有比》優(yōu)先級低的表達式的情況。ー個重要的問題是,像max()這樣定義的宏每個操作數(shù)都會出現(xiàn)兩次并且會被求值兩次。因此,在這個例子中,如果a比b大,則a就會被求值兩次:一次是在比較的時候,而另一次是在計算max()值的時候。這不僅是低效的,還會發(fā)生錯誤:biggest=x[0];i=1;while(i<n)biggest=max(biggest,x[i++]);當max()是ー個真正的函數(shù)時,這會正常地工作,但當max()是一個宏的時候會失敗。譬如,假設x[候是2、x⑴是3、x[2]是1。我們來看看在第一次循環(huán)時會發(fā)生什么。賦值語句會被擴展為:biggest=((biggest)>(x[i++])?(biggest):(x[i++]));首先,biggest與x[i++]進行比較。由于i是1而x[l]是3,這個關系是“假”。其副作用是,i增長到2。由于關系是"假”,x[i++]的值要賦給biggest。然而,這時的i變成2了,因此賦給biggest的值是x[2]的值,即1。避免這些問題的方法是保證max()宏的參數(shù)沒有副作用:biggest=x[0];for(i=1;i<n;i++)biggest=max(biggest,x[i]);還有一個危險的例子是混合宏及其副作用。這是來自UNIX第八版的くstdio.h>中putc()宏的定義:ttdefineputc(x,p)(一(p)->_cnt>=0?(*(p)->_ptr++=(x)):_fIsbuf(x,p))putc()的第一個參數(shù)是一個要寫入到文件中的字符,第二個參數(shù)是ー個指向ー個表示文件的內(nèi)部數(shù)據(jù)結構的指針。注意第一個參數(shù)完全可以使用如?Z++之類的東西,盡管它在宏中兩次出現(xiàn),但只會被求值ー次。而第二個參數(shù)會被求值兩次(在宏體中,X出現(xiàn)了兩次,但由于它的兩次出現(xiàn)分別在一個:的兩邊,因此在putc()的ー個實例中它們之中有且僅有一個被求值)。由于putc()中的文件參數(shù)可能帶有副作用,這偶爾會出現(xiàn)問題。不過,用戶手冊文檔中提到:“由于putc()被實現(xiàn)為宏,其對待stream可能會具有副作用。特別是putc(c,*f++)不能正確地工作?!钡莗utc(*c++,f)在這個實現(xiàn)中是可以工作的。有些C實現(xiàn)很不小心。例如,沒有人能正確處理putc(*c++,f)〇另ー個例子,考慮很多C庫中出現(xiàn)的toupper()函數(shù)。它將一個小寫字母轉(zhuǎn)換為相應的大寫字母,而其它字符不變。如果我們假設所有的小寫字母和所有的大寫字母都是相鄰的(大小寫之間可能有所差距),我們可以得到這樣的函數(shù):toupper(c){if(c>='a'&&cく='z')c+='A'-,a,;returnc;)在很多C實現(xiàn)中,為了減少比實際計算還要多的調(diào)用開銷,通常將其實現(xiàn)為宏:ftdefinetoupper(c)((c)>='a&&(c)く='z'?(c)+('A'-'a'):(c))很多時候這確實比函數(shù)要快。然而,當你試著寫toupper(*p++)時,會出現(xiàn)奇怪的結果。另ー個需要注意的地方是使用宏可能會產(chǎn)生巨大的表達式。例如,繼續(xù)考慮max()的定義:#definemax(a,b)((a)>(b)?(a):(b))假設我們這個定義來查找a、b、c和d中的最大值。如果我們直接寫:max(a,max(b,max(c,d)))它將被擴展為:((a)>(((b)>(((c)>(d)?(c):(d)))?(b):(((c)>(d)?(c):(d)))))?(a):(((b)>(((c)>(d)?(c):(d)))?(b):(((c)>(d)?(c):(d))))))這出奇的龐大。我們可以通過平衡操作數(shù)來使它短ー些:max(max(a,b),max(c,d))這會得到:((((a)>(b)?(a):(b)))>(((c)>(d)?(c):(d)))?(((a)>(b)?(a):(b))):(((c)>(d)?(c):(d))))這看起來還是寫:biggest=a;if(biggest<b)biggest=b;if(biggest<c)biggest=c;if(biggest<d)biggest=d;比較好一些。6.2宏不是類型定義宏的ー個通常的用途是保證不同地方的多個事物具有相同的類型:ttdefineFOOTYPEstructfooFOOTYPEa;FOOTYPEb,c;這允許程序員可以通過只改變程序中的一行就能改變a、b和c的類型,盡管a、b和c可能聲明在很遠的不同地方。使用這樣的宏定義還有著可移植性的優(yōu)勢ーー所有的C編譯器都支持它。很多C編譯器并不支持另ー種方法:typedefstructfooFOOTYPE;這將FOOTYPE定義為ー個與structfoo等價的新類型。這兩種為類型命名的方法可以是等價的,但typedef更靈活ー些。例如,考慮下面的例子:#defineT1structfoo*typedefstructfoo*T2;這兩個定義使得T!和T2都等價于ー個structfoo的指針。但看看當我們試圖在一行中聲明多于ー個變量的時候會發(fā)生什么:T1a,b;T2c,d;第一個聲明被擴展為:structfoo*a,b;這里a被定義為一個結構指針,但b被定義為ー個結構(而不是指針)。相反,第二個聲明中c和d都被定義為指向結構的指針,因為T2的行為好像真正的類型ー樣。7可移植性缺陷C被很多人實現(xiàn)并運行在很多機器上。這也正是在ー個地方寫的C程序應該能夠很容易地轉(zhuǎn)移到另一個編程環(huán)境中去的原因。然而,由于有很多的實現(xiàn)者,它們并不和其他人交流。此外,不同的系統(tǒng)有不同的需求,因此一臺機器上的c實現(xiàn)和另一臺上的多少會有些不同。由于很多早期的C實現(xiàn)都關系到UNIX操作系統(tǒng),因此這些函數(shù)的性質(zhì)都是專于該系統(tǒng)的。當ー些人開始在其他系統(tǒng)中實現(xiàn)C時,他們嘗試使庫的行為類似于UNIX系統(tǒng)中的行為。但他們并不總是能夠成功。更有甚者,很多人從UNIX系統(tǒng)的不同版本入手,ー些庫函數(shù)的本質(zhì)不可避免地發(fā)生分歧。今天,ー個C程序員如果想寫出對于不同環(huán)境中的用戶都有用的程序就必須知道很多這些細微的差別。1ー個名字中都有什么?ー些C編譯器將一個標識符中的所有字符視為簽名。而另ー些在存貯標識符是會忽略ー個極限之外的所有字符。C編譯器產(chǎn)生的目標程序同將要被加載器進行處理以訪問庫中的子程序。加載器對于它們能夠處理的名字通常應用自己的約束。ー個常見的加載器約束是所有的外部名字必須只能是大寫的。面對這樣的加載器約束,C實現(xiàn)者會強制要求所有的外部名字都是大寫的。這種約束在C語言參考手冊中第2.1節(jié)由所描述。ー個標識符是一個字符和數(shù)字序列,第一個字符必須是ー個字母。下劃線ー算作字母。大寫字母和小寫字母是不同的。只有前八個字符是簽名,但可以使用更多的字符。可以被多種匯編器和加載器使用的外部標識符,有著更多的限制:這里,參考手冊中繼續(xù)給出了一些例子如有些實現(xiàn)要求外部標識符具有單獨的大小寫格式、或者少于八個字符、或者二者都有。正因為所有這些,在ー個希望可以移植的程序中小心地選擇標識符是很重要的。為兩個子程序選擇print_fields和print_float這樣的名字不是個好辦法??紤]下面這個顯著的函數(shù):char*Malloc(unsignedn){char*p,*malloc();p=malloc(n);if(p==NULL)panic(,zoutofmemory);returnp;)這個函數(shù)是保證耗盡內(nèi)存而不會導致沒有檢測的ー個簡單的辦法。程序員可以通過調(diào)用Mallo()來代替malloc()〇如果malloc()不幸失敗,將調(diào)用panic。來顯示一個恰當?shù)腻e誤消息并終止程序。然而,考慮當該函數(shù)用于一個忽略大小寫區(qū)別的系統(tǒng)中時會發(fā)生什么。這時,名字malloc和Malloc是等價的。換句話說,庫函數(shù)malloc()被上面的MallocO函數(shù)完全取代了,當調(diào)用malloc。時它調(diào)用的是它自己。顯然,其結果就是第一次嘗試分配內(nèi)存就會陷入一個遞歸循環(huán)并隨之發(fā)生混亂。但在一些能夠區(qū)分大小寫的實現(xiàn)中這個函數(shù)還是可以工作的。ー個整數(shù)有多大?C為程序員提供三種整數(shù)尺寸:普通、短和長,還有字符,其行為像ー個很小的整數(shù)。C語言定義對各種整數(shù)的大小不作任何保證:整數(shù)的四種尺寸是非遞減的。普通整數(shù)的大小要足夠存放任意的數(shù)組下標。字符的大小應該體現(xiàn)特定硬件的本質(zhì)。許多現(xiàn)代機器具有8位字符,不過還有一些具有7位獲9位字符。因此字符通常是7、8或9位。長整數(shù)通常至少32位,因此一個長整數(shù)可以用于表示文件的大小。普通整數(shù)通常至少16位,因為太小的整數(shù)會更多地限制一個數(shù)組的最大大小。短整數(shù)總是恰好16位。在實踐中這些都意味著什么?最重要的一點就是別指望能夠使用任何ー個特定的精度。非正式情況下你可以假設一個短整數(shù)或ー個普通整數(shù)是16位的,而一個長整數(shù)是32位的,但并不保證總是會有這些大小。你當然可以用普通整數(shù)來壓縮表大小和下標,但當ー個變量必須存放ー個一千萬的數(shù)字的時候呢?ー種更可移植的做法是定義ー個“新的”類型:typedeflongtenmil;現(xiàn)在你就可以使用這個類型來聲明一個變量并知道它的寬度了,最壞的情況下,你也只要改變這個單獨的類型定義就可以使所有這些變量具有正確的類型。字符是帶符號的還是無符號的?很多現(xiàn)代計算機支持8位字符,因此很多現(xiàn)代C編譯器將字符實現(xiàn)為8位整數(shù)。然而,并不是所有的編譯器都按照同將的方式解釋這些8位數(shù)。這些問題在將一個char制轉(zhuǎn)換為ー個更大的整數(shù)時變得尤為重要。對于相反的轉(zhuǎn)換,其結果卻是定義良好的:多余的位被簡單地丟棄掉。但ー個編譯器將一個char轉(zhuǎn)換為ー個int卻需要作出選擇:將char視為帶符號量還是無符號量?如果是前者,將char擴展為int時要復制符號位:如果是后者,則要將多余的位用。填充。這個決定的結果對于那些在處理字符時習慣將高位置1的人來說非常重要。這決定著8位的字符范圍是從ー128到127還是從〇到255〇這又影響著程序員對哈希表和轉(zhuǎn)換表之類的東西的設計。如果你關心一個字符值最高位置ー時是否被視為ー個負數(shù),你應該顯式地將它聲明為unsignedchar〇這樣就能保證在轉(zhuǎn)換為整數(shù)時是基0的,而不像普通char變量那樣在ー些實現(xiàn)中是帶符號的而在另ー些實現(xiàn)中是無符號的。另外,還有一種誤解是認為當c是一個字符變量時,可以通過寫(unsigned)c來得到與c等價的無符號整數(shù)。這是錯誤的,因為ー個char值在進行任何操作(包括轉(zhuǎn)換)之前轉(zhuǎn)換為into這時c會首先轉(zhuǎn)換為一個帶符號整數(shù)在轉(zhuǎn)換為ー個無符號整數(shù),這會產(chǎn)生奇怪的結果。正確的方法是寫(unsignedchar)c〇右移位是帶符號的還是無符號的?這里再一次重復:ー個關心右移操作如何進行的程序最好將所有待移位的量聲明為無符號的。除法如何舍入?假設我們用b除a得到商為q余數(shù)為r:q=a/b;r=a%b;我們暫時假設b>〇〇我們期望a、b、q和r之間有什么關聯(lián)?最重要的,我們期望q*b+r==a,因為這是對余數(shù)的定義。如果a的符號發(fā)生改變,我們期望q的符號也發(fā)生改變,但絕對值不變。我們希望保證r>=0且rくb。例如,如果余數(shù)將作為ー個哈希表的索引,它必須要保證總是ー個有效的索引。這三點清楚地描述了整數(shù)除法和求余操作。不幸的是,它們不能同時為真??紤]3/2,商1余0。這滿足第一點。而ー3/2的值呢?根據(jù)第二點,商應該是T,但如果是這樣的話,余數(shù)必須也是ー1,這違反了第三點?;蛘?我們可以通過將余數(shù)標記為1來滿足第三點,但這時根據(jù)第一點商應該是ー2。這又違反了第二點。因此C和其他任何實現(xiàn)了整數(shù)除法舍入的語言必須放棄上述三個原則中的至少ー個。很多程序設計語言放棄了第三點,要求余數(shù)的符號必須和被除數(shù)相同。這可以保證第一點和第二點。很多C實現(xiàn)也是這樣做的。然而,C語言的定義只保證了第一點和|r|く|b|以及當a>=0且b>0時r>=0。這比第二點或第三點的限制要小,實際上有些編譯器滿足第二點或第三點,但不太常見(如一個實現(xiàn)可能總是向著距離O最遠的方向進行舍入)。盡管有些時候不需要靈活性,C語言還是足夠可以讓我們令除法完成我們所要做的、提供我們所想知道的。例如,假設我們有一個數(shù)n表示ー個標識符中的字符的一些函數(shù),并且我們想通過除法得到ー個哈希表入口h,其中〇く=h希HASHSIZEo如果我們知道n是非負的,我們可以簡單地寫:h=n%HASHSIZE;然而,如果n有可能是負的,這樣寫就不好了,因為h可能也是負的。然而,我們知道h>-HASHSIZE,因此我們可以寫:h-n%HASHSIZE;if(n<0)h+=HASHSIZE;同樣,將n聲明為unsigned也可以。ー個隨機數(shù)有多大?這個尺寸是模糊的,還受庫設計的影響。在PDP-11[10]機器上運行的僅有的C實現(xiàn)中,有一個稱為rand()的函數(shù)可以返回一個(偽)隨機非負整數(shù)。PDP-11中整數(shù)長度包括符號位是16位,因此rand()返回一個〇到215-1之間的整數(shù)。當C在VAX-11上實現(xiàn)時,整數(shù)的長度變?yōu)?2位長。那么VAX-"上的randO函數(shù)返回值范圍是什么呢?對于這個系統(tǒng),加利福尼亞大學的人認為rand()的返回值應該涵蓋所有可能的非負整數(shù),因此它們的rand()版本返回一個0到231-1之間的整數(shù)。而AT&T的人則覺得如果rand()函數(shù)仍然返回一個0到215之間的值則可以很容易地將PDP-11中期望rand()能夠返回一個小于215的值的程序移植到VAX-11±o因此,現(xiàn)在還很難寫出不依賴實現(xiàn)而調(diào)用rand()函數(shù)的程序。大小寫轉(zhuǎn)換toupper()和tolower()函數(shù)有著類似的歷史。他們最初都被實現(xiàn)為宏:#definetoupper(c)((c)+'A'-'a')#definetolower(c)((c)+'A'-'a')當給定一個小寫字母作為輸入時,toupper()將產(chǎn)生相應的大寫字母。tolower()反之。這兩個宏都依賴于實現(xiàn)的字符集,它們需要所有的大寫字母和對應的小寫字母之間的差別都是常數(shù)的。這個假設對于ASC口和EBCDIC字符集來說都是有效的,可能不是很危險,因為這些不可移植的宏定義可以被封裝到ー個單獨的文件中并包含它們。這些宏確實有一個缺陷,即:當給定的東西不是一個恰當?shù)淖址?它會返回垃圾。因此,下面這個通過使用這些宏來將一個文件轉(zhuǎn)為小寫的程序是無法工作的:intc;while((c=getchar())!=EOF)putchar(tolower(c));我們必須寫:intc;while((c=getchar())!=EOF)putchar(isupper(c)?tolower(c):c);就這一點,AT&T中的UNIX開發(fā)組織提醒我們,toupper()和tolower()都是事先經(jīng)過ー些適當?shù)膮?shù)進行測試的??紤]這樣重寫這些宏:#definetoupper(c)((c)>='a&&(c)く='z'?(c)+'A'-'a':(c))#definetolower(c)((c)>='A'&&(c)0'Z'?(c)+'a'-'A':(c))但要知道,這里c的三次出現(xiàn)都要被求值,這會破壞如toupper(*p++)這樣的表達式。因此,可以考慮將toupper()和tolower()重寫為函數(shù)。toupper()看起來可能像這樣:inttoupper(intc){if(c>='a'&&cく='z')returnc+'A'-'a';returnc;)tolower()類似。這個改變帶來更多的問題,每次使用這些函數(shù)的時候都會引入函數(shù)調(diào)用開銷。我們的英雄認為ー些人可能不愿意支付這些開銷,因此他們將這個宏重命名為:#definetoupper(c)((c)+'A'-'a')#definetolower(c)((c)+'a'-'A')這就允許用戶選擇方便或速度。這里面其實只有一個問題:伯克利的人們和其他的C實現(xiàn)者并沒有跟著這么做。這意味著ー個在AT&T系統(tǒng)上編寫的使用了toupper?;騮olower()的程序,如果沒有為其傳遞正確大小寫字母參數(shù),在其他C實現(xiàn)中可能不會正常工作。如果不知道這些歷史,可能很難對這類錯誤進行跟蹤。先釋放,再重新分配很多C實現(xiàn)為用戶提供了三個內(nèi)存分配函數(shù):malloc()>realloc()和free()〇調(diào)用malloc(n)返回ー個指向有n個字符的新分配的內(nèi)存的指針,這個指針可以由程序員使用。給free。傳遞ー個指向由malloc()分配的內(nèi)存的指針可以使這塊內(nèi)存得以重用。通過ー個指向已分配區(qū)域的指針和一個新的大小調(diào)用realloc()可以將這塊內(nèi)存擴大或縮小到新尺寸,這個過程中可能要復制內(nèi)存。也許有人會想,真相真是有點微妙啊。下面是SystemV接口定義中出現(xiàn)的對realloc()的描述:realloc改變ー個由ptr指向的size個字節(jié)的塊,并返回該塊(可能被移動)的指針。在新舊尺寸中比較小的ー個尺寸之下的內(nèi)容不會被改變。而UNIX系統(tǒng)第七版的參考手冊中包含了這一段的副本。此外,還包含了描述realloc()的另外一段:如果在最后一次調(diào)用malloc、realloc或calloc后釋放了ptr所指向的塊,realloc依舊可以工作;因此,free^malloc和realloc的順序可以利用malloc壓縮存貯的查找策略。因此,下面的代碼片段在UNIX第七版中是合法的:free(p);p=realloc(p,newsize);這ー特性保留在從UNIX第七版衍生出來的系統(tǒng)中:可以先釋放ー塊存儲區(qū)域,然后再重新分配它。這意味著,在這些系統(tǒng)中釋放的內(nèi)存中的內(nèi)容在下一次內(nèi)存分配之前可以保證不變。因此,在這些系統(tǒng)中,我們可以用下面這種奇特的思想來釋放ー個鏈表中的所有元素:for(p=head;p!=NULL;p=p->next)

free((char*)p);而不用擔心調(diào)用free。會導致p->next不可用。不用說,這種技術是不推薦的,因

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
  • 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論