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

下載本文檔

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

文檔簡介

C語言陷阱和缺陷1原著:Andrew Koenig - AT&T Bell Laboratories Murray Hill, New Jersey 07094原文:收藏翻譯:lover_P出處:本站譯序 那些自認為已經(jīng)“學完”C語言的人,請你們仔細讀閱讀這篇文章吧。路還長,很多東西要學。我也是概述 C語言像一把雕刻刀,鋒利,并且在技師手中非常有用。和任何鋒利的工具一樣,C會傷到那些不能掌握它的人。本文介紹C語言傷害粗心的人的方法,以及如何避免傷害。內(nèi)容 0 簡介 1 詞法缺陷o 1.1 = 不是 =o 1.2 & 和 | 不是 & 和 |o 1.3 多字符記號o 1.4 例外o 1.5 字符串和字符 2 句法缺陷o 2.1 理解聲明o 2.2 運算符并不總是具有你所想象的優(yōu)先級o 2.3 看看這些分號!o 2.4 switch語句o 2.5 函數(shù)調用o 2.6 懸掛else問題 3 鏈接o 3.1 你必須自己檢查外部類型 4 語義缺陷o 4.1 表達式求值順序o 4.2 &、|和!運算符o 4.3 下標從零開始o 4.4 C并不總是轉換實參o 4.5 指針不是數(shù)組o 4.6 避免提喻法o 4.7 空指針不是空字符串o 4.8 整數(shù)溢出o 4.9 移位運算符 5 庫函數(shù)o 5.1 getc()返回整數(shù)o 5.2 緩沖輸出和內(nèi)存分配 6 預處理器o 6.1 宏不是函數(shù)o 6.2 宏不是類型定義 7 可移植性缺陷o 7.1 一個名字中都有什么?o 7.2 一個整數(shù)有多大?o 7.3 字符是帶符號的還是無符號的?o 7.4 右移位是帶符號的還是無符號的?o 7.5 除法如何舍入?o 7.6 一個隨機數(shù)有多大?o 7.7 大小寫轉換o 7.8 先釋放,再重新分配o 7.9 可移植性問題的一個實例 8 這里是空閑空間 參考 腳注0 簡介 C語言及其典型實現(xiàn)被設計為能被專家們?nèi)菀椎厥褂?。這門語言簡潔并附有表達力。但有一些限制可以保護那些浮躁的人。一個浮躁的人可以從這些條款中獲得一些幫助。 在本文中,我們將會看一看這些未可知的益處。這是由于它的未可知,我們無法為其進行完全的分類。不過,我們?nèi)匀煌ㄟ^研究為了一個C程序的運行所需要做的事來做到這些。我們假設讀者對C語言至少有個粗淺的了解。 第一部分研究了當程序被劃分為記號時會發(fā)生的問題。第二部分繼續(xù)研究了當程序的記號被編譯器組合為聲明、表達式和語句時會出現(xiàn)的問題。第三部分研究了由多個部分組成、分別編譯并綁定到一起的C程序。第四部分處理了概念上的誤解:當一個程序具體執(zhí)行時會發(fā)生的事情。第五部分研究了我們的程序和它們所使用的常用庫之間的關系。在第六部分中,我們注意到了我們所寫的程序也不并不是我們所運行的程序;預處理器將首先運行。最后,第七部分討論了可移植性問題:一個能在一個實現(xiàn)中運行的程序無法在另一個實現(xiàn)中運行的原因。1 詞法缺陷 編譯器的第一個部分常被稱為詞法分析器(lexical analyzer)。詞法分析器檢查組成程序的字符序列,并將它們劃分為記號(token)一個記號是一個有一個或多個字符的序列,它在語言被編譯時具有一個(相關地)統(tǒng)一的意義。在C中, 例如,記號-的意義和組成它的每個獨立的字符具有明顯的區(qū)別,而且其意義獨立于-出現(xiàn)的上下文環(huán)境。 另外一個例子,考慮下面的語句:if(x big) big = x;該語句中的每一個分離的字符都被劃分為一個記號,除了關鍵字if和標識符big的兩個實例。 事實上,C程序被兩次劃分為記號。首先是預處理器讀取程序。它必須對程序進行記號劃分以發(fā)現(xiàn)標識宏的標識符。它必須通過對每個宏進行求值來替換宏調用。最后,經(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 | c = n) c = getc(f);在與t進行比較的地方程序員錯誤地使用=代替了=。這個“比較”實際上是將t賦給c,然后判斷c的(新的)值是否為零。因為t不為零,這個“比較”將一直為真,因此這個循環(huán)會吃盡整個文件。這之后會發(fā)生什么取決于特定的實現(xiàn)是否允許一個程序讀取超過文件尾部的部分。如果允許,這個循環(huán)會一直運行。 一些C編譯器會對形如e1 = e2的條件給出一個警告以提醒用戶。當你趨勢需要先對一個變量進行賦值之后再檢查變量是否非零時,為了在這種編譯器中避免警告信息,應考慮顯式給出比較符。換句話說,將:if(x = y) foo();改寫為:if(x = y) != 0) foo();這樣可以清晰地表示你的意圖。1.2 & 和 | 不是 & 和 | 容易將=錯寫為=是因為很多其他語言使用=表示比較運算。 其他容易寫錯的運算符還有&和&,或|和|,這主要是因為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=-1;視為a =- 1;或a = a - 1;這會讓打算寫a = -1;的程序員感到吃驚。 另一方面,這種老版本的C編譯器會將a=/*b;斷句為a =/ *b;盡管/*看起來像一個注釋。1.4 例外 組合賦值運算符如+=實際上是兩個記號。因此,a + /* strange */ = 1和a += 1是一個意思??雌饋硐褚粋€單獨的記號而實際上是多個記號的只有這一個特例。特別地,p - a是不合法的。它和p - a不是同義詞。 另一方面,有些老式編譯器還是將=+視為一個單獨的記號并且和+=是同義詞。1.5 字符串和字符 單引號和雙引號在C中的意義完全不同,在一些混亂的上下文中它們會導致奇怪的結果而不是錯誤消息。 包圍在單引號中的一個字符只是書寫整數(shù)的另一種方法。這個整數(shù)是給定的字符在實現(xiàn)的對照序列中的一個對應的值。因此,在一個ASCII實現(xiàn)中,a和0141或97表示完全相同的東西。而一個包圍在雙引號中的字符串,只是書寫一個有雙引號之間的字符和一個附加的二進制值為零的字符所初始化的一個無名數(shù)組的指針的一種簡短方法。 線面的兩個程序片斷是等價的:printf(Hello worldn);char hello = H, e, l, l, o, , w, o, r, l, 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é)中,我們將著眼于一些不明顯句法構造。2.1 理解聲明 我曾經(jīng)和一些人聊過天,他們那時在書寫在一個小型的微處理器上單機運行的C程序。當這臺機器的開關打開的時候,硬件會調用地址為0處的子程序。 為了模仿電源打開的情形,我們要設計一條C語句來顯式地調用這個子程序。經(jīng)過一些思考,我們寫出了下面的語句:(*(void(*)()0)(); 這樣的表達式會令C程序員心驚膽戰(zhàn)。但是,并不需要這樣,因為他們可以在一個簡單的規(guī)則的幫助下很容易地構造它:以你使用的方式聲明它。 每個C變量聲明都具有兩個部分:一個類型和一組具有特定格式的期望用來對該類型求值的表達式。最簡單的表達式就是一個變量:float f, g;說明表達式f和g在求值的時候具有類型float。由于待求值的時表達式,因此可以自由地使用圓括號:float (f);者表示(f)求值為float并且因此,通過推斷,f也是一個float。 同樣的邏輯用在函數(shù)和指針類型。例如:float ff();表示表達式ff()是一個float,因此ff是一個返回一個float的函數(shù)。類似地,float *pf;表示*pf是一個float并且因此pf是一個指向一個float的指針。 這些形式的組合聲明對表達式是一樣的。因此,float *g(), (*h)();表示*g()和(*h)()都是float表達式。由于()比*綁定得更緊密,*g()和*(g()表示同樣的東西:g是一個返回指float指針的函數(shù),而h是一個指向返回float的函數(shù)的指針。 當我們知道如何聲明一個給定類型的變量以后,就能夠很容易地寫出一個類型的模型(cast):只要刪除變量名和分號并將所有的東西包圍在一對圓括號中即可。因此,由于float *g();聲明g是一個返回float指針的函數(shù),所以(float *()就是它的模型。 有了這些知識的武裝,我們現(xiàn)在可以準備解決(*(void(*)()0)()了。 我們可以將它分為兩個部分進行分析。首先,假設我們有一個變量fp,它包含了一個函數(shù)指針,并且我們希望調用fp所指向的函數(shù)??梢赃@樣寫:(*fp)();如果fp是一個指向函數(shù)的指針,則*fp就是函數(shù)本身,因此(*fp)()是調用它的一種方法。(*fp)中的括號是必須的,否則這個表達式將會被分析為*(fp()。我們現(xiàn)在要找一個適當?shù)谋磉_式來替換fp。 這個問題就是我們的第二步分析。如果C可以讀入并理解類型,我們可以寫:(*0)();但這樣并不行,因為*運算符要求必須有一個指針作為他的操作數(shù)。另外,這個操作數(shù)必須是一個指向函數(shù)的指針,以保證*的結果可以被調用。因此,我們需要將0轉換為一個可以描述“指向一個返回void的函數(shù)的指針”的類型。 如果fp是一個指向返回void的函數(shù)的指針,則(*fp)()是一個void值,并且它的聲明將會是這樣的:void (*fp)();因此,我們需要寫:void (*fp)();(*fp)();來聲明一個啞變量。一旦我們知道了如何聲明該變量,我們也就知道了如何將一個常數(shù)轉換為該類型:只要從變量的聲明中去掉名字即可。因此,我們像下面這樣將0轉換為一個“指向返回void的函數(shù)的指針”:(void(*)()0接下來,我們用(void(*)()0來替換fp:(*(void(*)()0)();結尾處的分號用于將這個表達式轉換為一個語句。 在這里,我們就解決了這個問題時沒有使用typedef聲明。通過使用它,我們可以更清晰地解決這個問題:typedef void (*funcptr)();(*(funcptr)0)();2.2 運算符并不總是具有你所想象的優(yōu)先級 假設有一個聲明了的常量FLAG是一個整數(shù),其二進制表示中的某一位被置位(換句話說,它是2的某次冪),并且你希望測試一個整型變量flags該位是否被置位。通常的寫法是:if(flags & FLAG) .其意義對于很多C程序員都是很明確的:if語句測試括號中的表達式求值的結果是否為0。出于清晰的目的我們可以將它寫得更明確:if(flags & FLAG != 0) .這個語句現(xiàn)在更容易理解了。但它仍然是錯的,因為!=比&綁定得更緊密,因此它被分析為:if(flags & (FLAG != 0) .這(偶爾)是可以的,如FLAG是1或0(!)的時候,但對于其他2的冪是不行的2。 假設你有兩個整型變量,h和l,它們的值在0和15(含0和15)之間,并且你希望將r設置為8位值,其低位為l,高位為h。一種自然的寫法是:r = h 4 + 1;不幸的是,這是錯誤的。加法比移位綁定得更緊密,因此這個例子等價于:r = h (4 + l);正確的方法有兩種:r = (h 4) + l;r = h 4 | l; 避免這種問題的一個方法是將所有的東西都用括號括起來,但表達式中的括號過度就會難以理解,因此最好還是是記住C中的優(yōu)先級。 不幸的是,這有15個,太困難了。然而,通過將它們分組可以變得容易。 綁定得最緊密的運算符并不是真正的運算符:下標、函數(shù)調用和結構選擇。這些都與左邊相關聯(lián)。 接下來是一元運算符。它們具有真正的運算符中的最高優(yōu)先級。由于函數(shù)調用比一元運算符綁定得更緊密,你必須寫(*p)()來調用p指向的函數(shù);*p()表示p是一個返回一個指針的函數(shù)。轉換是一元運算符,并且和其他一元運算符具有相同的優(yōu)先級。一元運算符是右結合的,因此*p+表示*(p+),而不是(*p)+。 在接下來是真正的二元運算符。其中數(shù)學運算符具有最高的優(yōu)先級,然后是移位運算符、關系運算符、邏輯運算符、賦值運算符,最后是條件運算符。需要記住的兩個重要的東西是:1. 所有的邏輯運算符具有比所有關系運算符都低的優(yōu)先級。2. 一位運算符比關系運算符綁定得更緊密,但又不如數(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 aty) = STRTY) | t = UNIONTY) 這條語句希望給t賦一個值,然后看t是否與STRTY或UNIONTY相等。而實際的效果卻大不相同3。 C中的邏輯運算符的優(yōu)先級具有歷史原因。BC的前輩具有和C中的&和|運算符對應的邏輯運算符。盡管它們的定義是按位的 ,但編譯器在條件判斷上下文中將它們視為和&和|一樣。當在C中將它們分開后,優(yōu)先級的改變是很危險的4。2.3 看看這些分號! C中的一個多余的分號通常會帶來一點點不同:或者是一個空語句,無任何效果;或者編譯器可能提出一個診斷消息,可以方便除去掉它。一個重要的區(qū)別是在必須跟有一個語句的if和while語句中??紤]下面的例子:if(xi big); big = xi;這不會發(fā)生編譯錯誤,但這段程序的意義與:if(xi big) big = xi;就大不相同了。第一個程序段等價于:if(xi big) big = xi;也就是等價于:big = xi;(除非x、i或big是帶有副作用的宏)。 另一個因分號引起巨大不同的地方是函數(shù)定義前面的結構聲明的末尾譯注:這句話不太好聽,看例子就明白了??紤]下面的程序片段:struct foo int x;f() .在緊挨著f的第一個后面丟失了一個分號。它的效果是聲明了一個函數(shù)f,返回值類型是struct foo,這個結構成了函數(shù)聲明的一部分。如果這里出現(xiàn)了分號,則f將被定義為具有默認的整型返回值5。2.4 switch語句 通常C中的switch語句中的case段可以進入下一個。例如,考慮下面的C和Pascal程序片斷:switch(color) case 1: printf (red); break;case 2: printf (yellow); break;case 3: printf (blue); break;case color of1: write (red);2: write (yellow);3: write (blue);end 這兩個程序片斷都作相同的事情:根據(jù)變量color的值是1、2還是3打印red、yellow或blue(沒有新行符)。這兩個程序片斷非常相似,只有一點不同:Pascal程序中沒有C中相應的break語句。C中的case標簽是真正的標簽:控制流程可以無限制地進入到一個case標簽中。 看看另一種形式,假設C程序段看起來更像Pascal:switch(color) case 1: printf (red);case 2: printf (yellow);case 3: printf (blue);并且假設color的值是2。則該程序將打印yellowblue,因為控制自然地轉入到下一個printf()的調用。 這既是C語言switch語句的優(yōu)點又是它的弱點。說它是弱點,是因為很容易忘記一個break語句,從而導致程序出現(xiàn)隱晦的異常行為。說它是優(yōu)點,是因為通過故意去掉break語句,可以很容易實現(xiàn)其他方法難以實現(xiàn)的控制結構。尤其是在一個大型的switch語句中,我們經(jīng)常發(fā)現(xiàn)對一個case的處理可以簡化其他一些特殊的處理。 例如,設想有一個程序是一臺假想的機器的翻譯器。這樣的一個程序可能包含一個switch語句來處理各種操作碼。在這樣一臺機器上,通常減法在對其第二個運算數(shù)進行變號后就變成和加法一樣了。因此,最好可以寫出這樣的語句:case SUBTRACT: opnd2 = -opnd2; /* no break; */case ADD: . 另外一個例子,考慮編譯器通過跳過空白字符來查找一個記號。這里,我們將空格、制表符和新行符視為是相同的,除了新行符還要引起行計數(shù)器的增長外:case n: linecount+; /* no break */case t:case : .2.5 函數(shù)調用 和其他程序設計語言不同,C要求一個函數(shù)調用必須有一個參數(shù)列表,但可以沒有參數(shù)。因此,如果f是一個函數(shù),f();就是對該函數(shù)進行調用的語句,而f;什么也不做。它會作為函數(shù)地址被求值,但不會調用它6。2.6 懸掛else問題 在討論任何語法缺陷時我們都不會忘記提到這個問題。盡管這一問題不是C語言所獨有的,但它仍然傷害著那些有著多年經(jīng)驗的C程序員。 考慮下面的程序片斷:if(x = 0) if(y = 0) error();else z = x + y; f(&z); 寫這段程序的程序員的目的明顯是將情況分為兩種:x = 0和x != 0。在第一種情況中,程序段什么都不做,除非y = 0時調用error()。第二種情況中,程序設置z = x + y并以z的地址作為參數(shù)調用f()。 然而, 這段程序的實際效果卻大為不同。其原因是一個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的程序來捕獲這些錯誤。如果具有一個這樣的程序,那么無論怎樣地強調它的重要性都不過分。3.1 你必須自己檢查外部類型 假設你有一個C程序,被劃分為兩個文件。其中一個包含如下聲明:int n;而令一個包含如下聲明:long n;這不是一個有效的C程序,因為一些外部名稱在兩個文件中被聲明為不同的類型。然而,很多實現(xiàn)檢測不到這個錯誤,因為編譯器在編譯其中一個文件時并不知道另一個文件的內(nèi)容。因此,檢查類型的工作只能由鏈接器(或一些工具程序如lint)來完成;如果操作系統(tǒng)的鏈接器不能識別數(shù)據(jù)類型,C編譯器也沒法過多地強制它。 那么,這個程序運行時實際會發(fā)生什么?這有很多可能性:1. 實現(xiàn)足夠聰明,能夠檢測到類型沖突。則我們會得到一個診斷消息,說明n在兩個文件中具有不同的類型。2. 你所使用的實現(xiàn)將int和long視為相同的類型。典型的情況是機器可以自然地進行32位運算。在這種情況下你的程序或許能夠工作,好象你兩次都將變量聲明為long(或int)。但這種程序的工作純屬偶然。3. n的兩個實例需要不同的存儲,它們以某種方式共享存儲區(qū),即對其中一個的賦值對另一個也有效。這可能發(fā)生,例如,編譯器可以將int安排在long的低位。不論這是基于系統(tǒng)的還是基于機器的,這種程序的運行同樣是偶然。4. n的兩個實例以另一種方式共享存儲區(qū),即對其中一個賦值的效果是對另一個賦以不同的值。在這種情況下,程序可能失敗。 這種情況發(fā)生的里一個例子出奇地頻繁。程序的某一個文件包含下面的聲明:char filename = etc/passwd;而另一個文件包含這樣的聲明:char *filename; 盡管在某些環(huán)境中數(shù)組和指針的行為非常相似,但它們是不同的。在第一個聲明中,filename是一個字符數(shù)組的名字。盡管使用數(shù)組的名字可以產(chǎn)生數(shù)組第一個元素的指針,但這個指針只有在需要的時候才產(chǎn)生并且不會持續(xù)。在第二個聲明中,filename是一個指針的名字。這個指針可以指向程序員讓它指向的任何地方。如果程序員沒有給它賦一個值,它將具有一個默認的0值(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運算符以一種已知的、特定的順序對其操作數(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) yi = xi+;其中的問題是yi的地址并不保證在i增長之前被求值。在某些實現(xiàn)中,這是可能的;但在另一些實現(xiàn)中卻不可能。另一種情況出于同樣的原因會失?。篿 = 0;while(i n) yi+ = xi;而下面的代碼是可以工作的:i = 0;while(i n) yi = xi; i+;當然,這可以簡寫為:for(i = 0; i n; i+) yi = xi;4.2 &、|和!運算符 C中有兩種邏輯運算符,在某些情況下是可以交換的:按位運算符&、|和,以及邏輯運算符&、|和!。一個程序員如果用某一類運算符替換相應的另一類運算符會得到某些奇怪的效果:程序可能會正確地工作,但這純屬偶然。 &、|和!運算符將它們的參數(shù)視為僅有“真”或“假”,通常約定0代表“假”而其它的任意值都代表“真”。這些運算符返回1表示“真”而返回0表示“假”,而且&和|運算符當可以通過左邊的操作數(shù)確定其返回值時,就不會對右邊的操作數(shù)進行求值。 因此!10是零,因為10非零;10 & 12是1,因為10和12都非零;10 | 12也是1,因為10非零。另外,最后一個表達式中的12不會被求值,10 | f()中的f()也不會被求值。 考慮下面這段用于在一個表中查找一個特定元素的程序:i = 0;while(i tabsize & tabi != x) i+;這段循環(huán)背后的意思是如果i等于tabsize時循環(huán)結束,元素未被找到。否則,i包含了元素的索引。 假設這個例子中的&不小心被替換為了&,這個循環(huán)可能仍然能夠工作,但只有兩種幸運的情況可以使它停下來。 首先,這兩個操作都是當條件為假時返回0,當條件為真時返回1。只要x和y都是1或0,x & y和x & y都具有相同的值。然而,如果當使用了出了1之外的非零值表示“真”時互換了這兩個運算符,這個循環(huán)將不會工作。 其次,由于數(shù)組元素不會改變,因此越過數(shù)組最后一個元素進一個位置時是無害的,循環(huán)會幸運地停下來。失誤的程序會越過數(shù)組的結尾,因為&不像&,總是會對所有的操作數(shù)進行求值。因此循環(huán)的最后一次獲取tabi時i的值已經(jīng)等于tabsize了。如果tabsize是tab中元素的數(shù)量, 則會取到tab中不存在的一個值。4.3 下標從零開始 在很多語言中,具有n個元素的數(shù)組其元素的號碼和它的下標是從1到n嚴格對應的。但在C中不是這樣。 一個具有n個元素的C數(shù)組中沒有下標為n的元素,其中的元素的下標是從0到n - 1。因此從其它語言轉到C語言的程序員應該特別小心地使用數(shù)組:int i, a10;for(i = 1; i = 10; i+) ai = 0;這個例子的目的是要將a中的每個元素都設置為0,但沒有期望的效果。因為for語句中的比較i 10被替換成了i = 10,a中的一個編號為10的并不存在的元素被設置為了0,這樣內(nèi)存中a后面的一個字被破壞了。如果編譯該程序的編譯器按照降序地址為用戶變量分配內(nèi)存,則a后面就是i。將i設置為零會導致該循環(huán)陷入一個無限循環(huán)。4.4 C并不總是轉換實參 下面的程序段由于兩個原因會失敗:double s;s = sqrt(2);printf(%gn, s); 第一個原因是sqrt()需要一個double值作為它的參數(shù),但沒有得到。第二個原因是它返回一個double值但沒有這樣聲名。改正的方法只有一個:double s, sqrt();s = sqrt(2.0);printf(%gn, s); C中有兩個簡單的規(guī)則控制著函數(shù)參數(shù)的轉換:(1)比int短的整型被轉換為int;(2)比double短的浮點類型被轉換為double。所有的其它值不被轉換。確保函數(shù)參數(shù)類型的正確行使程序員的責任。 因此,一個程序員如果想使用如sqrt()這樣接受一個double類型參數(shù)的函數(shù),就必須僅傳遞給它float或double類型的參數(shù)。常數(shù)2是一個int,因此其類型是錯誤的。 當一個函數(shù)的值被用在表達式中時,其值會被自動地轉換為適當?shù)念愋?。然而,為了完成這個自動轉換,編譯器必須知道該函數(shù)實際返回的類型。沒有更進一步聲名的函數(shù)被假設返回int,因此聲名這樣的函數(shù)并不是必須的。然而,sqrt()返回double,因此在成功使用它之前必須要聲名。 實際上,C實現(xiàn)通常允許一個文件包含include語句來包含如sqrt()這些庫函數(shù)的聲名,但是對那些自己寫函數(shù)的程序員來說,書寫聲名也是必要的或者說,對那些書寫非凡的C程序的人來說是有必要的。 這里有一個更加壯觀的例子:main() int i; char c; for(i = 0; i 5; i+) scanf(%d, &c); printf(%d, i); printf(n); 表面上看,這個程序從標準輸入中讀取五個整數(shù)并向標準輸出寫入0 1 2 3 4。實際上,它并不總是這么做。譬如在一些編譯器中,它的輸出為0 0 0 0 0 1 2 3 4。 為什么?因為c的聲名是char而不是int。當你令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程序通常將一個字符串轉換為一個以空字符結尾的字符數(shù)組。假設我們有兩個這樣的字符串s和t,并且我們想要將它們連接為一個單獨的字符串r。我們通常使用庫函數(shù)strcpy()和strcat()來完成。下面這種明顯的方法并不會工作:char *r;strcpy(r, s);strcat(r, t);這是因為r沒有被 初始化為指向任何地方。盡管r可能潛在地表示某一塊內(nèi)存,但這并不存在,直到你分配它。 讓我們再試試,為r分配一些內(nèi)存:char r100;strcpy(r, s);strcat(r, t);這只有在s和t所指向的字符串不很大的時候才能夠工作。不幸的是,C要求我們?yōu)閿?shù)組指定的大小是一個常數(shù),因此無法確定r是否足夠大。然而,很多C實現(xiàn)帶有一個叫做malloc()的庫函數(shù),它接受一個數(shù)字并分配這么多的內(nèi)存。通常還有一個函數(shù)成為strlen(),可以告訴我們一個字符串中有多少個字符:因此,我們可以寫:char *r, *malloc();r = malloc(strlen(s) + strlen(t);strcpy(r, s);strcat(r, t); 然而這個例子會因為兩個原因而失敗。首先,malloc()可能會耗盡內(nèi)存,而這個事件僅通過靜靜地返回一個空指針來表示。 其次,更重要的是,malloc()并沒有分配足夠的內(nèi)存。一個字符串是以一個空字符結束的。而strlen()函數(shù)返回其字符串參數(shù) 中所包含字符的數(shù)量,但不包括結尾的空字符。因此,如果strlen(s)是n,則s需要n + 1個字符來盛放它。因此我們需要為r分配額外的一個字符。再加上檢查malloc()是否成功,我們得到:char *r, *malloc();r = malloc(strlen(s) + strlen(t) + 1);if(!r) complain(); exit(1);strcpy(r, s);strcat(r, t);4.6 避免提喻法 提喻法(Synecdoche, sin-ECK-duh-key)是一種文學手法,有點類似于明喻或暗喻,在牛津英文詞典中解釋如下:“a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.(將全面的單位用作不全面的單位,或反之;如整體對局部或局部對整體、一般對特殊或特殊對一般,等等。)” 這可以精確地描述C中通常將指針誤以為是其指向的數(shù)據(jù)的錯誤。正將常會在字符串中發(fā)生。例如:char *p, *q;p = xyz;盡管認為p的值是xyz有時是有用的,但這并不是真的,理解這一點非常重要。p的值是指向一個有四個字符的數(shù)組中第0個元素的指針,這四個字符是x、y、z和0。因此,如果我們現(xiàn)在執(zhí)行:q = p;p和q會指向同一塊內(nèi)存。內(nèi)存中的字符沒有因為賦值而被復制。這種情況看起來是這樣的: 要記住的是,復制一個指針并不能復制它所指向的東西。 因此,如果之后我們執(zhí)行:q1 = Y;q所指向的內(nèi)存包含字符串xYz。p也是,因為p和q指向相同的內(nèi)存。4.7 空指針不是空字符串 將一個整數(shù)轉換為一個指針的結果是實現(xiàn)相關的(implementation-dependent),除了一個例外。這個例外是常數(shù)0,它可以保證被轉換為一個與其它任何有效指針都不相等的指針。這個值通常類似這樣定義:#define NULL 0但其效果是相同的。要記住的一個重要的事情是,當用0作為指針時它決不能被解除引用。換句話說,當你將0賦給一個指針變量后,你就不能訪問它所指向的內(nèi)存。不能這樣寫:if(p = (char *)0) .也不能這樣寫:if(strcmp(p, (char *)0) = 0) .因為strcmp()總是通過其參數(shù)來查看內(nèi)存地址的。 如果p是一個空指針,這樣寫也是無效的:printf(p);或printf(%s, p);4.8 整數(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),這個測試會失敗。 使這個特殊的測試能夠成功的一個正確的方法是依賴于無符號算術的良好定義,既要在有符號和無符號之間進行轉換:if(int)(unsigned)a + (unsigned)b) 0) complain();4.9 移位運算符 兩個原因會令使用移位運算符的人感到煩惱:1. 在右移運算中,空出的位是用0填充還是用符號位填充?2. 移位的數(shù)量允許使用哪些數(shù)? 第一個問題的答案很簡單,但有時是實現(xiàn)相關的。如果要進行移位的操作數(shù)是無符號的,會移入0。如果操作數(shù)是帶符號的,則實現(xiàn)有權決定是移入0還是移入符號位。如果在一個右移操作中你很關心空位,那么用unsigned來聲明變量。這樣你就有權假設空位被設置為0。 第二個問題的答案同樣簡單:如果待移位的數(shù)長度為n,則移位的數(shù)量必須大于等于0并且嚴格地小于n。因此,在一次單獨的操作中不可能將所有的位從變量中移出。 例如,如果一個int是32位,且n是一個int,寫n 31和n 0是合法的,但n 32和n 1的值,這是不可能為0的。譯注:(-1) / 2的結果是0。5 庫函數(shù) 每個有用的C程序都會用到庫函數(shù),因為沒有辦法把輸入和輸出內(nèi)建到語言中去。在這一節(jié)中,我們將會看到一些廣泛使用的庫函數(shù)在某種情況下會出現(xiàn)的一些非預期行為。5.1 getc()返回整數(shù) 考慮下面的程序:#include main() char c; while(c = getchar() != EOF) putchar(c); 這段程序看起來好像要講標準輸入復制到標準輸出。實際上,它并不完全會做這些。 原

溫馨提示

  • 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

提交評論