国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書(shū)等14項(xiàng)超值服

開(kāi)通VIP
C語(yǔ)言缺陷與陷阱(筆記)

C語(yǔ)言像一把雕刻刀,鋒利,并且在技師手中非常有用。和任何鋒利的工具一樣,C會(huì)傷到那些不能掌握它的人。本文介紹C語(yǔ)言傷害粗心的人的方法,以及如何避免傷害。
第一部分研究了當(dāng)程序被劃分為記號(hào)時(shí)會(huì)發(fā)生的問(wèn)題。第二部分繼續(xù)研究了當(dāng)程序的記號(hào)被編譯器組合為聲明、表達(dá)式和語(yǔ)句時(shí)會(huì)出現(xiàn)的問(wèn)題。第三部分研究了由多個(gè)部分組成、分別編譯并綁定到一起的C程序。第四部分處理了概念上的誤解:當(dāng)一個(gè)程序具體執(zhí)行時(shí)會(huì)發(fā)生的事情。第五部分研究了我們的程序和它們所使用的常用庫(kù)之間的關(guān)系。在第六部分中,我們注意到了我們所寫(xiě)的程序也許并不是我們所運(yùn)行的程序;預(yù)處理器將首先運(yùn)行。最后,第七部分討論了可移植性問(wèn)題:一個(gè)能在一個(gè)實(shí)現(xiàn)中運(yùn)行的程序無(wú)法在另一個(gè)實(shí)現(xiàn)中運(yùn)行的原因。
詞法分析器(lexical analyzer):檢查組成程序的字符序列,并將它們劃分為記號(hào)(token)一個(gè)記號(hào)是一個(gè)由一個(gè)或多個(gè)字符構(gòu)成的序列,它在語(yǔ)言被編譯時(shí)具有一個(gè)(相關(guān)地)統(tǒng)一的意義。
C程序被兩次劃分為記號(hào),首先是預(yù)處理器讀取程序,它必須對(duì)程序進(jìn)行記號(hào)劃分以發(fā)現(xiàn)標(biāo)識(shí)宏的標(biāo)識(shí)符。通過(guò)對(duì)每個(gè)宏進(jìn)行求值來(lái)替換宏調(diào)用,最后,經(jīng)過(guò)宏替換的程序又被匯集成字符流送給編譯器。編譯器再第二次將這個(gè)流劃分為記號(hào)。
1.1= 不是 ==:C語(yǔ)言則是用=表示賦值而用==表示比較。這是因?yàn)橘x值的頻率要高于比較,因此為其分配更短的符號(hào)。C還將賦值視為一個(gè)運(yùn)算符,因此可以很容易地寫(xiě)出多重賦值(如a = b = c),并且可以將賦值嵌入到一個(gè)大的表達(dá)式中。
1.2 & 和| 不是&& 和||
1.3 多字符記號(hào)
C語(yǔ)言參考手冊(cè)說(shuō)明了如何決定:“如果輸入流到一個(gè)給定的字符串為止已經(jīng)被識(shí)別為記號(hào),則應(yīng)該包含下一個(gè)字符以組成能夠構(gòu)成記號(hào)的最長(zhǎng)的字符串” “最長(zhǎng)子串原則”
1.4 例外
    組合賦值運(yùn)算符如+=實(shí)際上是兩個(gè)記號(hào)。因此,
a + /* strange */ = 1

a += 1
是一個(gè)意思??雌饋?lái)像一個(gè)單獨(dú)的記號(hào)而實(shí)際上是多個(gè)記號(hào)的只有這一個(gè)特例。特別地,
p - > a
是不合法的。它和
p -> a
不是同義詞。
另一方面,有些老式編譯器還是將=+視為一個(gè)單獨(dú)的記號(hào)并且和+=是同義詞。
1.5 字符串和字符
包圍在單引號(hào)中的一個(gè)字符只是編寫(xiě)整數(shù)的另一種方法。這個(gè)整數(shù)是給定的字符在實(shí)現(xiàn)的對(duì)照序列中的一個(gè)對(duì)應(yīng)的值。而一個(gè)包圍在雙引號(hào)中的字符串,只是編寫(xiě)一個(gè)有雙引號(hào)之間的字符和一個(gè)附加的二進(jìn)制值為零的字符所初始化的一個(gè)無(wú)名數(shù)組的指針的一種簡(jiǎn)短方法。
使用一個(gè)指針來(lái)代替一個(gè)整數(shù)通常會(huì)得到一個(gè)警告消息(反之亦然),使用雙引號(hào)來(lái)代替單引號(hào)也會(huì)得到一個(gè)警告消息(反之亦然)。但對(duì)于不檢查參數(shù)類型的編譯器卻除外。
由于一個(gè)整數(shù)通常足夠大,以至于能夠放下多個(gè)字符,一些C編譯器允許在一個(gè)字符常量中存放多個(gè)字符。這意味著用'yes'代替"yes"將不會(huì)被發(fā)現(xiàn)。后者意味著“分別包含y、e、s和一個(gè)空字符的四個(gè)連續(xù)存儲(chǔ)器區(qū)域中的第一個(gè)的地址”,而前者意味著“在一些實(shí)現(xiàn)定義的樣式中表示由字符y、e、s聯(lián)合構(gòu)成的一個(gè)整數(shù)”。這兩者之間的任何一致性都純屬巧合。
2 句法缺陷
理解這些記號(hào)是如何構(gòu)成聲明、表達(dá)式、語(yǔ)句和程序的。
2.1 理解聲明
每個(gè)C變量聲明都具有兩個(gè)部分:一個(gè)類型和一組具有特定格式的、期望用來(lái)對(duì)該類型求值的表達(dá)式。
float *g(), (*h)();
表示*g()和(*h)()都是float表達(dá)式。由于()比*綁定得更緊密,*g()和*(g())表示同樣的東西:g是一個(gè)返回指float指針的函數(shù),而h是一個(gè)指向返回float的函數(shù)的指針。
當(dāng)我們知道如何聲明一個(gè)給定類型的變量以后,就能夠很容易地寫(xiě)出一個(gè)類型的模型(cast):只要?jiǎng)h除變量名和分號(hào)并將所有的東西包圍在一對(duì)圓括號(hào)中即可。
float *g();
聲明g是一個(gè)返回float指針的函數(shù),所以(float *())就是它的模型。
(*(void(*)())0)();硬件會(huì)調(diào)用地址為0處的子程序
(*0)(); 但這樣并不行,因?yàn)?運(yùn)算符要求必須有一個(gè)指針作為它的操作數(shù)。另外,這個(gè)操作數(shù)必須是一個(gè)指向函數(shù)的指針,以保證*的結(jié)果可以被調(diào)用。需要將0轉(zhuǎn)換為一個(gè)可以描述“指向一個(gè)返回void的函數(shù)的指針”的類型。(Void(*)())0
在這里,我們解決這個(gè)問(wèn)題時(shí)沒(méi)有使用typedef聲明。通過(guò)使用它,我們可以更清晰地解決這個(gè)問(wèn)題:
typedef void (*funcptr)();// typedef funcptr void (*)();指向返回void的函數(shù)的指針
(*(funcptr)0)();//調(diào)用地址為0處的子程序
2.2 運(yùn)算符并不總是具有你所想象的優(yōu)先級(jí)
綁定得最緊密的運(yùn)算符并不是真正的運(yùn)算符:下標(biāo)、函數(shù)調(diào)用和結(jié)構(gòu)選擇。這些都與左邊相關(guān)聯(lián)。
接下來(lái)是一元運(yùn)算符。它們具有真正的運(yùn)算符中的最高優(yōu)先級(jí)。由于函數(shù)調(diào)用比一元運(yùn)算符綁定得更緊密,你必須寫(xiě)(*p)()來(lái)調(diào)用p指向的函數(shù);*p()表示p是一個(gè)返回一個(gè)指針的函數(shù)。轉(zhuǎn)換是一元運(yùn)算符,并且和其他一元運(yùn)算符具有相同的優(yōu)先級(jí)。一元運(yùn)算符是右結(jié)合的,因此*p++表示*(p++),而不是(*p)++。
在接下來(lái)是真正的二元運(yùn)算符。其中數(shù)學(xué)運(yùn)算符具有最高的優(yōu)先級(jí),然后是移位運(yùn)算符、關(guān)系運(yùn)算符、邏輯運(yùn)算符、賦值運(yùn)算符,最后是條件運(yùn)算符。需要記住的兩個(gè)重要的東西是:
1.    所有的邏輯運(yùn)算符具有比所有關(guān)系運(yùn)算符都低的優(yōu)先級(jí)。
2.    移位運(yùn)算符比關(guān)系運(yùn)算符綁定得更緊密,但又不如數(shù)學(xué)運(yùn)算符。
乘法、除法和求余具有相同的優(yōu)先級(jí),加法和減法具有相同的優(yōu)先級(jí),以及移位運(yùn)算符具有相同的優(yōu)先級(jí)。
還有就是六個(gè)關(guān)系運(yùn)算符并不具有相同的優(yōu)先級(jí):==和!=的優(yōu)先級(jí)比其他關(guān)系運(yùn)算符要低。
在邏輯運(yùn)算符中,沒(méi)有任何兩個(gè)具有相同的優(yōu)先級(jí)。按位運(yùn)算符比所有順序運(yùn)算符綁定得都緊密,每種與運(yùn)算符都比相應(yīng)的或運(yùn)算符綁定得更緊密,并且按位異或(^)運(yùn)算符介于按位與和按位或之間。
    三元運(yùn)算符的優(yōu)先級(jí)比我們提到過(guò)的所有運(yùn)算符的優(yōu)先級(jí)都低。
這個(gè)例子還說(shuō)明了賦值運(yùn)算符具有比條件運(yùn)算符更低的優(yōu)先級(jí)是有意義的。另外,所有的復(fù)合賦值運(yùn)算符具有相同的優(yōu)先級(jí)并且是自右至左結(jié)合的
具有最低優(yōu)先級(jí)的是逗號(hào)運(yùn)算符。賦值是另一種運(yùn)算符,通常具有混合的優(yōu)先級(jí)。
2.3 看看這些分號(hào)!
或者是一個(gè)空語(yǔ)句,無(wú)任何效果;或者編譯器可能提出一個(gè)診斷消息,可以方便除去掉它。一個(gè)重要的區(qū)別是在必須跟有一個(gè)語(yǔ)句的if和while語(yǔ)句中。另一個(gè)因分號(hào)引起巨大不同的地方是函數(shù)定義前面的結(jié)構(gòu)聲明的末尾,考慮下面的程序片段:
struct foo {
    int x;
}

f() {
    ...
}
在緊挨著f的第一個(gè)}后面丟失了一個(gè)分號(hào)。它的效果是聲明了一個(gè)函數(shù)f,返回值類型是struct foo,這個(gè)結(jié)構(gòu)成了函數(shù)聲明的一部分。如果這里出現(xiàn)了分號(hào),則f將被定義為具有默認(rèn)的整型返回值[5]。
2.4 switch語(yǔ)句
C中的case標(biāo)簽是真正的標(biāo)簽:控制流程可以無(wú)限制地進(jìn)入到一個(gè)case標(biāo)簽中。
    看看另一種形式,假設(shè)C程序段看起來(lái)更像Pascal:
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
并且假設(shè)color的值是2。則該程序?qū)⒋蛴ellowblue,因?yàn)榭刂谱匀坏剞D(zhuǎn)入到下一個(gè)printf()的調(diào)用。
這既是C語(yǔ)言switch語(yǔ)句的優(yōu)點(diǎn)又是它的弱點(diǎn)。說(shuō)它是弱點(diǎn),是因?yàn)楹苋菀淄浺粋€(gè)break語(yǔ)句,從而導(dǎo)致程序出現(xiàn)隱晦的異常行為。說(shuō)它是優(yōu)點(diǎn),是因?yàn)橥ㄟ^(guò)故意去掉break語(yǔ)句,可以很容易實(shí)現(xiàn)其他方法難以實(shí)現(xiàn)的控制結(jié)構(gòu)。尤其是在一個(gè)大型的switch語(yǔ)句中,我們經(jīng)常發(fā)現(xiàn)對(duì)一個(gè)case的處理可以簡(jiǎn)化其他一些特殊的處理。
2.5 函數(shù)調(diào)用
和其他程序設(shè)計(jì)語(yǔ)言不同,C要求一個(gè)函數(shù)調(diào)用必須有一個(gè)參數(shù)列表,但可以沒(méi)有參數(shù)。因此,如果f是一個(gè)函數(shù),
f();
就是對(duì)該函數(shù)進(jìn)行調(diào)用的語(yǔ)句,而
f;
什么也不做。它會(huì)作為函數(shù)地址被求值,但不會(huì)調(diào)用它[6]。
2.6 懸掛else問(wèn)題
一個(gè)else總是與其最近的if相關(guān)聯(lián)。
3 連接
一個(gè)C程序可能有很多部分組成,它們被分別編譯,并由一個(gè)通常稱為連接器、連接編輯器或加載器的程序綁定到一起。由于編譯器一次通常只能看到一個(gè)文件,因此它無(wú)法檢測(cè)到需要程序的多個(gè)源文件的內(nèi)容才能發(fā)現(xiàn)的錯(cuò)誤。
3.1 你必須自己檢查外部類型
假設(shè)你有一個(gè)C程序,被劃分為兩個(gè)文件。其中一個(gè)包含如下聲明:
int n;
而令一個(gè)包含如下聲明:
long n;
這不是一個(gè)有效的C程序,因?yàn)橐恍┩獠棵Q在兩個(gè)文件中被聲明為不同的類型。然而,很多實(shí)現(xiàn)檢測(cè)不到這個(gè)錯(cuò)誤,因?yàn)榫幾g器在編譯其中一個(gè)文件時(shí)并不知道另一個(gè)文件的內(nèi)容。因此,檢查類型的工作只能由連接器(或一些工具程序如lint)來(lái)完成;如果操作系統(tǒng)的連接器不能識(shí)別數(shù)據(jù)類型,C編譯器也沒(méi)法過(guò)多地強(qiáng)制它。
    那么,這個(gè)程序運(yùn)行時(shí)實(shí)際會(huì)發(fā)生什么?這有很多可能性:
1.    實(shí)現(xiàn)足夠聰明,能夠檢測(cè)到類型沖突。則我們會(huì)得到一個(gè)診斷消息,說(shuō)明n在兩個(gè)文件中具有不同的類型。
2.    你所使用的實(shí)現(xiàn)將int和long視為相同的類型。典型的情況是機(jī)器可以自然地進(jìn)行32位運(yùn)算。在這種情況下你的程序或許能夠工作,好象你兩次都將變量聲明為long(或int)。但這種程序的工作純屬偶然。
3.    n的兩個(gè)實(shí)例需要不同的存儲(chǔ),它們以某種方式共享存儲(chǔ)區(qū),即對(duì)其中一個(gè)的賦值對(duì)另一個(gè)也有效。這可能發(fā)生,例如,編譯器可以將int安排在long的低位。不論這是基于系統(tǒng)的還是基于機(jī)器的,這種程序的運(yùn)行同樣是偶然。
4.    n的兩個(gè)實(shí)例以另一種方式共享存儲(chǔ)區(qū),即對(duì)其中一個(gè)賦值的效果是對(duì)另一個(gè)賦以不同的值。在這種情況下,程序可能失敗。
這種情況發(fā)生的另一個(gè)例子出奇地頻繁。程序的某一個(gè)文件包含下面的聲明:
char filename[] = "etc/passwd";
而另一個(gè)文件包含這樣的聲明:
char *filename;
    盡管在某些環(huán)境中數(shù)組和指針的行為非常相似,但它們是不同的。在第一個(gè)聲明中,filename是一個(gè)字符數(shù)組的名字。盡管使用數(shù)組的名字可以產(chǎn)生數(shù)組第一個(gè)元素的指針,但這個(gè)指針只有在需要的時(shí)候才產(chǎn)生并且不會(huì)持續(xù)。在第二個(gè)聲明中,filename是一個(gè)指針的名字。這個(gè)指針可以指向程序員讓它指向的任何地方。如果程序員沒(méi)有給它賦一個(gè)值,它將具有一個(gè)默認(rèn)的0值(NULL)([譯注]實(shí)際上,在C中一個(gè)為初始化的指針通常具有一個(gè)隨機(jī)的值,這是很危險(xiǎn)的?。?。
    這兩個(gè)聲明以不同的方式使用存儲(chǔ)區(qū),它們不可能共存。
    避免這種類型沖突的一個(gè)方法是使用像lint這樣的工具(如果可以的話)。為了在一個(gè)程序的不同編譯單元之間檢查類型沖突,一些程序需要一次看到其所有部分。典型的編譯器無(wú)法完成,但lint可以。
    避免該問(wèn)題的另一種方法是將外部聲明放到包含文件中。這時(shí),一個(gè)外部對(duì)象的類型僅出現(xiàn)一次[7]。
4 語(yǔ)義缺陷
4.1 表達(dá)式求值順序
    一些C運(yùn)算符以一種已知的、特定的順序?qū)ζ洳僮鲾?shù)進(jìn)行求值。但另一些不能。例如,考慮下面的表達(dá)式:
a < b && c < d
C語(yǔ)言定義規(guī)定a < b首先被求值。如果a確實(shí)小于b,c < d必須緊接著被求值以計(jì)算整個(gè)表達(dá)式的值。但如果a大于或等于b,則c < d根本不會(huì)被求值。
要對(duì)a < b求值,編譯器對(duì)a和b的求值就會(huì)有一個(gè)先后。但在一些機(jī)器上,它們也許是并行進(jìn)行的。
C中只有四個(gè)運(yùn)算符&&、||、?:和,指定了求值順序。&&和||最先對(duì)左邊的操作數(shù)進(jìn)行求值,而右邊的操作數(shù)只有在需要的時(shí)候才進(jìn)行求值。而?:運(yùn)算符中的三個(gè)操作數(shù):a、b和c,最先對(duì)a進(jìn)行求值,之后僅對(duì)b或c中的一個(gè)進(jìn)行求值,這取決于a的值。,運(yùn)算符首先對(duì)左邊的操作數(shù)進(jìn)行求值,然后拋棄它的值,對(duì)右邊的操作數(shù)進(jìn)行求值[8]。
C中所有其它的運(yùn)算符對(duì)操作數(shù)的求值順序都是未定義的。事實(shí)上,賦值運(yùn)算符不對(duì)求值順序做出任何保證。
    出于這個(gè)原因,下面這種將數(shù)組x中的前n個(gè)元素復(fù)制到數(shù)組y中的方法是不可行的:
i = 0;
while(i < n)
    y[i] = x[i++];
其中的問(wèn)題是y[i]的地址并不保證在i增長(zhǎng)之前被求值。在某些實(shí)現(xiàn)中,這是可能的;但在另一些實(shí)現(xiàn)中卻不可能。另一種情況出于同樣的原因會(huì)失敗:
i = 0;
while(i < n)
    y[i++] = x[i];
而下面的代碼是可以工作的:
i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}
當(dāng)然,這可以簡(jiǎn)寫(xiě)為:
for(i = 0; i < n; i++)
    y[i] = x[i];
4.2 &&、||和!運(yùn)算符
4.3 下標(biāo)從零開(kāi)始
    在很多語(yǔ)言中,具有n個(gè)元素的數(shù)組其元素的號(hào)碼和它的下標(biāo)是從1到n嚴(yán)格對(duì)應(yīng)的。但在C中不是這樣。
個(gè)具有n個(gè)元素的C數(shù)組中沒(méi)有下標(biāo)為n的元素,其中的元素的下標(biāo)是從0到n - 1。因此從其它語(yǔ)言轉(zhuǎn)到C語(yǔ)言的程序員應(yīng)該特別小心地使用數(shù)組:
int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;
4.4 C并不總是轉(zhuǎn)換實(shí)參
    下面的程序段由于兩個(gè)原因會(huì)失?。?br>double s;
s = sqrt(2);
printf("%g\n", s);
    第一個(gè)原因是sqrt()需要一個(gè)double值作為它的參數(shù),但沒(méi)有得到。第二個(gè)原因是它返回一個(gè)double值但沒(méi)有這樣聲名。改正的方法只有一個(gè):
double s, sqrt();
s = sqrt(2.0);
printf("%g\n", s);
C中有兩個(gè)簡(jiǎn)單的規(guī)則控制著函數(shù)參數(shù)的轉(zhuǎn)換:(1)比int短的整型被轉(zhuǎn)換為int;(2)比double短的浮點(diǎn)類型被轉(zhuǎn)換為double。所有的其它值不被轉(zhuǎn)換。確保函數(shù)參數(shù)類型的正確性是程序員的責(zé)任。
因此,一個(gè)程序員如果想使用如sqrt()這樣接受一個(gè)double類型參數(shù)的函數(shù),就必須僅傳遞給它float或double類型的參數(shù)。常數(shù)2是一個(gè)int,因此其類型是錯(cuò)誤的。
    當(dāng)一個(gè)函數(shù)的值被用在表達(dá)式中時(shí),其值會(huì)被自動(dòng)地轉(zhuǎn)換為適當(dāng)?shù)念愋?。然而,為了完成這個(gè)自動(dòng)轉(zhuǎn)換,編譯器必須知道該函數(shù)實(shí)際返回的類型。沒(méi)有更進(jìn)一步聲名的函數(shù)被假設(shè)返回int,因此聲名這樣的函數(shù)并不是必須的。然而,sqrt()返回double,因此在成功使用它之前必須要聲名。
這里有一個(gè)更加壯觀的例子:
main() {
    int i;
    char c;
    for(i = 0; i < 5; i++) {
        scanf("%d", &c);
        printf("%d", i);
    }
    printf("\n");
}
    表面上看,這個(gè)程序從標(biāo)準(zhǔn)輸入中讀取五個(gè)整數(shù)并向標(biāo)準(zhǔn)輸出寫(xiě)入0 1 2 3 4。實(shí)際上,它并不總是這么做。譬如在一些編譯器中,它的輸出為0 0 0 0 0 1 2 3 4。
    為什么?因?yàn)閏的聲名是char而不是int。當(dāng)你令scanf()去讀取一個(gè)整數(shù)時(shí),它需要一個(gè)指向一個(gè)整數(shù)的指針。但這里它得到的是一個(gè)字符的指針。但scanf()并不知道它沒(méi)有得到它所需要的:它將輸入看作是一個(gè)指向整數(shù)的指針并將一個(gè)整數(shù)存貯到那里。由于整數(shù)占用比字符更多的內(nèi)存,這樣做會(huì)影響到c附近的內(nèi)存。
    c附近確切是什么是編譯器的事;在這種情況下這有可能是i的低位。因此,每當(dāng)向c中讀入一個(gè)值,i就被置零。當(dāng)程序最后到達(dá)文件結(jié)尾時(shí),scanf()不再嘗試向c中放入新值,i才可以正常地增長(zhǎng),直到循環(huán)結(jié)束。
4.5 指針不是數(shù)組
C程序通常將一個(gè)字符串轉(zhuǎn)換為一個(gè)以空字符結(jié)尾的字符數(shù)組。假設(shè)我們有兩個(gè)這樣的字符串s和t,并且我們想要將它們連接為一個(gè)單獨(dú)的字符串r。我們通常使用庫(kù)函數(shù)strcpy()和strcat()來(lái)完成。下面這種明顯的方法并不會(huì)工作:
char *r;
strcpy(r, s);
strcat(r, t);
這是因?yàn)閞沒(méi)有被初始化為指向任何地方。盡管r可能潛在地表示某一塊內(nèi)存,但這并不存在,直到你分配它。
    讓我們?cè)僭囋?,為r分配一些內(nèi)存:
char r[100];
strcpy(r, s);
strcat(r, t);
這只有在s和t所指向的字符串不很大的時(shí)候才能夠工作。不幸的是,C要求我們?yōu)閿?shù)組指定的大小是一個(gè)常數(shù),因此無(wú)法確定r是否足夠大。然而,很多C實(shí)現(xiàn)帶有一個(gè)叫做malloc()的庫(kù)函數(shù),它接受一個(gè)數(shù)字并分配這么多的內(nèi)存。通常還有一個(gè)函數(shù)稱為strlen(),可以告訴我們一個(gè)字符串中有多少個(gè)字符:因此,我們可以寫(xiě):
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
    然而這個(gè)例子會(huì)因?yàn)閮蓚€(gè)原因而失敗。首先,malloc()可能會(huì)耗盡內(nèi)存,而這個(gè)事件僅通過(guò)靜靜地返回一個(gè)空指針來(lái)表示。
    其次,更重要的是,malloc()并沒(méi)有分配足夠的內(nèi)存。一個(gè)字符串是以一個(gè)空字符結(jié)束的。而strlen()函數(shù)返回其字符串參數(shù)中所包含字符的數(shù)量,但不包括結(jié)尾的空字符。因此,如果strlen(s)是n,則s需要n + 1個(gè)字符來(lái)盛放它。因此我們需要為r分配額外的一個(gè)字符。再加上檢查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)是一種文學(xué)手法,有點(diǎn)類似于明喻或暗喻,在牛津英文詞典中解釋如下:“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.(將全面的單位用作不全面的單位,或反之;如整體對(duì)局部或局部對(duì)整體、一般對(duì)特殊或特殊對(duì)一般,等等。)”
要記住的是,復(fù)制一個(gè)指針并不能復(fù)制它所指向的東西。
4.7 空指針不是空字符串
將一個(gè)整數(shù)轉(zhuǎn)換為一個(gè)指針的結(jié)果是實(shí)現(xiàn)相關(guān)的(implementation-dependent),除了一個(gè)例外。這個(gè)例外是常數(shù)0,它可以保證被轉(zhuǎn)換為一個(gè)與其它任何有效指針都不相等的指針。這個(gè)值通常類似這樣定義:
#define NULL 0
但其效果是相同的。要記住的一個(gè)重要的事情是,當(dāng)用0作為指針時(shí)它決不能被解除引用。換句話說(shuō),當(dāng)你將0賦給一個(gè)指針變量后,你就不能訪問(wèn)它所指向的內(nèi)存。不能這樣寫(xiě):
if(p == (char *)0) ...
也不能這樣寫(xiě):
if(strcmp(p, (char *)0) == 0) ...
因?yàn)閟trcmp()總是通過(guò)其參數(shù)來(lái)查看內(nèi)存地址的。
如果p是一個(gè)空指針,這樣寫(xiě)也是無(wú)效的:
printf(p);

printf("%s", p);
4.8 整數(shù)溢出
C語(yǔ)言關(guān)于整數(shù)操作的上溢或下溢定義得非常明確。
    只要有一個(gè)操作數(shù)是無(wú)符號(hào)的,結(jié)果就是無(wú)符號(hào)的,并且以2n為模,其中n為字長(zhǎng)。如果兩個(gè)操作數(shù)都是帶符號(hào)的,則結(jié)果是未定義的。
例如,假設(shè)a和b是兩個(gè)非負(fù)整型變量,你希望測(cè)試a + b是否溢出。一個(gè)明顯的辦法是這樣的:
if(a + b < 0)
    complain();
通常,這是不會(huì)工作的。
    一旦a + b發(fā)生了溢出,對(duì)于結(jié)果的任何賭注都是沒(méi)有意義的。例如,在某些機(jī)器上,一個(gè)加法運(yùn)算會(huì)將一個(gè)內(nèi)部寄存器設(shè)置為四種狀態(tài):正、負(fù)、零或溢出。在這樣的機(jī)器上,編譯器有權(quán)將上面的例子實(shí)現(xiàn)為首先將a和b加在一起,然后檢查內(nèi)部寄存器狀態(tài)是否為負(fù)。如果該運(yùn)算溢出,內(nèi)部寄存器將處于溢出狀態(tài),這個(gè)測(cè)試會(huì)失敗。
    使這個(gè)特殊的測(cè)試能夠成功的一個(gè)正確的方法是依賴于無(wú)符號(hào)算術(shù)的良好定義,即要在有符號(hào)和無(wú)符號(hào)之間進(jìn)行轉(zhuǎn)換:
if((int)((unsigned)a + (unsigned)b) < 0)
    complain();
4.9 移位運(yùn)算符
兩個(gè)原因會(huì)令使用移位運(yùn)算符的人感到煩惱:
1.    在右移運(yùn)算中,空出的位是用0填充還是用符號(hào)位填充?
2.    移位的數(shù)量允許使用哪些數(shù)?
第一個(gè)問(wèn)題的答案很簡(jiǎn)單,但有時(shí)是實(shí)現(xiàn)相關(guān)的。如果要進(jìn)行移位的操作數(shù)是無(wú)符號(hào)的,會(huì)移入0。如果操作數(shù)是帶符號(hào)的,則實(shí)現(xiàn)有權(quán)決定是移入0還是移入符號(hào)位。如果在一個(gè)右移操作中你很關(guān)心空位,那么用unsigned來(lái)聲明變量。這樣你就有權(quán)假設(shè)空位被設(shè)置為0。
    第二個(gè)問(wèn)題的答案同樣簡(jiǎn)單:如果待移位的數(shù)長(zhǎng)度為n,則移位的數(shù)量必須大于等于0并且嚴(yán)格地小于n。因此,在一次單獨(dú)的操作中不可能將所有的位從變量中移出。
例如,如果一個(gè)int是32位,且n是一個(gè)int,寫(xiě)n << 31和n << 0是合法的,但n << 32和n << -1是不合法的。
    注意,即使實(shí)現(xiàn)將符號(hào)為移入空位,對(duì)一個(gè)帶符號(hào)整數(shù)的右移運(yùn)算和除以2的某次冪也不是等價(jià)的。為了證明這一點(diǎn),考慮(-1) >> 1的值,這是不可能為0的。[譯注:(-1) / 2的結(jié)果是0。]
5 庫(kù)函數(shù)
5.1 getc()返回整數(shù)
考慮下面的程序:
#include

main() {
    char c;//int c;

    while((c = getchar()) != EOF)
        putchar(c);
}
    這段程序看起來(lái)好像要將標(biāo)準(zhǔn)輸入復(fù)制到標(biāo)準(zhǔn)輸出。實(shí)際上,它并不完全會(huì)做這些。
    原因是c被聲明為字符而不是整數(shù)。這意味著它將不能接收可能出現(xiàn)的所有字符包括EOF。
因此這里有兩種可能性。有時(shí)一些合法的輸入字符會(huì)導(dǎo)致c攜帶和EOF相同的值,有時(shí)又會(huì)使c無(wú)法存放EOF值。在前一種情況下,程序會(huì)在文件的中間停止復(fù)制。在后一種情況下,程序會(huì)陷入一個(gè)無(wú)限循環(huán)。
    實(shí)際上,還存在著第三種可能:程序會(huì)偶然地正確工作。C語(yǔ)言參考手冊(cè)嚴(yán)格地定義了表達(dá)式
((c = getchar()) != EOF)
的結(jié)果。其6.1節(jié)中聲明:
當(dāng)一個(gè)較長(zhǎng)的整數(shù)被轉(zhuǎn)換為一個(gè)較短的整數(shù)或一個(gè)char時(shí),它會(huì)被截去左側(cè);超出的位被簡(jiǎn)單地丟棄。
7.14節(jié)聲明:
存在著很多賦值運(yùn)算符,它們都是從右至左結(jié)合的。它們都需要一個(gè)左值作為左側(cè)的操作數(shù),而賦值表達(dá)式的類型就是其左側(cè)的操作數(shù)的類型。其值就是已經(jīng)賦過(guò)值的左操作數(shù)的值。
這兩個(gè)條款的組合效果就是必須通過(guò)丟棄getchar()的結(jié)果的高位,將其截短為字符,之后這個(gè)被截短的值再與EOF進(jìn)行比較。作為這個(gè)比較的一部分,c必須被擴(kuò)展為一個(gè)整數(shù),或者采取將左側(cè)的位用0填充,或者適當(dāng)?shù)夭扇》?hào)擴(kuò)展。
然而,一些編譯器并沒(méi)有正確地實(shí)現(xiàn)這個(gè)表達(dá)式。它們確實(shí)將getchar()的值的低幾位賦給c。但在c和EOF的比較中,它們卻使用了getchar()的值!這樣做的編譯器會(huì)使這個(gè)事例程序看起來(lái)能夠“正確地”工作。
5.2 緩沖輸出和內(nèi)存分配
立即安排輸出的顯示通常比將其暫時(shí)保存在一大塊一起輸出要昂貴得多。因此,C實(shí)現(xiàn)通常允許程序員控制產(chǎn)生多少輸出后在實(shí)際地寫(xiě)出它們。
    這個(gè)控制通常約定為一個(gè)稱為setbuf()的庫(kù)函數(shù)。如果buf是一個(gè)具有適當(dāng)大小的字符數(shù)組,則
setbuf(stdout, buf);
將告訴I/O庫(kù)寫(xiě)入到stdout中的輸出要以buf作為一個(gè)輸出緩沖,并且等到buf滿了或程序員直接調(diào)用fflush()再實(shí)際寫(xiě)出。緩沖區(qū)的合適的大小在中定義為BUFSIZ。
因此,下面的程序解釋了通過(guò)使用setbuf()來(lái)講標(biāo)準(zhǔn)輸入復(fù)制到標(biāo)準(zhǔn)輸出:
#include

main() {
    int c;

    char buf[BUFSIZ];
    setbuf(stdout, buf);

    while((c = getchar()) != EOF)
        putchar(c);
}
    不幸的是,這個(gè)程序是錯(cuò)誤的,因?yàn)橐粋€(gè)細(xì)微的原因。
    要知道毛病出在哪,我們需要知道緩沖區(qū)最后一次刷新是在什么時(shí)候。答案;主程序完成之后,庫(kù)將控制交回到操作系統(tǒng)之前所執(zhí)行的清理的一部分。在這一時(shí)刻,緩沖區(qū)已經(jīng)被釋放了!
    有兩種方法可以避免這一問(wèn)題。
    首先,使用靜態(tài)緩沖區(qū),或者將其顯式地聲明為靜態(tài):
static char buf[BUFSIZ];
或者將整個(gè)聲明移到主函數(shù)之外。
    另一種可能的方法是動(dòng)態(tài)地分配緩沖區(qū)并且從不釋放它:
char *malloc();
setbuf(stdout, malloc(BUFSIZ));
注意在后一種情況中,不必檢查malloc()的返回值,因?yàn)槿绻×耍瑫?huì)返回一個(gè)空指針。而setbuf()可以接受一個(gè)空指針作為其第二個(gè)參數(shù),這將使得stdout變成非緩沖的。這會(huì)運(yùn)行得很慢,但它是可以運(yùn)行的。
6 預(yù)處理器
6.1 宏不是函數(shù)
由于宏可以象函數(shù)那樣出現(xiàn),有些程序員有時(shí)就會(huì)將它們視為等價(jià)的。因此,看下面的定義:
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏體中所有的括號(hào)。它們是為了防止出現(xiàn)a和b是帶有比>優(yōu)先級(jí)低的表達(dá)式的情況。
    一個(gè)重要的問(wèn)題是,像max()這樣定義的宏每個(gè)操作數(shù)都會(huì)出現(xiàn)兩次并且會(huì)被求值兩次。因此,在這個(gè)例子中,如果a比b大,則a就會(huì)被求值兩次:一次是在比較的時(shí)候,而另一次是在計(jì)算max()值的時(shí)候。
    這不僅是低效的,還會(huì)發(fā)生錯(cuò)誤:
biggest = x[0];
i = 1;
while(i < n)
    biggest = max(biggest, x[i++]);
當(dāng)max()是一個(gè)真正的函數(shù)時(shí),這會(huì)正常地工作,但當(dāng)max()是一個(gè)宏的時(shí)候會(huì)失敗。譬如,假設(shè)x[0]是2、x[1]是3、x[2]是1。我們來(lái)看看在第一次循環(huán)時(shí)會(huì)發(fā)生什么。賦值語(yǔ)句會(huì)被擴(kuò)展為:
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先,biggest與x[i++]進(jìn)行比較。由于i是1而x[1]是3,這個(gè)關(guān)系是“假”。其副作用是,i增長(zhǎng)到2。
    由于關(guān)系是“假”,x[i++]的值要賦給biggest。然而,這時(shí)的i變成2了,因此賦給biggest的值是x[2]的值,即1。
避免這些問(wèn)題的方法是保證max()宏的參數(shù)沒(méi)有副作用:
biggest = x[0];
for(i = 1; i < n; i++)
    biggest = max(biggest, x[i]);
還有一個(gè)危險(xiǎn)的例子是混合宏及其副作用。這是來(lái)自UNIX第八版的中putc()宏的定義:
#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc()的第一個(gè)參數(shù)是一個(gè)要寫(xiě)入到文件中的字符,第二個(gè)參數(shù)是一個(gè)指向一個(gè)表示文件的內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針。注意第一個(gè)參數(shù)完全可以使用如*z++之類的東西,盡管它在宏中兩次出現(xiàn),但只會(huì)被求值一次。而第二個(gè)參數(shù)會(huì)被求值兩次(在宏體中,x出現(xiàn)了兩次,但由于它的兩次出現(xiàn)分別在一個(gè):的兩邊,因此在putc()的一個(gè)實(shí)例中它們之中有且僅有一個(gè)被求值)。由于putc()中的文件參數(shù)可能帶有副作用,這偶爾會(huì)出現(xiàn)問(wèn)題。不過(guò),用戶手冊(cè)文檔中提到:“由于putc()被實(shí)現(xiàn)為宏,其對(duì)待stream可能會(huì)具有副作用。特別是putc(c, *f++)不能正確地工作。”但是putc(*c++, f)在這個(gè)實(shí)現(xiàn)中是可以工作的。
有些C實(shí)現(xiàn)很不小心。例如,沒(méi)有人能正確處理putc(*c++, f)。另一個(gè)例子,考慮很多C庫(kù)中出現(xiàn)的toupper()函數(shù)。它將一個(gè)小寫(xiě)字母轉(zhuǎn)換為相應(yīng)的大寫(xiě)字母,而其它字符不變。如果我們假設(shè)所有的小寫(xiě)字母和所有的大寫(xiě)字母都是相鄰的(大小寫(xiě)之間可能有所差距),我們可以得到這樣的函數(shù):
toupper(c) {
    if(c >= 'a' && c <= 'z')
        c += 'A' - 'a';
    return c;
}
在很多C實(shí)現(xiàn)中,為了減少比實(shí)際計(jì)算還要多的調(diào)用開(kāi)銷,通常將其實(shí)現(xiàn)為宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' - 'a') : (c))
很多時(shí)候這確實(shí)比函數(shù)要快。然而,當(dāng)你試著寫(xiě)toupper(*p++)時(shí),會(huì)出現(xiàn)奇怪的結(jié)果。
    另一個(gè)需要注意的地方是使用宏可能會(huì)產(chǎn)生巨大的表達(dá)式。例如,繼續(xù)考慮max()的定義:
#define max(a, b) ((a) > (b) ? (a) : (b))
假設(shè)我們這個(gè)定義來(lái)查找a、b、c和d中的最大值。如果我們直接寫(xiě):
max(a, max(b, max(c, d)))
它將被擴(kuò)展為:
((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ?
 (a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
這出奇的龐大。我們可以通過(guò)平衡操作數(shù)來(lái)使它短一些:
max(max(a, b), max(c, d))
這會(huì)得到:
((((a) > (b) ? (a) : (b))) > (((c) > (d) ? (c) : (d))) ?
 (((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
這看起來(lái)還是寫(xiě):
biggest = a;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
比較好一些。
6.2 宏不是類型定義
宏的一個(gè)通常的用途是保證不同地方的多個(gè)事物具有相同的類型:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;
這允許程序員可以通過(guò)只改變程序中的一行就能改變a、b和c的類型,盡管a、b和c可能聲明在很遠(yuǎn)的不同地方。
    使用這樣的宏定義還有著可移植性的優(yōu)勢(shì)——所有的C編譯器都支持它。很多C編譯器并不支持另一種方法:
typedef struct foo FOOTYPE;
這將FOOTYPE定義為一個(gè)與struct foo等價(jià)的新類型。
    這兩種為類型命名的方法可以是等價(jià)的,但typedef更靈活一些。例如,考慮下面的例子:
#define T1 struct foo *
typedef struct foo * T2;
這兩個(gè)定義使得T1和T2都等價(jià)于一個(gè)struct foo的指針。但看看當(dāng)我們?cè)噲D在一行中聲明多于一個(gè)變量的時(shí)候會(huì)發(fā)生什么:
T1 a, b;
T2 c, d;
第一個(gè)聲明被擴(kuò)展為:
struct foo * a, b;
這里a被定義為一個(gè)結(jié)構(gòu)指針,但b被定義為一個(gè)結(jié)構(gòu)(而不是指針)。相反,第二個(gè)聲明中c和d都被定義為指向結(jié)構(gòu)的指針,因?yàn)門(mén)2的行為好像真正的類型一樣。
7 可移植性缺陷
今天,一個(gè)C程序員如果想寫(xiě)出對(duì)于不同環(huán)境中的用戶都有用的程序就必須知道很多這些細(xì)微的差別。
7.1 一個(gè)名字中都有什么?
一個(gè)標(biāo)識(shí)符是一個(gè)字符和數(shù)字序列,第一個(gè)字符必須是一個(gè)字母。下劃線_算作字母。大寫(xiě)字母和小寫(xiě)字母是不同的。只有前八個(gè)字符是簽名,但可以使用更多的字符??梢员欢喾N匯編器和加載器使用的外部標(biāo)識(shí)符,有著更多的限制:
考慮下面這個(gè)顯著的函數(shù):
char *Malloc(unsigned n) {
    char *p, *malloc();
    p = malloc(n);
    if(p == NULL)
        panic("out of memory");
    return p;
}
    這個(gè)函數(shù)是保證耗盡內(nèi)存而不會(huì)導(dǎo)致沒(méi)有檢測(cè)的一個(gè)簡(jiǎn)單的辦法。程序員可以通過(guò)調(diào)用Mallo()來(lái)代替malloc()。如果malloc()不幸失敗,將調(diào)用panic()來(lái)顯示一個(gè)恰當(dāng)?shù)腻e(cuò)誤消息并終止程序。
然而,考慮當(dāng)該函數(shù)用于一個(gè)忽略大小寫(xiě)區(qū)別的系統(tǒng)中時(shí)會(huì)發(fā)生什么。這時(shí),名字malloc和Malloc是等價(jià)的。換句話說(shuō),庫(kù)函數(shù)malloc()被上面的Malloc()函數(shù)完全取代了,當(dāng)調(diào)用malloc()時(shí)它調(diào)用的是它自己。顯然,其結(jié)果就是第一次嘗試分配內(nèi)存就會(huì)陷入一個(gè)遞歸循環(huán)并隨之發(fā)生混亂。但在一些能夠區(qū)分大小寫(xiě)的實(shí)現(xiàn)中這個(gè)函數(shù)還是可以工作的。
7.2 一個(gè)整數(shù)有多大?
C為程序員提供三種整數(shù)尺寸:普通、短和長(zhǎng),還有字符,其行為像一個(gè)很小的整數(shù)。C語(yǔ)言定義對(duì)各種整數(shù)的大小不作任何保證:
1.    整數(shù)的四種尺寸是非遞減的。
2.    普通整數(shù)的大小要足夠存放任意的數(shù)組下標(biāo)。
3.    字符的大小應(yīng)該體現(xiàn)特定硬件的本質(zhì)。
許多現(xiàn)代機(jī)器具有8位字符,不過(guò)還有一些具有7位獲9位字符。因此字符通常是7、8或9位。
    長(zhǎng)整數(shù)通常至少32位,因此一個(gè)長(zhǎng)整數(shù)可以用于表示文件的大小。
    普通整數(shù)通常至少16位,因?yàn)樘〉恼麛?shù)會(huì)更多地限制一個(gè)數(shù)組的最大大小。
    短整數(shù)總是恰好16位。
一種更可移植的做法是定義一個(gè)“新的”類型:
typedef long tenmil;
現(xiàn)在你就可以使用這個(gè)類型來(lái)聲明一個(gè)變量并知道它的寬度了,最壞的情況下,你也只要改變這個(gè)單獨(dú)的類型定義就可以使所有這些變量具有正確的類型。
7.3 字符是帶符號(hào)的還是無(wú)符號(hào)的?
這些問(wèn)題在將一個(gè)char制轉(zhuǎn)換為一個(gè)更大的整數(shù)時(shí)變得尤為重要。對(duì)于相反的轉(zhuǎn)換,其結(jié)果卻是定義良好的:多余的位被簡(jiǎn)單地丟棄掉。但一個(gè)編譯器將一個(gè)char轉(zhuǎn)換為一個(gè)int卻需要作出選擇:將char視為帶符號(hào)量還是無(wú)符號(hào)量?如果是前者,將char擴(kuò)展為int時(shí)要復(fù)制符號(hào)位;如果是后者,則要將多余的位用0填充。
這個(gè)決定的結(jié)果對(duì)于那些在處理字符時(shí)習(xí)慣將高位置1的人來(lái)說(shuō)非常重要。這決定著8位的字符范圍是從-128到127還是從0到255。這又影響著程序員對(duì)哈希表和轉(zhuǎn)換表之類的東西的設(shè)計(jì)。
    如果你關(guān)心一個(gè)字符值最高位置一時(shí)是否被視為一個(gè)負(fù)數(shù),你應(yīng)該顯式地將它聲明為unsigned char。這樣就能保證在轉(zhuǎn)換為整數(shù)時(shí)是基0的,而不像普通char變量那樣在一些實(shí)現(xiàn)中是帶符號(hào)的而在另一些實(shí)現(xiàn)中是無(wú)符號(hào)的。
另外,還有一種誤解是認(rèn)為當(dāng)c是一個(gè)字符變量時(shí),可以通過(guò)寫(xiě)(unsigned)c來(lái)得到與c等價(jià)的無(wú)符號(hào)整數(shù)。這是錯(cuò)誤的,因?yàn)橐粋€(gè)char值在進(jìn)行任何操作(包括轉(zhuǎn)換)之前轉(zhuǎn)換為int。這時(shí)c會(huì)首先轉(zhuǎn)換為一個(gè)帶符號(hào)整數(shù)再轉(zhuǎn)換為一個(gè)無(wú)符號(hào)整數(shù),這會(huì)產(chǎn)生奇怪的結(jié)果。
    正確的方法是寫(xiě)(unsigned char)c。
7.4 右移位是帶符號(hào)的還是無(wú)符號(hào)的?
這里再一次重復(fù):一個(gè)關(guān)心右移操作如何進(jìn)行的程序最好將所有待移位的量聲明為無(wú)符號(hào)的。
7.5 除法如何舍入?
假設(shè)我們用b除a得到商為q余數(shù)為r:
q = a / b;
r = a % b;
我們暫時(shí)假設(shè)b > 0。
1.    最重要的,我們期望q * b + r == a,因?yàn)檫@是對(duì)余數(shù)的定義。
2.    如果a的符號(hào)發(fā)生改變,我們期望q的符號(hào)也發(fā)生改變,但絕對(duì)值不變。
3.    我們希望保證r >= 0且r < b。例如,如果余數(shù)將作為一個(gè)哈希表的索引,它必須要保證總是一個(gè)有效的索引。
    這三點(diǎn)清楚地描述了整數(shù)除法和求余操作。不幸的是,它們不能同時(shí)為真。
考慮3 / 2,商1余0。(1)這滿足第一點(diǎn)。而-3 / 2的值呢?根據(jù)第二點(diǎn),商應(yīng)該是-1,但如果是這樣的話,余數(shù)必須也是-1,這違反了第三點(diǎn)?;蛘?,我們可以通過(guò)將余數(shù)標(biāo)記為1來(lái)滿足第三點(diǎn),但這時(shí)根據(jù)第一點(diǎn)商應(yīng)該是-2。這又違反了第二點(diǎn)。
因此C和其他任何實(shí)現(xiàn)了整數(shù)除法舍入的語(yǔ)言必須放棄上述三個(gè)原則中的至少一個(gè)。
很多程序設(shè)計(jì)語(yǔ)言放棄了第三點(diǎn),要求余數(shù)的符號(hào)必須和被除數(shù)相同。這可以保證第一點(diǎn)和第二點(diǎn)。很多C實(shí)現(xiàn)也是這樣做的。
 盡管有些時(shí)候不需要靈活性,C語(yǔ)言還是足夠可以讓我們令除法完成我們所要做的、提供我們所想知道的。例如,假設(shè)我們有一個(gè)數(shù)n表示一個(gè)標(biāo)識(shí)符中的字符的一些函數(shù),并且我們想通過(guò)除法得到一個(gè)哈希表入口h,其中0 <= h <= HASHSIZE。如果我們知道n是非負(fù)的,我們可以簡(jiǎn)單地寫(xiě):
h = n % HASHSIZE;
然而,如果n有可能是負(fù)的,這樣寫(xiě)就不好了,因?yàn)閔可能也是負(fù)的。然而,我們知道h > -HASHSIZE,因此我們可以寫(xiě):
h = n % HASHSIZE;
if(n < 0)
    h += HASHSIZE;
    同樣,將n聲明為unsigned也可以。
7.6 一個(gè)隨機(jī)數(shù)有多大?
這個(gè)尺寸是模糊的,還受庫(kù)設(shè)計(jì)的影響。在PDP-11[10]機(jī)器上運(yùn)行的僅有的C實(shí)現(xiàn)中,有一個(gè)稱為rand()的函數(shù)可以返回一個(gè)(偽)隨機(jī)非負(fù)整數(shù)。PDP-11中整數(shù)長(zhǎng)度包括符號(hào)位是16位,因此rand()返回一個(gè)0到215-1之間的整數(shù)。
    當(dāng)C在VAX-11上實(shí)現(xiàn)時(shí),整數(shù)的長(zhǎng)度變?yōu)?2位長(zhǎng)。那么VAX-11上的rand()函數(shù)返回值范圍是什么呢?
    對(duì)于這個(gè)系統(tǒng),加利福尼亞大學(xué)的人認(rèn)為rand()的返回值應(yīng)該涵蓋所有可能的非負(fù)整數(shù),因此它們的rand()版本返回一個(gè)0到231-1之間的整數(shù)。
    而AT&T的人則覺(jué)得如果rand()函數(shù)仍然返回一個(gè)0到215之間的值則可以很容易地將PDP-11中期望rand()能夠返回一個(gè)小于215的值的程序移植到VAX-11上。
    因此,現(xiàn)在還很難寫(xiě)出不依賴實(shí)現(xiàn)而調(diào)用rand()函數(shù)的程序。
7.7 大小寫(xiě)轉(zhuǎn)換
toupper()和tolower()函數(shù)有著類似的歷史。他們最初都被實(shí)現(xiàn)為宏:
#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
這些宏確實(shí)有一個(gè)缺陷,即:當(dāng)給定的東西不是一個(gè)恰當(dāng)?shù)淖址?,它?huì)返回垃圾。因此,下面這個(gè)通過(guò)使用這些宏來(lái)將一個(gè)文件轉(zhuǎn)為小寫(xiě)的程序是無(wú)法工作的:
int c;
while((c = getchar()) != EOF)
    putchar(tolower(c));
我們必須寫(xiě):
int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);
就這一點(diǎn),AT&T中的UNIX開(kāi)發(fā)組織提醒我們,toupper()和tolower()都是事先經(jīng)過(guò)一些適當(dāng)?shù)膮?shù)進(jìn)行測(cè)試的??紤]這樣重寫(xiě)這些宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))
但要知道,這里c的三次出現(xiàn)都要被求值,這會(huì)破壞如toupper(*p++)這樣的表達(dá)式。因此,可以考慮將toupper()和tolower()重寫(xiě)為函數(shù)。toupper()看起來(lái)可能像這樣:
int toupper(int c) {
    if(c >= 'a' && c <= 'z')
        return c + 'A' - 'a';
    return c;
}
tolower()類似。
    這個(gè)改變帶來(lái)更多的問(wèn)題,每次使用這些函數(shù)的時(shí)候都會(huì)引入函數(shù)調(diào)用開(kāi)銷。我們的英雄認(rèn)為一些人可能不愿意支付這些開(kāi)銷,因此他們將這個(gè)宏重命名為:
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
這就允許用戶選擇方便或速度。
    這里面其實(shí)只有一個(gè)問(wèn)題:伯克利的人們和其他的C實(shí)現(xiàn)者并沒(méi)有跟著這么做。這意味著一個(gè)在AT&T系統(tǒng)上編寫(xiě)的使用了toupper()或tolower()的程序,如果沒(méi)有為其傳遞正確大小寫(xiě)字母參數(shù),在其他C實(shí)現(xiàn)中可能不會(huì)正常工作。
    如果不知道這些歷史,可能很難對(duì)這類錯(cuò)誤進(jìn)行跟蹤。
7.8 先釋放,再重新分配
很多C實(shí)現(xiàn)為用戶提供了三個(gè)內(nèi)存分配函數(shù):malloc()、realloc()和free()。調(diào)用malloc(n)返回一個(gè)指向有n個(gè)字符的新分配的內(nèi)存的指針,這個(gè)指針可以由程序員使用。給free()傳遞一個(gè)指向由malloc()分配的內(nèi)存的指針可以使這塊內(nèi)存得以再次使用。通過(guò)一個(gè)指向已分配區(qū)域的指針和一個(gè)新的大小調(diào)用realloc()可以將這塊內(nèi)存擴(kuò)大或縮小到新尺寸,這個(gè)過(guò)程中可能要復(fù)制內(nèi)存。
    也許有人會(huì)想,真相真是有點(diǎn)微妙啊。下面是System V接口定義中出現(xiàn)的對(duì)realloc()的描述:
realloc改變一個(gè)由ptr指向的size個(gè)字節(jié)的塊,并返回該塊(可能被移動(dòng))的指針。在新舊尺寸中比較小的一個(gè)尺寸之下的內(nèi)容不會(huì)被改變。此外,還包含了描述realloc()的另外一段:
如果在最后一次調(diào)用malloc、realloc或calloc后釋放了ptr所指向的塊,realloc依舊可以工作;因此,free、malloc和realloc的順序可以利用malloc壓縮存貯的查找策略。
因此,下面的代碼片段在UNIX第七版中是合法的:
free (p);
p = realloc(p, newsize);
    這一特性保留在從UNIX第七版衍生出來(lái)的系統(tǒng)中:可以先釋放一塊存儲(chǔ)區(qū)域,然后再重新分配它。這意味著,在這些系統(tǒng)中釋放的內(nèi)存中的內(nèi)容在下一次內(nèi)存分配之前可以保證不變。因此,在這些系統(tǒng)中,我們可以用下面這種奇特的思想來(lái)釋放一個(gè)鏈表中的所有元素:
for(p = head; p != NULL; p = p->next)
    free((char *)p);
而不用擔(dān)心調(diào)用free()會(huì)導(dǎo)致p->next不可用。
不用說(shuō),這種技術(shù)是不推薦的,因?yàn)椴皇撬蠧實(shí)現(xiàn)都能在內(nèi)存被釋放后將它的內(nèi)容保留足夠長(zhǎng)的時(shí)間。然而,第七版的手冊(cè)遺留了一個(gè)未聲明的問(wèn)題:realloc()的原始實(shí)現(xiàn)實(shí)際上是必須要先釋放再重新分配的。出于這個(gè)原因,一些C程序都是先釋放內(nèi)存再重新分配的,而當(dāng)這些程序移植到其他實(shí)現(xiàn)中時(shí)就會(huì)出現(xiàn)問(wèn)題。
7.9 可移植性問(wèn)題的一個(gè)實(shí)例
下面的程序帶有兩個(gè)參數(shù):一個(gè)長(zhǎng)整數(shù)和一個(gè)函數(shù)(的指針)。它將整數(shù)轉(zhuǎn)換位十進(jìn)制數(shù),并用代表其中每一個(gè)數(shù)字的字符來(lái)調(diào)用給定的函數(shù)。
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}
    這個(gè)程序非常簡(jiǎn)單。首先檢查n是否為負(fù)數(shù);如果是,則打印一個(gè)符號(hào)并將n變?yōu)檎龜?shù)。接下來(lái),測(cè)試是否n >= 10。如果是,則它的十進(jìn)制表示中包含兩個(gè)或更多個(gè)數(shù)字,因此我們遞歸地調(diào)用printnum()來(lái)打印除最后一個(gè)數(shù)字外的所有數(shù)字。最后,我們打印最后一個(gè)數(shù)字。
    這個(gè)程序——由于它的簡(jiǎn)單——具有很多可移植性問(wèn)題。首先是將n的低位數(shù)字轉(zhuǎn)換成字符形式的方法。用n % 10來(lái)獲取低位數(shù)字的值是好的,但為它加上'0'來(lái)獲得相應(yīng)的字符表示就不好了。這個(gè)加法假設(shè)機(jī)器中順序的數(shù)字所對(duì)應(yīng)的字符數(shù)順序的,沒(méi)有間隔,因此'0' + 5和'5'的值是相同的,等等。盡管這個(gè)假設(shè)對(duì)于ASCII和EBCDIC字符集是成立的,但對(duì)于其他一些機(jī)器可能不成立。避免這個(gè)問(wèn)題的方法是使用一個(gè)表:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}
    另一個(gè)問(wèn)題發(fā)生在當(dāng)n < 0時(shí)。這時(shí)程序會(huì)打印一個(gè)負(fù)號(hào)并將n設(shè)置為-n。這個(gè)賦值會(huì)發(fā)生溢出,因?yàn)樵谑褂?的補(bǔ)碼的機(jī)器上通常能夠表示的負(fù)數(shù)比正數(shù)要多。例如,一個(gè)(長(zhǎng))整數(shù)有k位和一個(gè)附加位表示符號(hào),則-2k可以表示而2k卻不能。
    解決這一問(wèn)題有很多方法。最直觀的一種是將n賦給一個(gè)unsigned long值。然而,一些C便一起可能沒(méi)有實(shí)現(xiàn)unsigned long,因此我們來(lái)看看沒(méi)有它怎么辦。
    在第一個(gè)實(shí)現(xiàn)和第二個(gè)實(shí)現(xiàn)的機(jī)器上,改變一個(gè)正整數(shù)的符號(hào)保證不會(huì)發(fā)生溢出。問(wèn)題僅出在改變一個(gè)負(fù)數(shù)的符號(hào)時(shí)。因此,我們可以通過(guò)避免將n變?yōu)檎龜?shù)來(lái)避免這個(gè)問(wèn)題。
    當(dāng)然,一旦我們打印了負(fù)數(shù)的符號(hào),我們就能夠?qū)⒇?fù)數(shù)和正數(shù)視為是一樣的。下面的方法就強(qiáng)制在打印符號(hào)之后n為負(fù)數(shù),并且用負(fù)數(shù)值完成我們所有的算法。如果我們這么做,我們就必須保證程序中打印符號(hào)的部分只執(zhí)行一次;一個(gè)簡(jiǎn)單的方法是將這個(gè)程序劃分為兩個(gè)函數(shù):
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        printneg(n, p);
    }
    else
        printneg(-n, p);
}

void printneg(long n, void (*p)()) {
    if(n <= -10)
        printneg(n / 10, p);
    (*p)("0123456789"[-(n % 10)]);
}
    printnum()現(xiàn)在只檢查要打印的數(shù)是否為負(fù)數(shù);如果是的話則打印一個(gè)符號(hào)。否則,它以n的負(fù)絕對(duì)值來(lái)調(diào)用printneg()。我們同時(shí)改變了printneg()的函數(shù)體來(lái)適應(yīng)n永遠(yuǎn)是負(fù)數(shù)或零這一事實(shí)。
    我們得到什么?我們使用n / 10和n % 10來(lái)獲取n的前導(dǎo)數(shù)字和結(jié)尾數(shù)字(經(jīng)過(guò)適當(dāng)?shù)姆?hào)變換)。調(diào)用整數(shù)除法的行為在其中一個(gè)操作數(shù)為負(fù)的時(shí)候是實(shí)現(xiàn)相關(guān)的。因此,n % 10有可能是正的!這時(shí),-(n % 10)是負(fù)數(shù),將會(huì)超出我們的數(shù)字字符數(shù)組的末尾。
    為了解決這一問(wèn)題,我們建立兩個(gè)臨時(shí)變量來(lái)存放商和余數(shù)。作完除法后,我們檢查余數(shù)是否在正確的范圍內(nèi),如果不是的話則調(diào)整這兩個(gè)變量。printnum()沒(méi)有改變,因此我們只列出printneg():
void printneg(long n, void (*p)()) {
    long q;
    int r;
    if(r > 0) {
        r -= 10;
        q++;
    }
    if(n <= -10) {
        printneg(q, p);
    }
    (*p)("0123456789"[-r]);
}
8 這里是空閑空間
參考
    《The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具權(quán)威的C著作。它包含了一個(gè)優(yōu)秀的教程,面向那些熟悉其他高級(jí)語(yǔ)言程序設(shè)計(jì)的人,和一個(gè)參考手冊(cè),簡(jiǎn)潔地描述了整個(gè)語(yǔ)言。盡管自1978年以來(lái)這門(mén)語(yǔ)言發(fā)生了不少變化,這本書(shū)對(duì)于很多主題來(lái)說(shuō)仍然是個(gè)定論。這本書(shū)同時(shí)還包含了本文中多次提到的“C語(yǔ)言參考手冊(cè)”。
    《The C Puzzle Book》(Feuer, Prentice-Hall, 1982)是一本少見(jiàn)的磨煉人們文法能力的書(shū)。這本書(shū)收集了很多謎題(和答案),它們的解決方法能夠測(cè)試讀者對(duì)于C語(yǔ)言精妙之處的知識(shí)。
    《C: A Referenct Manual》(Harbison and Steele, Prentice Hall 1984)是特意為實(shí)現(xiàn)者編寫(xiě)的一本參考資料。其他人也會(huì)發(fā)現(xiàn)它是特別有用的——因?yàn)樗軓闹袇⒖技?xì)節(jié)。
1.這本書(shū)是基于圖書(shū)《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一個(gè)擴(kuò)充,有興趣的讀者可以讀一讀它。 

本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/skyyunmi/archive/2006/12/27/1464719.aspx

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
C語(yǔ)言陷阱和缺陷
C陷阱和缺陷學(xué)習(xí)筆記
免費(fèi)自學(xué)編程,讓月薪上萬(wàn)成為現(xiàn)實(shí)!
C 內(nèi)存管理詳解
C語(yǔ)言入門(mén)教程-字符串須知
C語(yǔ)言難點(diǎn)分析整理
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服