久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

C語言陷阱和缺陷

 lvgs 2006-07-28

[譯序]

    那些自認為已經(jīng)“學完”C語言的人,,請你們仔細讀閱讀這篇文章吧,。路還長,,很多東西要學。我也是……

[概述]

    C語言像一把雕刻刀,,鋒利,,并且在技師手中非常有用。和任何鋒利的工具一樣,,C會傷到那些不能掌握它的人,。本文介紹C語言傷害粗心的人的方法,以及如何避免傷害,。

[內(nèi)容]

0 簡介
1 詞法缺陷
1.1 = 不是 ==
1.2 & 和 | 不是 && 和 ||
1.3 多字符記號
1.4 例外
1.5 字符串和字符
2 句法缺陷
2.1 理解聲明
2.2 運算符并不總是具有你所想象的優(yōu)先級
2.3 看看這些分號,!
2.4 switch語句
2.5 函數(shù)調(diào)用
2.6 懸掛else問題
3 鏈接
3.1 你必須自己檢查外部類型
4 語義缺陷
4.1 表達式求值順序
4.2 &&、||和!運算符
4.3 下標從零開始
4.4 C并不總是轉(zhuǎn)換實參
4.5 指針不是數(shù)組
4.6 避免提喻法
4.7 空指針不是空字符串
4.8 整數(shù)溢出
4.9 移位運算符
5 庫函數(shù)
5.1 getc()返回整數(shù)
5.2 緩沖輸出和內(nèi)存分配
6 預處理器
6.1 宏不是函數(shù)
6.2 宏不是類型定義
7 可移植性缺陷
7.1 一個名字中都有什么,?
7.2 一個整數(shù)有多大,?
7.3 字符是帶符號的還是無符號的?
7.4 右移位是帶符號的還是無符號的,?
7.5 除法如何舍入,?
7.6 一個隨機數(shù)有多大?
7.7 大小寫轉(zhuǎn)換
7.8 先釋放,,再重新分配
7.9 可移植性問題的一個實例
8 這里是空閑空間
參考
腳注
 
0 簡介
    C語言及其典型實現(xiàn)被設(shè)計為能被專家們?nèi)菀椎厥褂?。這門語言簡潔并附有表達力。但有一些限制可以保護那些浮躁的人,。一個浮躁的人可以從這些條款中獲得一些幫助。

    在本文中,,我們將會看一看這些未可知的益處,。這是由于它的未可知,我們無法為其進行完全的分類,。不過,,我們?nèi)匀煌ㄟ^研究為了一個C程序的運行所需要做的事來做到這些。我們假設(shè)讀者對C語言至少有個粗淺的了解,。

    第一部分研究了當程序被劃分為記號時會發(fā)生的問題,。第二部分繼續(xù)研究了當程序的記號被編譯器組合為聲明、表達式和語句時會出現(xiàn)的問題,。第三部分研究了由多個部分組成,、分別編譯并綁定到一起的C程序,。第四部分處理了概念上的誤解:當一個程序具體執(zhí)行時會發(fā)生的事情。第五部分研究了我們的程序和它們所使用的常用庫之間的關(guān)系,。在第六部分中,,我們注意到了我們所寫的程序也不并不是我們所運行的程序;預處理器將首先運行,。最后,,第七部分討論了可移植性問題:一個能在一個實現(xiàn)中運行的程序無法在另一個實現(xiàn)中運行的原因。

1 詞法缺陷
    編譯器的第一個部分常被稱為詞法分析器(lexical analyzer),。詞法分析器檢查組成程序的字符序列,,并將它們劃分為記號(token)一個記號是一個有一個或多個字符的序列,它在語言被編譯時具有一個(相關(guān)地)統(tǒng)一的意義,。在C中,, 例如,記號->的意義和組成它的每個獨立的字符具有明顯的區(qū)別,,而且其意義獨立于->出現(xiàn)的上下文環(huán)境,。

    另外一個例子,考慮下面的語句:

if(x > big) big = x;

該語句中的每一個分離的字符都被劃分為一個記號,,除了關(guān)鍵字if和標識符big的兩個實例,。

    事實上,C程序被兩次劃分為記號,。首先是預處理器讀取程序,。它必須對程序進行記號劃分以發(fā)現(xiàn)標識宏的標識符。它必須通過對每個宏進行求值來替換宏調(diào)用,。最后,,經(jīng)過宏替換的程序又被匯集成字符流送給編譯器。編譯器再第二次將這個流劃分為記號,。

    在這一節(jié)中,,我們將探索對記號的意義的普遍的誤解以及記號和組成它們的字符之間的關(guān)系。稍后我們將談到預處理器,。

1.1 = 不是 ==
    從Algol派生出來的語言,,如Pascal和Ada,用:=表示賦值而用=表示比較,。而C語言則是用=表示賦值而用==表示比較,。這是因為賦值的頻率要高于比較,因此為其分配更短的符號,。

    此外,,C還將賦值視為一個運算符,因此可以很容易地寫出多重賦值(如a = b = c),,并且可以將賦值嵌入到一個大的表達式中,。

    這種便捷導致了一個潛在的問題:可能將需要比較的地方寫成賦值,。因此,下面的語句好像看起來是要檢查x是否等于y:

if(x = y)
    foo();

而實際上是將x設(shè)置為y的值并檢查結(jié)果是否非零,。在考慮下面的一個希望跳過空格,、制表符和換行符的循環(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的條件給出一個警告以提醒用戶。當你趨勢需要先對一個變量進行賦值之后再檢查變量是否非零時,,為了在這種編譯器中避免警告信息,,應(yīng)考慮顯式給出比較符。換句話說,,將:

if(x = y)
    foo();

改寫為:

if((x = y) != 0)
    foo();

這樣可以清晰地表示你的意圖,。

1.2 & 和 | 不是 && 和 ||
    容易將==錯寫為=是因為很多其他語言使用=表示比較運算。 其他容易寫錯的運算符還有&和&&,,或|和||,,這主要是因為C語言中的&和|運算符于其他語言中具有類似功能的運算符大為不同。我們將在第4節(jié)中貼近地觀察這些運算符,。

1.3 多字符記號
    一些C記號,,如/、*和=只有一個字符,。而其他一些C記號,,如/*和==,以及標識符,,具有多個字符,。當C編譯器遇到緊連在一起的/和*時,它必須能夠決定是將這兩個字符識別為兩個分離的記號還是一個單獨的記號,。C語言參考手冊說明了如何決定:“如果輸入流到一個給定的字符串為止已經(jīng)被識別為記號,則應(yīng)該包含下一個字符以組成能夠構(gòu)成記號的最長的字符串”,。因此,,如果/是一個記號的第一個字符,并且/后面緊隨了一個*,,則這兩個字符構(gòu)成了注釋的開始,,不管其他上下文環(huán)境,。

    下面的語句看起來像是將y的值設(shè)置為x的值除以p所指向的值:

y = x/*p    /* p 指向除數(shù) */;

實際上,/*開始了一個注釋,,因此編譯器簡單地吞噬程序文本,,直到*/的出現(xiàn)。換句話說,,這條語句僅僅把y的值設(shè)置為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中的意義完全不同,在一些混亂的上下文中它們會導致奇怪的結(jié)果而不是錯誤消息,。

    包圍在單引號中的一個字符只是書寫整數(shù)的另一種方法,。這個整數(shù)是給定的字符在實現(xiàn)的對照序列中的一個對應(yīng)的值。因此,,在一個ASCII實現(xiàn)中,,‘a(chǎn)‘和0141或97表示完全相同的東西。而一個包圍在雙引號中的字符串,,只是書寫一個有雙引號之間的字符和一個附加的二進制值為零的字符所初始化的一個無名數(shù)組的指針的一種簡短方法,。

    線面的兩個程序片斷是等價的:

printf("Hello world\n");

char hello[] = { ‘H‘, ‘e‘, ‘l‘, ‘l‘, ‘o‘, ‘ ‘, ‘w‘, ‘o‘, ‘r‘, ‘l‘, ‘d‘, ‘\n‘, 0 };
printf(hello);

    使用一個指針來代替一個整數(shù)通常會得到一個警告消息(反之亦然),使用雙引號來代替單引號也會得到一個警告消息(反之亦然),。但對于不檢查參數(shù)類型的編譯器卻除外,。因此,用

printf(‘\n‘);

來代替

printf("\n");

通常會在運行時得到奇怪的結(jié)果。

    由于一個整數(shù)通常足夠大,,以至于能夠放下多個字符,,一些C編譯器允許在一個字符常量中存放多個字符。這意味著用‘yes‘代替"yes"將不會被發(fā)現(xiàn),。后者意味著“分別包含y,、e、s和一個空字符的四個連續(xù)存貯器區(qū)域中的第一個的地址”,,而前者意味著“在一些實現(xiàn)定義的樣式中表示由字符y,、e、s聯(lián)合構(gòu)成的一個整數(shù)”,。這兩者之間的任何一致性都純屬巧合,。

2 句法缺陷
    要理解C語言程序,僅了解構(gòu)成它的記號是不夠的,。還要理解這些記號是如何構(gòu)成聲明,、表達式、語句和程序的,。盡管這些構(gòu)成通常都是定義良好的,,但這些定義有時候是有悖于直覺的或混亂的。

    在這一節(jié)中,,我們將著眼于一些不明顯句法構(gòu)造,。

2.1 理解聲明
    我曾經(jīng)和一些人聊過天,他們那時在書寫在一個小型的微處理器上單機運行的C程序,。當這臺機器的開關(guān)打開的時候,,硬件會調(diào)用地址為0處的子程序。

    為了模仿電源打開的情形,,我們要設(shè)計一條C語句來顯式地調(diào)用這個子程序,。經(jīng)過一些思考,我們寫出了下面的語句:

(*(void(*)())0)();

    這樣的表達式會令C程序員心驚膽戰(zhàn),。但是,,并不需要這樣,因為他們可以在一個簡單的規(guī)則的幫助下很容易地構(gòu)造它:以你使用的方式聲明它,。

    每個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)()了,。 我們可以將它分為兩個部分進行分析,。首先,假設(shè)我們有一個變量fp,,它包含了一個函數(shù)指針,,并且我們希望調(diào)用fp所指向的函數(shù)??梢赃@樣寫:

(*fp)();

如果fp是一個指向函數(shù)的指針,,則*fp就是函數(shù)本身,因此(*fp)()是調(diào)用它的一種方法,。(*fp)中的括號是必須的,,否則這個表達式將會被分析為*(fp())。我們現(xiàn)在要找一個適當?shù)谋磉_式來替換fp,。

    這個問題就是我們的第二步分析,。如果C可以讀入并理解類型,我們可以寫:

(*0)();

但這樣并不行,,因為*運算符要求必須有一個指針作為他的操作數(shù),。另外,這個操作數(shù)必須是一個指向函數(shù)的指針,,以保證*的結(jié)果可以被調(diào)用,。因此,我們需要將0轉(zhuǎn)換為一個可以描述“指向一個返回void的函數(shù)的指針”的類型,。

    如果fp是一個指向返回void的函數(shù)的指針,,則(*fp)()是一個void值,并且它的聲明將會是這樣的:

void (*fp)();

因此,我們需要寫:

void (*fp)();
(*fp)();

來聲明一個啞變量,。一旦我們知道了如何聲明該變量,,我們也就知道了如何將一個常數(shù)轉(zhuǎn)換為該類型:只要從變量的聲明中去掉名字即可。因此,,我們像下面這樣將0轉(zhuǎn)換為一個“指向返回void的函數(shù)的指針”:

(void(*)())0

接下來,,我們用(void(*)())0來替換fp:

(*(void(*)())0)();

結(jié)尾處的分號用于將這個表達式轉(zhuǎn)換為一個語句。

    在這里,,我們就解決了這個問題時沒有使用typedef聲明,。通過使用它,我們可以更清晰地解決這個問題:

typedef void (*funcptr)();
(*(funcptr)0)();

2.2 運算符并不總是具有你所想象的優(yōu)先級
    假設(shè)有一個聲明了的常量FLAG是一個整數(shù),,其二進制表示中的某一位被置位(換句話說,,它是2的某次冪),并且你希望測試一個整型變量flags該位是否被置位,。通常的寫法是:

if(flags & FLAG) ...

其意義對于很多C程序員都是很明確的:if語句測試括號中的表達式求值的結(jié)果是否為0,。出于清晰的目的我們可以將它寫得更明確:

if(flags & FLAG != 0) ...

這個語句現(xiàn)在更容易理解了。但它仍然是錯的,,因為!=比&綁定得更緊密,,因此它被分析為:

if(flags & (FLAG != 0)) ...

這(偶爾)是可以的,如FLAG是1或0(?。┑臅r候,,但對于其他2的冪是不行的[2]。

    假設(shè)你有兩個整型變量,,h和l,,它們的值在0和15(含0和15)之間,并且你希望將r設(shè)置為8位值,,其低位為l,,高位為h。一種自然的寫法是:

r = h << 4 + 1;

不幸的是,,這是錯誤的,。加法比移位綁定得更緊密,因此這個例子等價于:

r = h << (4 + l);

正確的方法有兩種:

r = (h << 4) + l;

r = h << 4 | l;

    避免這種問題的一個方法是將所有的東西都用括號括起來,,但表達式中的括號過度就會難以理解,,因此最好還是是記住C中的優(yōu)先級。

    不幸的是,,這有15個,,太困難了。然而,,通過將它們分組可以變得容易,。

    綁定得最緊密的運算符并不是真正的運算符:下標,、函數(shù)調(diào)用和結(jié)構(gòu)選擇。這些都與左邊相關(guān)聯(lián),。

    接下來是一元運算符,。它們具有真正的運算符中的最高優(yōu)先級。由于函數(shù)調(diào)用比一元運算符綁定得更緊密,,你必須寫(*p)()來調(diào)用p指向的函數(shù),;*p()表示p是一個返回一個指針的函數(shù)。轉(zhuǎn)換是一元運算符,,并且和其他一元運算符具有相同的優(yōu)先級。一元運算符是右結(jié)合的,,因此*p++表示*(p++),,而不是(*p)++。

    在接下來是真正的二元運算符,。其中數(shù)學運算符具有最高的優(yōu)先級,,然后是移位運算符、關(guān)系運算符,、邏輯運算符,、賦值運算符,最后是條件運算符,。需要記住的兩個重要的東西是:

所有的邏輯運算符具有比所有關(guān)系運算符都低的優(yōu)先級,。
一位運算符比關(guān)系運算符綁定得更緊密,但又不如數(shù)學運算符,。
    在這些運算符類別中,,有一些奇怪的地方。乘法,、除法和求余具有相同的優(yōu)先級,,加法和減法具有相同的優(yōu)先級,以及移位運算符具有相同的優(yōu)先級,。

    還有就是六個關(guān)系運算符并不具有相同的優(yōu)先級:==和!=的優(yōu)先級比其他關(guān)系運算符要低,。這就允許我們判斷a和b是否具有與c和d相同的順序,例如:

a < b == c < d

    在邏輯運算符中,,沒有任何兩個具有相同的優(yōu)先級,。按位運算符比所有順序運算符綁定得都緊密,每種與運算符都比相應(yīng)的或運算符綁定得更緊密,,并且按位異或(^)運算符介于按位與和按位或之間,。

    三元運算符的優(yōu)先級比我們提到過的所有運算符的優(yōu)先級都低。這可以保證選擇表達式中包含的關(guān)系運算符的邏輯組合特性,,如:

z = a < b && b < c ? d : e

    這個例子還說明了賦值運算符具有比條件運算符更低的優(yōu)先級是有意義的,。另外,,所有的復合賦值運算符具有相同的優(yōu)先級并且是自右至左結(jié)合的,因此

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以結(jié)束循環(huán)。不幸的是,,賦值的優(yōu)先級比任何比較操作都低,,因此c的值將會是getc(in)和EOF比較的結(jié)果,并且會被拋棄,。因此,,“復制”得到的文件將是一個由值為1的字節(jié)流組成的文件。

    上面這個例子正確的寫法并不難:

while((c = getc(in)) != EOF)
    putc(c, out);

然而,,這種錯誤在很多復雜的表達式中卻很難被發(fā)現(xiàn),。例如,隨UNIX系統(tǒng)一同發(fā)布的lint程序通常帶有下面的錯誤行:

if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY) {

這條語句希望給t賦一個值,,然后看t是否與STRTY或UNIONTY相等,。而實際的效果卻大不相同[3]。

    C中的邏輯運算符的優(yōu)先級具有歷史原因,。B——C的前輩——具有和C中的&和|運算符對應(yīng)的邏輯運算符,。盡管它們的定義是按位的 ,但編譯器在條件判斷上下文中將它們視為和&&和||一樣,。當在C中將它們分開后,,優(yōu)先級的改變是很危險的[4]。

2.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ù)定義前面的結(jié)構(gòu)聲明的末尾[譯注:這句話不太好聽,,看例子就明白了]??紤]下面的程序片段:

struct foo {
    int x;
}

f() {
    ...
}

在緊挨著f的第一個}后面丟失了一個分號,。它的效果是聲明了一個函數(shù)f,返回值類型是struct foo,,這個結(jié)構(gòu)成了函數(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 of
1: write (‘red‘);
2: write (‘yellow‘);
3: write (‘blue‘);
end

    這兩個程序片斷都作相同的事情:根據(jù)變量color的值是1,、2還是3打印red,、yellow或blue(沒有新行符)。這兩個程序片斷非常相似,,只有一點不同:Pascal程序中沒有C中相應(yīng)的break語句。C中的case標簽是真正的標簽:控制流程可以無限制地進入到一個case標簽中,。

    看看另一種形式,,假設(shè)C程序段看起來更像Pascal:

switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}

并且假設(shè)color的值是2。則該程序?qū)⒋蛴ellowblue,,因為控制自然地轉(zhuǎn)入到下一個printf()的調(diào)用,。

    這既是C語言switch語句的優(yōu)點又是它的弱點。說它是弱點,,是因為很容易忘記一個break語句,,從而導致程序出現(xiàn)隱晦的異常行為。說它是優(yōu)點,,是因為通過故意去掉break語句,,可以很容易實現(xiàn)其他方法難以實現(xiàn)的控制結(jié)構(gòu)。尤其是在一個大型的switch語句中,,我們經(jīng)常發(fā)現(xiàn)對一個case的處理可以簡化其他一些特殊的處理,。

    例如,設(shè)想有一個程序是一臺假想的機器的翻譯器,。這樣的一個程序可能包含一個switch語句來處理各種操作碼,。在這樣一臺機器上,通常減法在對其第二個運算數(shù)進行變號后就變成和加法一樣了,。因此,,最好可以寫出這樣的語句:

case SUBTRACT:
    opnd2 = -opnd2;
    /* no break; */
case ADD:
    ...

    另外一個例子,考慮編譯器通過跳過空白字符來查找一個記號,。這里,,我們將空格,、制表符和新行符視為是相同的,除了新行符還要引起行計數(shù)器的增長外:

case ‘\n‘:
    linecount++;
    /* no break */
case ‘\t‘:
case ‘ ‘:
    ...

2.5 函數(shù)調(diào)用
    和其他程序設(shè)計語言不同,,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);
}

    寫這段程序的程序員的目的明顯是將情況分為兩種:x = 0和x != 0,。在第一種情況中,,程序段什么都不做,除非y = 0時調(diào)用error(),。第二種情況中,,程序設(shè)置z = x + y并以z的地址作為參數(shù)調(diào)用f()。

    然而,, 這段程序的實際效果卻大為不同,。其原因是一個else總是與其最近的if相關(guān)聯(lián)。如果我們希望這段程序能夠按照實際的情況運行,,應(yīng)該這樣寫:

if(x == 0) {
    if(y == 0)
        error();
    else {
        z = x + y;
        f(&z);
    }
}

換句話說,,當x != 0發(fā)生時什么也不做。如果要達到第一個例子的效果,,應(yīng)該寫:

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 你必須自己檢查外部類型
    假設(shè)你有一個C程序,,被劃分為兩個文件。其中一個包含如下聲明:

int n;

而令一個包含如下聲明:

long n;

這不是一個有效的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ā)生的里一個例子出奇地頻繁,。程序的某一個文件包含下面的聲明:

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)生未定義結(jié)果的環(huán)境。我們這里討論的東西并不保證能夠在所有的C實現(xiàn)中工作,。我們暫且忘記這些能夠在一些實現(xiàn)中工作但可能不能在另一些實現(xiàn)中工作的東西,,直到第7節(jié)討論可以執(zhí)行問題為止。

4.1 表達式求值順序
    一些C運算符以一種已知的,、特定的順序?qū)ζ洳僮鲾?shù)進行求值,。但另一些不能。例如,,考慮下面的表達式:

a < b && c < d

C語言定義規(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)中卻不可能。另一種情況出于同樣的原因會失?。?/p>

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 &&、||和!運算符
    C中有兩種邏輯運算符,,在某些情況下是可以交換的:按位運算符&、|和~,,以及邏輯運算符&&,、||和!。一個程序員如果用某一類運算符替換相應(yīng)的另一類運算符會得到某些奇怪的效果:程序可能會正確地工作,,但這純屬偶然,。

    &&、||和!運算符將它們的參數(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 && tab[i] != x)
    i++;

這段循環(huán)背后的意思是如果i等于tabsize時循環(huán)結(jié)束,,元素未被找到。否則,,i包含了元素的索引,。

    假設(shè)這個例子中的&&不小心被替換為了&,這個循環(huán)可能仍然能夠工作,,但只有兩種幸運的情況可以使它停下來,。

    首先,這兩個操作都是當條件為假時返回0,,當條件為真時返回1,。只要x和y都是1或0,x & y和x && y都具有相同的值,。然而,,如果當使用了出了1之外的非零值表示“真”時互換了這兩個運算符,這個循環(huán)將不會工作,。

    其次,,由于數(shù)組元素不會改變,因此越過數(shù)組最后一個元素進一個位置時是無害的,,循環(huán)會幸運地停下來,。失誤的程序會越過數(shù)組的結(jié)尾,因為&不像&&,,總是會對所有的操作數(shù)進行求值,。因此循環(huán)的最后一次獲取tab[i]時i的值已經(jīng)等于tabsize了。如果tabsize是tab中元素的數(shù)量,, 則會取到tab中不存在的一個值,。

4.3 下標從零開始
    在很多語言中,具有n個元素的數(shù)組其元素的號碼和它的下標是從1到n嚴格對應(yīng)的,。但在C中不是這樣,。

    一個具有n個元素的C數(shù)組中沒有下標為n的元素,其中的元素的下標是從0到n - 1。因此從其它語言轉(zhuǎn)到C語言的程序員應(yīng)該特別小心地使用數(shù)組:

int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;

這個例子的目的是要將a中的每個元素都設(shè)置為0,,但沒有期望的效果,。因為for語句中的比較i < 10被替換成了i <= 10,a中的一個編號為10的并不存在的元素被設(shè)置為了0,,這樣內(nèi)存中a后面的一個字被破壞了,。如果編譯該程序的編譯器按照降序地址為用戶變量分配內(nèi)存,則a后面就是i,。將i設(shè)置為零會導致該循環(huán)陷入一個無限循環(huán),。

4.4 C并不總是轉(zhuǎn)換實參
    下面的程序段由于兩個原因會失敗:

double s;
s = sqrt(2);
printf("%g\n", s);

    第一個原因是sqrt()需要一個double值作為它的參數(shù),,但沒有得到,。第二個原因是它返回一個double值但沒有這樣聲名。改正的方法只有一個:

double s, sqrt();
s = 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()這樣接受一個double類型參數(shù)的函數(shù),就必須僅傳遞給它float或double類型的參數(shù),。常數(shù)2是一個int,,因此其類型是錯誤的。

    當一個函數(shù)的值被用在表達式中時,,其值會被自動地轉(zhuǎn)換為適當?shù)念愋?。然而,為了完成這個自動轉(zhuǎn)換,,編譯器必須知道該函數(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就被置零,。當程序最后到達文件結(jié)尾時,,scanf()不再嘗試向c中放入新值,i才可以正常地增長,,直到循環(huán)結(jié)束,。

4.5 指針不是數(shù)組
    C程序通常將一個字符串轉(zhuǎn)換為一個以空字符結(jié)尾的字符數(shù)組。假設(shè)我們有兩個這樣的字符串s和t,,并且我們想要將它們連接為一個單獨的字符串r,。我們通常使用庫函數(shù)strcpy()和strcat()來完成。下面這種明顯的方法并不會工作:

char *r;
strcpy(r, s);
strcat(r, t);

這是因為r沒有被 初始化為指向任何地方,。盡管r可能潛在地表示某一塊內(nèi)存,,但這并不存在,直到你分配它,。

    讓我們再試試,,為r分配一些內(nèi)存:

char r[100];
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)存,。一個字符串是以一個空字符結(jié)束的。而strlen()函數(shù)返回其字符串參數(shù) 中所包含字符的數(shù)量,,但不包括結(jié)尾的空字符,。因此,如果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í)行:

q[1] = ‘Y‘;

q所指向的內(nèi)存包含字符串xYz。p也是,,因為p和q指向相同的內(nèi)存,。

4.7 空指針不是空字符串
    將一個整數(shù)轉(zhuǎn)換為一個指針的結(jié)果是實現(xiàn)相關(guān)的(implementation-dependent),,除了一個例外。這個例外是常數(shù)0,,它可以保證被轉(zhuǎn)換為一個與其它任何有效指針都不相等的指針。這個值通常類似這樣定義:

#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語言關(guān)于整數(shù)操作的上溢或下溢定義得非常明確。

    只要有一次操作數(shù)是無符號的,,結(jié)果就是無符號的,,并且以2n為模,其中n為字長,。如果兩個操作數(shù)都是帶符號的,,則結(jié)果是未定義的。

    例如,,假設(shè)a和b是兩個非負整型變量,,你希望測試a + b是否溢出。一個明顯的辦法是這樣的:

if(a + b < 0)
    complain();

通常,,這是不會工作的,。

    一旦a + b發(fā)生了溢出,對于結(jié)果的任何賭注都是沒有意義的,。例如,,在某些機器上,一個加法運算會將一個內(nèi)部寄存器設(shè)置為四種狀態(tài):正,、負,、零或溢出。 在這樣的機器上,,編譯器有權(quán)將上面的例子實現(xiàn)為首先將a和b加在一起,,然后檢查內(nèi)部寄存器狀態(tài)是否為負。如果該運算溢出,,內(nèi)部寄存器將處于溢出狀態(tài),,這個測試會失敗。

    使這個特殊的測試能夠成功的一個正確的方法是依賴于無符號算術(shù)的良好定義,,既要在有符號和無符號之間進行轉(zhuǎn)換:

if((int)((unsigned)a + (unsigned)b) < 0)
    complain();

4.9 移位運算符
    兩個原因會令使用移位運算符的人感到煩惱:

在右移運算中,,空出的位是用0填充還是用符號位填充,?
移位的數(shù)量允許使用哪些數(shù)?
    第一個問題的答案很簡單,,但有時是實現(xiàn)相關(guān)的,。如果要進行移位的操作數(shù)是無符號的,會移入0,。如果操作數(shù)是帶符號的,,則實現(xiàn)有權(quán)決定是移入0還是移入符號位。如果在一個右移操作中你很關(guān)心空位,,那么用unsigned來聲明變量,。這樣你就有權(quán)假設(shè)空位被設(shè)置為0。

    第二個問題的答案同樣簡單:如果待移位的數(shù)長度為n,,則移位的數(shù)量必須大于等于0并且嚴格地小于n,。因此,在一次單獨的操作中不可能將所有的位從變量中移出,。

    例如,,如果一個int是32位,且n是一個int,,寫n << 31和n << 0是合法的,,但n << 32和n << -1是不合法的。

    注意,,即使實現(xiàn)將符號為移入空位,,對一個帶符號整數(shù)的右移運算和除以2的某次冪也不是等價的。為了證明這一點,,考慮(-1) >> 1的值,,這是不可能為0的。[譯注:(-1) / 2的結(jié)果是0,。]

5 庫函數(shù)
    每個有用的C程序都會用到庫函數(shù),,因為沒有辦法把輸入和輸出內(nèi)建到語言中去。在這一節(jié)中,,我們將會看到一些廣泛使用的庫函數(shù)在某種情況下會出現(xiàn)的一些非預期行為,。

5.1 getc()返回整數(shù)
    考慮下面的程序:

#include <stdio.h>

main() {
    char c;

    while((c = getchar()) != EOF)
        putchar(c);
}

    這段程序看起來好像要講標準輸入復制到標準輸出。實際上,,它并不完全會做這些,。

    原因是c被聲明為字符而不是整數(shù)。這意味著它將不能接收可能出現(xiàn)的所有字符包括EOF,。

    因此這里有兩種可能性,。有時一些合法的輸入字符會導致c攜帶和EOF相同的值,有時又會使c無法存放EOF值,。在前一種情況下,,程序會在文件的中間停止復制,。在后一種情況下,程序會陷入一個無限循環(huán),。

    實際上,,還存在著第三種可能:程序會偶然地正確工作。C語言參考手冊嚴格地定義了表達式

((c = getchar()) != EOF)

的結(jié)果,。其6.1節(jié)中聲明:

當一個較長的整數(shù)被轉(zhuǎn)換為一個較短的整數(shù)或一個char時,,它會被截去左側(cè);超出的位被簡單地丟棄,。

7.14節(jié)聲明:

存在著很多賦值運算符,它們都是從右至左結(jié)合的,。它們都需要一個左值作為左側(cè)的操作數(shù),,而賦值表達式的類型就是其左側(cè)的操作數(shù)的類型。其值就是已經(jīng)付過值的左操作數(shù)的值,。

這兩個條款的組合效果就是必須通過丟棄getchar()的結(jié)果的高位,,將其截短為字符,之后這個被截短的值再與EOF進行比較,。作為這個比較的一部分,,c必須被擴展為一個整數(shù),或者采取將左側(cè)的位用0填充,,或者適當?shù)夭扇》枖U展,。

    然而,一些編譯器并沒有正確地實現(xiàn)這個表達式,。它們確實將getchar()的值的低幾位賦給c,。但在c和EOF的比較中,它們卻使用了getchar()的值,!這樣做的編譯器會使這個事例程序看起來能夠“正確地”工作,。

5.2 緩沖輸出和內(nèi)存分配
    當一個程序產(chǎn)生輸出時,能夠立即看到它有多重要,?這取決于程序,。

    例如,終端上顯示輸出并要求人們坐在終端前面回答一個問題,,人們能夠看到輸出以知道該輸入什么就顯得至關(guān)重要了,。另一方面,如果輸出到一個文件中,,并最終被發(fā)送到一個行式打印機,,只有所有的輸出最終能夠到達那里是重要的。

    立即安排輸出的顯示通常比將其暫時保存在一大塊一起輸出要昂貴得多,。因此,,C實現(xiàn)通常允許程序員控制產(chǎn)生多少輸出后在實際地寫出它們,。

    這個控制通常約定為一個稱為setbuf()的庫函數(shù)。如果buf是一個具有適當大小的字符數(shù)組,,則

setbuf(stdout, buf);

將告訴I/O庫寫入到stdout中的輸出要以buf作為一個輸出緩沖,,并且等到buf滿了或程序員直接調(diào)用fflush()再實際寫出。緩沖區(qū)的合適的大小在<stdio.h>中定義為BUFSIZ,。

    因此,,下面的程序解釋了通過使用setbuf()來講標準輸入復制到標準輸出:

#include <stdio.h>

main() {
    int c;

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

    while((c = getchar()) != EOF)
        putchar(c);
}

    不幸的是,這個程序是錯誤的,,因為一個細微的原因,。

    要知道毛病出在哪,我們需要知道緩沖區(qū)最后一次刷新是在什么時候,。答案,;主程序完成之后,作為庫在將控制交回到操作系統(tǒng)之前所執(zhí)行的清理的一部分,。在這一時刻,,緩沖區(qū)已經(jīng)被釋放了!

    有兩種方法可以避免這一問題,。

    首先,,是用靜態(tài)緩沖區(qū),或者將其顯式地聲明為靜態(tài):

static char buf[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)用所需的運行開銷。例如,putchar()和getchar()通常實現(xiàn)為宏以避免對每一個字符的輸入輸出都要進行函數(shù)調(diào)用,。

6.1 宏不是函數(shù)
    由于宏可以象函數(shù)那樣出現(xiàn),,有些程序員有時就會將它們視為等價的。因此,,看下面的定義:

#define max(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()是一個宏的時候會失敗。譬如,,假設(shè)x[0]是2,、x[1]是3、x[2]是1,。我們來看看在第一次循環(huán)時會發(fā)生什么,。賦值語句會被擴展為:

biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));

首先,biggest與x[i++]進行比較,。由于i是1而x[1]是3,,這個關(guān)系是“假”。其副作用是,,i增長到2,。

    由于關(guān)系是“假”,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()宏的定義:

#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))

putc()的第一個參數(shù)是一個要寫入到文件中的字符,,第二個參數(shù)是一個指向一個表示文件的內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針,。注意第一個參數(shù)完全可以使用如*z++之類的東西,盡管它在宏中兩次出現(xiàn),但只會被求值一次,。而第二個參數(shù)會被求值兩次(在宏體中,,x出現(xiàn)了兩次,但由于 它的兩次出現(xiàn)分別在一個:的兩邊,,因此在putc()的一個實例中它們之中有且僅有一個被求值),。由于putc()中的文件參數(shù)可能帶有副作用,這偶爾會出現(xiàn)問題,。不過,,用戶手冊文檔中提到:“由于putc()被實現(xiàn)為宏,其對待stream可能會具有副作用,。特別是putc(c, *f++)不能正確地工作,。”但是putc(*c++, f)在這個實現(xiàn)中是可以工作的。

    有些C實現(xiàn)很不小心,。例如,,沒有人能正確處理putc(*c++, f)。另一個例子,,考慮很多C庫中出現(xiàn)的toupper()函數(shù),。它將一個小寫字母轉(zhuǎn)換為相應(yīng)的大寫字母,而其它字符不變,。如果我們假設(shè)所有的小寫字母和所有的大寫字母都是相鄰的(大小寫之間可能有所差距),,我們可以得到這樣的函數(shù):

toupper(c) {
    if(c >= ‘a(chǎn)‘ && c <= ‘z‘)
        c += ‘A‘ - ‘a(chǎn)‘;
    return c;
}

在很多C實現(xiàn)中,為了減少比實際計算還要多的調(diào)用開銷,,通常將其實現(xiàn)為宏:

#define toupper(c) ((c) >= ‘a(chǎn)‘ && (c) <= ‘z‘ ? (c) + (‘A‘ - ‘a(chǎn)‘) : (c))

很多時候這確實比函數(shù)要快,。然而,當你試著寫toupper(*p++)時,,會出現(xiàn)奇怪的結(jié)果,。

    另一個需要注意的地方是使用宏可能會產(chǎn)生巨大的表達式。例如,,繼續(xù)考慮max()的定義:

#define max(a, b) ((a) > (b) ? (a) : (b))

假設(shè)我們這個定義來查找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 宏不是類型定義
    宏的一個通常的用途是保證不同地方的多個事物具有相同的類型:

#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;

這允許程序員可以通過只改變程序中的一行就能改變a,、b和c的類型,,盡管a、b和c可能聲明在很遠的不同地方,。

    使用這樣的宏定義還有著可移植性的優(yōu)勢——所有的C編譯器都支持它,。很多C編譯器并不支持另一種方法:

typedef struct foo FOOTYPE;

這將FOOTYPE定義為一個與struct foo等價的新類型,。

    這兩種為類型命名的方法可以是等價的,但typedef更靈活一些,。例如,,考慮下面的例子:

#define T1 struct foo *
typedef struct foo * T2;

這兩個定義使得T1和T2都等價于一個struct foo的指針。但看看當我們試圖在一行中聲明多于一個變量的時候會發(fā)生什么:

T1 a, b;
T2 c, d;

第一個聲明被擴展為:

struct foo * a, b;

這里a被定義為一個結(jié)構(gòu)指針,,但b被定義為一個結(jié)構(gòu)(而不是指針),。相反,第二個聲明中c和d都被定義為指向結(jié)構(gòu)的指針,,因為T2的行為好像真正的類型一樣,。

7 可移植性缺陷
    C被很多人實現(xiàn)并運行在很多機器上。這也正是在一個地方寫的C程序應(yīng)該能夠很容易地轉(zhuǎn)移到另一個編程環(huán)境中去的原因,。

    然而,,由于有很多的實現(xiàn)者,它們并不和其他人交流,。此外,,不同的系統(tǒng)有不同的需求,因此一臺機器上的C實現(xiàn)和另一臺上的多少會有些不同,。

    由于很多早期的C實現(xiàn)都關(guā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)境中的用戶都有用的程序就必須知道很多這些細微的差別。

7.1 一個名字中都有什么,?
    一些C編譯器將一個標識符中的所有字符視為簽名,。而另一些在存貯標識符是會忽略一個極限之外的所有字符。C編譯器產(chǎn)生的目標程序同將要被加載器進行處理以訪問庫中的子程序,。加載器對于它們能夠處理的名字通常應(yīng)用自己的約束,。

    一個常見的加載器約束是所有的外部名字必須只能是大寫的。面對這樣的加載器約束,,C實現(xiàn)者會強制要求所有的外部名字都是大寫的,。這種約束在C語言參考手冊中第2.1節(jié)由所描述。

一個標識符是一個字符和數(shù)字序列,,第一個字符必須是一個字母,。下劃線_算作字母,。大寫字母和小寫字母是不同的。只有前八個字符是簽名,,但可以使用更多的字符,。可以被多種匯編器和加載器使用的外部標識符,,有著更多的限制:

    這里,,參考手冊中繼續(xù)給出了一些例子如有些實現(xiàn)要求外部標識符具有單獨的大小寫格式、或者少于八個字符,、或者二者都有,。

    正因為所有這些,在一個希望可以移植的程序中小心地選擇標識符是很重要的,。為兩個 子程序選擇print_fields和print_float這樣的名字不是個好辦法,。

    考慮下面這個顯著的函數(shù):

char *Malloc(unsigned n) {
    char *p, *malloc();
    p = malloc(n);
    if(p == NULL)
        panic("out of memory");
    return p;
}

    這個函數(shù)是保證耗盡內(nèi)存而不會導致沒有檢測的一個簡單的辦法。程序員可以通過調(diào)用Mallo()來代替malloc(),。如果malloc()不幸失敗,,將調(diào)用panic()來顯示一個恰當?shù)腻e誤消息并終止程序。

    然而,,考慮當該函數(shù)用于一個忽略大小寫區(qū)別的系統(tǒng)中時會發(fā)生什么,。這時,名字malloc和Malloc是等價的,。換句話說,,庫函數(shù)malloc()被上面的Malloc()函數(shù)完全取代了,當調(diào)用malloc()時它調(diào)用的是它自己,。顯然,,其結(jié)果就是第一次嘗試分配內(nèi)存就會陷入一個遞歸循環(huán)并隨之發(fā)生混亂。但在一些能夠區(qū)分大小寫的實現(xiàn)中這個函數(shù)還是可以工作的,。

7.2 一個整數(shù)有多大,?
    C為程序員提供三種整數(shù)尺寸:普通、短和長,,還有字符,,其行為像一個很小的整數(shù)。C語言定義對各種整數(shù)的大小不作任何保證:

整數(shù)的四種尺寸是非遞減的,。
普通整數(shù)的大小要足夠存放任意的數(shù)組下標,。
字符的大小應(yīng)該體現(xiàn)特定硬件的本質(zhì)。
    許多現(xiàn)代機器具有8位字符,,不過還有一些具有7位獲9位字符,。因此字符通常是7、8或9位,。

    長整數(shù)通常至少32位,,因此一個長整數(shù)可以用于表示文件的大小,。

    普通整數(shù)通常至少16位,因為太小的整數(shù)會更多地限制一個數(shù)組的最大大小,。

    短整數(shù)總是恰好16位,。

    在實踐中這些都意味著什么?最重要的一點就是別指望能夠使用任何一個特定的精度,。非正式情況下你可以假設(shè)一個短整數(shù)或一個普通整數(shù)是16位的,,而一個長整數(shù)是32位的,但并不保證總是會有這些大小,。你當然可以用普通整數(shù)來壓縮表大小和下標,,但當一個變量必須存放一個一千萬的數(shù)字的時候呢?

    一種更可移植的做法是定義一個“新的”類型:

typedef long tenmil;

現(xiàn)在你就可以使用這個類型來聲明一個變量并知道它的寬度了,,最壞的情況下,,你也只要改變這個單獨的類型定義就可以使所有這些變量具有正確的類型。

7.3 字符是帶符號的還是無符號的,?
    很多現(xiàn)代計算機支持8位字符,,因此很多現(xiàn)代C編譯器將字符實現(xiàn)為8位整數(shù)。然而,,并不是所有的編譯器都按照同將的方式解釋這些8位數(shù),。

    這些問題在將一個char制轉(zhuǎn)換為一個更大的整數(shù)時變得尤為重要。對于相反的轉(zhuǎn)換,,其結(jié)果卻是定義良好的:多余的位被簡單地丟棄掉,。但一個編譯器將一個char轉(zhuǎn)換為一個int卻需要作出選擇:將char視為帶符號量還是無符號量?如果是前者,,將char擴展為int時要復制符號位,;如果是后者,則要將多余的位用0填充,。

    這個決定的結(jié)果對于那些在處理字符時習慣將高位置1的人來說非常重要,。這決定著8位的字符范圍是從-128到127還是從0到255。這又影響著程序員對哈希表和轉(zhuǎn)換表之類的東西的設(shè)計,。

    如果你關(guān)心一個字符值最高位置一時是否被視為一個負數(shù),,你應(yīng)該顯式地將它聲明為unsigned char,。這樣就能保證在轉(zhuǎn)換為整數(shù)時是基0的,,而不像普通char變量那樣在一些實現(xiàn)中是帶符號的而在另一些實現(xiàn)中是無符號的。

    另外,,還有一種誤解是認為當c是一個字符變量時,,可以通過寫(unsigned)c來得到與c等價的無符號整數(shù)。這是錯誤的,,因為一個char值在進行任何操作(包括轉(zhuǎn)換)之前轉(zhuǎn)換為int,。這時c會首先轉(zhuǎn)換為一個帶符號整數(shù)在轉(zhuǎn)換為一個無符號整數(shù),,這會產(chǎn)生奇怪的結(jié)果。

    正確的方法是寫(unsigned char)c,。

7.4 右移位是帶符號的還是無符號的,?
    這里再一次重復:一個關(guān)心右移操作如何進行的程序最好將所有待移位的量聲明為無符號的。

7.5 除法如何舍入,?
    假設(shè)我們用b除a得到商為q余數(shù)為r:

q = a / b;
r = a % b;

我們暫時假設(shè)b > 0,。

    我們期望a、b,、q和r之間有什么關(guān)聯(lián),?

最重要的,我們期望q * b + r == a,,因為這是對余數(shù)的定義,。
如果a的符號發(fā)生改變,我們期望q的符號也發(fā)生改變,,但絕對值不變,。
我們希望保證r >= 0且r < b。例如,,如果余數(shù)將作為一個哈希表的索引,,它必須要保證總是一個有效的索引。
    這三點清楚地描述了整數(shù)除法和求余操作,。不幸的是,,它們不能同時為真。

    考慮3 / 2,,商1余0,。這滿足第一點。而-3 / 2的值呢,?根據(jù)第二點,,商應(yīng)該是-1,但如果是這樣的話,,余數(shù)必須也是-1,,這違反了第三點?;蛘?,我們可以通過將余數(shù)標記為1來滿足第三點,但這時根據(jù)第一點商應(yīng)該是-2,。這又違反了第二點,。

    因此C和其他任何實現(xiàn)了整數(shù)除法舍入的語言必須放棄上述三個原則中的至少一個。

    很多程序設(shè)計語言放棄了第三點,,要求余數(shù)的符號必須和被除數(shù)相同,。這可以保證第一點和第二點,。很多C實現(xiàn)也是這樣做的。

    然而,,C語言的定義只保證了第一點和|r| < |b|以及當a >= 0且b > 0時r >= 0,。 這比第二點或第三點的限制要小, 實際上有些編譯器滿足第二點或第三點,,但不太常見(如一個實現(xiàn)可能總是向著距離0最遠的方向進行舍入),。

    盡管有些時候不需要靈活性,C語言還是足夠可以讓我們令除法完成我們所要做的,、提供我們所想知道的,。例如,假設(shè)我們有一個數(shù)n表示一個標識符中的字符的一些函數(shù),,并且我們想通過除法得到一個哈希表入口h,,其中0 <= h <= HASHSIZE。如果我們知道n是非負的,,我們可以簡單地寫:

h = n % HASHSIZE;

然而,,如果n有可能是負的,這樣寫就不好了,,因為h可能也是負的,。然而,我們知道h > -HASHSIZE,,因此我們可以寫:

h = n % HASHSIZE;
if(n < 0)
    h += HASHSIZE;

    同樣,,將n聲明為unsigned也可以。

7.6 一個隨機數(shù)有多大,?
    這個尺寸是模糊的,,還受庫設(shè)計的影響。在PDP-11[10]機器上運行的僅有的C實現(xiàn)中,,有一個稱為rand()的函數(shù)可以返回一個(偽)隨機非負整數(shù),。PDP-11中整數(shù)長度包括符號位是16位,因此rand()返回一個0到215-1之間的整數(shù),。

    當C在VAX-11上實現(xiàn)時,,整數(shù)的長度變?yōu)?2位長。那么VAX-11上的rand()函數(shù)返回值范圍是什么呢,?

    對于這個系統(tǒng),,加利福尼亞大學的人認為rand()的返回值應(yīng)該涵蓋所有可能的非負整數(shù),因此它們的rand()版本返回一個0到231-1之間的整數(shù),。

    而AT&T的人則覺得如果rand()函數(shù)仍然返回一個0到215之間的值 則可以很容易地將PDP-11中期望rand()能夠返回一個小于215的值的程序移植到VAX-11上,。

    因此,,現(xiàn)在還很難寫出不依賴實現(xiàn)而調(diào)用rand()函數(shù)的程序,。

7.7 大小寫轉(zhuǎn)換
    toupper()和tolower()函數(shù)有著類似的歷史,。他們最初都被實現(xiàn)為宏:

#define toupper(c) ((c) + ‘A‘ - ‘a(chǎn)‘)
#define tolower(c) ((c) + ‘A‘ - ‘a(chǎn)‘)

當給定一個小寫字母作為輸入時,toupper()將產(chǎn)生相應(yīng)的大寫字母,。tolower()反之,。這兩個宏都依賴于實現(xiàn)的字符集,它們需要所有的大寫字母和對應(yīng)的小寫字母之間的差別都是常數(shù)的,。這個假設(shè)對于ASCII和EBCDIC字符集來說都是有效的,,可能不是很危險,因為這些不可移植的宏定義可以被封裝到一個單獨的文件中并包含它們,。

    這些宏確實有一個缺陷,,即:當給定的東西不是一個恰當?shù)淖址鼤祷乩?。因此,,下面這個通過使用這些宏來將一個文件轉(zhuǎn)為小寫的程序是無法工作的:

int c;
while((c = getchar()) != EOF)
    putchar(tolower(c));

我們必須寫:

int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);

    就這一點,AT&T中的UNIX開發(fā)組織提醒我們,,toupper()和tolower()都是事先經(jīng)過一些適當?shù)膮?shù)進行測試的,。考慮這樣重寫這些宏:

#define toupper(c) ((c) >= ‘a(chǎn)‘ && (c) <= ‘z‘ ? (c) + ‘A‘ - ‘a(chǎn)‘ : (c))
#define tolower(c) ((c) >= ‘A‘ && (c) <= ‘Z‘ ? (c) + ‘a(chǎn)‘ - ‘A‘ : (c))

但要知道,,這里c的三次出現(xiàn)都要被求值,,這會破壞如toupper(*p++)這樣的表達式。因此,,可以考慮將toupper()和tolower()重寫為函數(shù),。toupper()看起來可能像這樣:

int toupper(int c) {
    if(c >= ‘a(chǎn)‘ && c <= ‘z‘)
        return c + ‘A‘ - ‘a(chǎn)‘;
    return c;
}

tolower()類似。

    這個改變帶來更多的問題,,每次使用這些函數(shù)的時候都會引入函數(shù)調(diào)用開銷,。我們的英雄認為一些人可能不愿意支付這些開銷,因此他們將這個宏重命名為:

#define _toupper(c) ((c) + ‘A‘ - ‘a(chǎn)‘)
#define _tolower(c) ((c) + ‘a(chǎn)‘ - ‘A‘)

這就允許用戶選擇方便或速度,。

    這里面其實只有一個問題:伯克利的人們和其他的C實現(xiàn)者并沒有跟著這么做,。 這意味著一個在AT&T系統(tǒng)上編寫的使用了toupper()或tolower()的程序,如果沒有為其傳遞正確大小寫字母參數(shù),,在其他C實現(xiàn)中可能不會正常工作,。

    如果不知道這些歷史,可能很難對這類錯誤進行跟蹤,。

7.8 先釋放,,再重新分配
    很多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)存,。

    也許有人會想,真相真是有點微妙啊,。下面是System V接口定義中出現(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不可用,。

    不用說,這種技術(shù)是不推薦的,,因為不是所有C實現(xiàn)都能在內(nèi)存被釋放后將它的內(nèi)容保留足夠長的時間,。然而,第七版的手冊遺留了一個未聲明的問題:realloc()的原始實現(xiàn)實際上是必須要先釋放再重新分配的,。出于這個原因,,一些C程序都是先釋放內(nèi)存再重新分配的,而當這些程序移植到其他實現(xiàn)中時就會出現(xiàn)問題,。

7.9 可移植性問題的一個實例
    讓我們來看一個已經(jīng)被很多人在很多時候解決了的問題,。下面的程序帶有兩個參數(shù):一個長整數(shù)和一個函數(shù)(的指針)。它將整數(shù)轉(zhuǎn)換位十進制數(shù),,并用代表其中每一個數(shù)字的字符來調(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‘);
}

    這個程序非常簡單。首先檢查n是否為負數(shù),;如果是,,則打印一個符號并將n變?yōu)檎龜?shù)。接下來,測試是否n >= 10,。如果是,,則它的十進制表示中包含兩個或更多個數(shù)字,因此我們遞歸地調(diào)用printnum()來打印除最后一個數(shù)字外的所有數(shù)字,。最后,我們打印最后一個數(shù)字,。

    這個程序——由于它的簡單——具有很多可移植性問題,。首先是將n的低位數(shù)字轉(zhuǎn)換成字符形式的方法。用n % 10來獲取低位數(shù)字的值是好的,,但為它加上‘0‘來獲得相應(yīng)的字符表示就不好了,。這個加法假設(shè)機器中順序的數(shù)字所對應(yīng)的字符數(shù)順序的,沒有間隔,,因此‘0‘ + 5和‘5‘的值是相同的,,等等。盡管這個假設(shè)對于ASCII和EBCDIC字符集是成立的,,但對于其他一些機器可能不成立,。避免這個問題的方法是使用一個表:

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)(‘-‘);
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}

    另一個問題發(fā)生在當n < 0時。這時程序會打印一個負號并將n設(shè)置為-n,。這個賦值會發(fā)生溢出,,因為在使用2的補碼的機器上通常能夠表示的負數(shù)比正數(shù)要多。例如,,一個(長)整數(shù)有k位和一個附加位表示符號,,則-2k可以表示而2k卻不能。

    解決這一問題有很多方法,。最直觀的一種是將n賦給一個unsigned long值,。然而,一些C便一起可能沒有實現(xiàn)unsigned long,,因此我們來看看沒有它怎么辦,。

    在第一個實現(xiàn)和第二個實現(xiàn)的機器上,改變一個正整數(shù)的符號保證不會發(fā)生溢出,。問題僅出在改變一個負數(shù)的符號時,。因此,我們可以通過避免將n變?yōu)檎龜?shù)來避免這個問題,。

    當然,,一旦我們打印了負數(shù)的符號,我們就能夠?qū)⒇摂?shù)和正數(shù)視為是一樣的,。下面的方法就強制在打印符號之后n為負數(shù),,并且用負數(shù)值完成我們所有的算法。如果我們這么做,我們就必須保證程序中打印符號的部分只執(zhí)行一次,;一個簡單的方法是將這個程序劃分為兩個函數(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ù)是否為負數(shù),;如果是的話則打印一個符號。否則,,它以n的負絕對值來調(diào)用printneg(),。我們同時改變了printneg()的函數(shù)體來適應(yīng)n永遠是負數(shù)或零這一事實。

    我們得到什么,?我們使用n / 10和n % 10來獲取n的前導數(shù)字和結(jié)尾數(shù)字(經(jīng)過適當?shù)姆栕儞Q),。調(diào)用整數(shù)除法的行為在其中一個操作數(shù)為負的時候是實現(xiàn)相關(guān)的。因此,,n % 10有可能是正的,!這時,-(n % 10)是正數(shù),,將會超出我們的數(shù)字字符數(shù)組的末尾,。

    為了解決這一問題,我們建立兩個臨時變量來存放商和余數(shù),。作完除法后,,我們檢查余數(shù)是否在正確的范圍內(nèi),如果不是的話則調(diào)整這兩個變量,。printnum()沒有改變,,因此我們只列出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]);
}

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,,謹防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報,。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多