C語言缺陷與陷阱

C言像一把雕刻刀,利,並且在技手中非常有用。和任何利的工具一C到那些不能掌握它的人。本文介C害粗心的人的方法,以及如何避免害。
第一部分研究了當程序被劃分爲記生的問題第二部分繼續研究了當程序的號被編譯聲明、表達式和會出問題第三部分研究了由多個部分成、分別編譯定到一起的C程序。第四部分理了概念上的解:當一個程序具體生的事情。第五部分研究了我的程序和它所使用的常用系。在第六部分中,我注意到了我所寫的程序也並不是我所運行的程序;預處理器將首先運行。最後,第七部分討論了可移植性問題:一個能在一個實現中運行的程序無法在另一個實現中運行的原因。
法分析器(lexical analyzer檢查組成程序的字符序列,並將它劃分爲記號(token)一個號是一個由一個或多個字符構成的序列,它在言被編譯時具有一個(相地)一的意
C程序被兩次劃分爲記首先是預處理器取程序它必須對程序號劃分以發現標識宏的標識符。通過對每個宏行求來替調最後,經過宏替的程序又被集成字符流送給編譯器。編譯器再第二次將個流劃分爲記號。
1.1= 不是 ==C是用=表示賦值而用==表示比是因爲賦值率要高於比,因此其分配更短的符號。C賦值視爲一個運算符,因此可以很容易地寫出多重賦值(如a = b = c),並且可以將賦值嵌入到一個大的表達式中。
C言參考手冊明瞭如何決定:如果入流到一個定的字符串止已識別爲記號,則應該包含下一個字符以成能構成號的最的字符串” “子串原
    賦值運算符如+=實際上是兩個號。因此,
a + /* strange */ = 1
a += 1
是一個意思。看起來像一個獨的號而實際上是多個號的只有一個特例。特地,
p - > a
是不合法的。它和
p -> a
不是同義詞
另一方面,有些老式編譯是將=+視爲一個獨的號並且和+=是同義詞
引號中的一個字符只是寫整數的另一方法。個整數是定的字符在實現照序列中的一個對應。而一個包在雙引號中的字符串,只是寫一個有雙引號之的字符和一個附加的二值爲零的字符所初始化的一個無名數的指的一種簡短方法。
使用一個指來代替一個整數通常會得到一個警告消息(反之亦然),使用雙引號來代替引號也會得到一個警告消息(反之亦然)。但於不檢查參數型的編譯器卻除外。
由於一個整數通常足大,以至於能放下多個字符,一些C編譯器允在一個字符常量中存放多個字符。意味着用'yes'代替"yes"將不會被發現。後者意味着包含yes和一個空字符的四個連續器區域中的第一個的地址,而前者意味着在一些實現式中表示由字符yes合構成的一個整數兩者之的任何一致性都屬巧合。
理解號是如何構成聲明、表達式、句和程序的。
C量聲明都具有兩個部分:一個型和一具有特定格式的、期望用來對該類型求的表達式。
float *g(), (*h)();
表示*g()(*h)()都是float表達式。由於()*定得更密,*g()*(g())表示同西:g是一個返回指float的函數,而h是一個指向返回float的函數的指
當我知道如何聲明一個型的量以後,就能很容易地寫出一個型的模型(cast):只要量名和分號並將所有的西包在一對圓括號中即可。
float *g();
聲明g是一個返回float的函數,所以(float *())就是它的模型。
(*(void(*)())0)();硬件會調用地址0的子程序
(*0)(); 這樣並不行,因*運算符要求必有一個指它的操作數。另外,個操作數必是一個指向函數的指,以保*果可以被調用。需要將0轉換爲一個可以描述指向一個返回void的函數的指型。(Void(*)())0
裏,我解決問題時沒有使用typedef聲明。通使用它,我可以更清晰地解決問題
typedef void (*funcptr)();// typedef funcptr void (*)();指向返回void的函數的指針
(*(funcptr)0)();
//調用地址爲0處的子程序
定得最密的運算符並不是真正的運算符:下、函數調用和選擇些都與左關聯
接下來是一元運算符。它具有真正的運算符中的最高。由於函數調用比一元運算符定得更密,你必(*p)()調p指向的函數;*p()表示p是一個返回一個指的函數。轉換是一元運算符,並且和其他一元運算符具有相同的。一元運算符是右合的,因此*p++表示*(p++),而不是(*p)++
在接下來是真正的二元運算符。其中數學運算符具有最高的,然後是移位運算符、系運算符、邏輯運算符、賦值運算符,最後是條件運算符。需要住的兩個重要的西是:
1.    所有的邏輯運算符具有比所有系運算符都低的
2.    移位運算符比系運算符定得更密,但又不如數學運算符。
乘法、除法和求餘具有相同的,加法和減法具有相同的,以及移位運算符具有相同的
有就是六個系運算符並不具有相同的==!=比其他系運算符要低。
邏輯運算符中,沒有任何兩個具有相同的。按位運算符比所有序運算符定得都密,每種與運算符都比相的或運算符定得更密,並且按位異或(^)運算符介於按位與和按位或之
    三元運算符的比我提到的所有運算符的都低。
個例子還說明瞭賦值運算符具有比條件運算符更低的是有意的。另外,所有的賦值運算符具有相同的並且是自右至左合的
具有最低的是逗號運算符。賦值是另一運算符,通常具有混合的
或者是一個空句,無任何效果;或者編譯器可能提出一個斷消息,可以方便除去掉它。一個重要的區是在必跟有一個句的ifwhile句中。另一個因分號引起巨大不同的地方是函數定前面的構聲明的末尾下面的程序片段:
struct foo {
    int x;
}

f() {
    ...
}
挨着f的第一個}後面失了一個分號。它的效果是聲明瞭一個函數f,返回值類型是struct foo構成了函數聲明的一部分。如果裏出了分號,f將被定義爲具有默的整型返回[5]
C中的case標籤是真正的標籤:控制流程可以無限制地入到一個case標籤中。
    看看另一形式,假C程序段看起來更像Pascal
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
並且假color2則該程序將打印yellowblue,因控制自然地入到下一個printf()調用。
既是Cswitch句的點又是它的弱點。它是弱點,是因很容易忘一個break句,從而致程序出現隱晦的異常行它是點,是因故意去掉break句,可以很容易實現其他方法實現的控制構。尤其是在一個大型的switch句中,我們經發現對一個case理可以化其他一些特殊的理。
和其他程序設計語言不同,C要求一個函數調用必有一個參數列表,但可以沒有參數。因此,如果f是一個函數,
f();
就是對該函數調用的句,而
f;
也不做。它會作函數地址被求,但不會調用它[6]
一個else是與其最近的if關聯
一個C程序可能有很多部分成,它被分別編譯,並由一個通常稱爲連接器、編輯器或加器的程序定到一起。由於編譯器一次通常只能看到一個文件,因此它無法檢測到需要程序的多個源文件的內容才能發現錯誤
你有一個C程序,被劃分兩個文件。其中一個包含如下聲明:
int n;
而令一個包含如下聲明:
long n;
不是一個有效的C程序,因一些外部名稱在兩個文件中被聲明不同的型。然而,很多實現檢測不到錯誤,因爲編譯器在編譯其中一個文件並不知道另一個文件的內容。因此,檢查類型的工作只能由接器(或一些工具程序如lint)來完成;如果操作系接器不能識別數據型,C編譯器也沒法多地制它。
    個程序運行時實際生什有很多可能性:
1.    實現夠聰明,能夠檢測型衝突。會得到一個斷消息,n在兩個文件中具有不同的型。
2.    你所使用的實現intlong視爲相同的型。典型的情況是機器可以自然地32位運算。在這種情況下你的程序或工作,好象你兩次都將量聲明long(或int)。這種程序的工作屬偶然。
3.    n的兩個例需要不同的存,它以某方式共享存區,即其中一個的賦值對另一個也有效。可能生,例如,編譯器可以將int安排在long的低位。不論這是基於系是基於機器的,這種程序的運行同是偶然。
4.    n的兩個例以另一方式共享存區,即其中一個賦值的效果是另一個以不同的。在這種情況下,程序可能失
這種情況生的一個例子出奇地繁。程序的某一個文件包含下面的聲明:
char filename[] = "etc/passwd";
而另一個文件包含這樣的聲明:
char *filename;
    儘管在某些境中數和指的行非常相似,但它是不同的。在第一個聲明中,filename是一個字符數的名字。儘管使用數的名字可以生數第一個元素的指,但個指只有在需要的候才生並且不會持。在第二個聲明中,filename是一個指的名字。個指可以指向程序員讓它指向的任何地方。如果程序沒有一個,它將具有一個默0NULL)([]實際上,在C中一個初始化的指通常具有一個隨機的是很危的!)。
    兩個聲明以不同的方式使用存區,它不可能共存。
    避免這種類型衝突的一個方法是使用像lint這樣的工具(如果可以的)。了在一個程序的不同編譯單元之間檢查類型衝突,一些程序需要一次看到其所有部分。典型的編譯器無法完成,但lint可以。
    避免該問題的另一方法是將外部聲明放到包含文件中這時,一個外部象的一次[7]
    一些C運算符以一已知的、特定的其操作數行求。但另一些不能。例如,考下面的表達式:
a < b && c < d
C言定義規a < b首先被求。如果a小於bc < d須緊接着被求算整個表達式的。但如果a大於或等於bc < d根本不會被求
a < b編譯ab的求就會有一個先後。但在一些機器上,它是並行行的。
C中只有四個運算符&&||?:,指定了求值順序。&&||最先的操作數行求,而右的操作數只有在需要的候才行求。而?:運算符中的三個操作數:abc,最先a行求,之後僅對bc中的一個行求取決於a,運算符首先的操作數行求,然後拋棄它的的操作數行求[8]
C中所有其它的運算符操作數的求值順序都是未定的。事上,賦值運算符不值順序做出任何保
    出於個原因,下面這種將數x中的前n個元素制到數y中的方法是不可行的:
i = 0;
while(i < n)
    y[i] = x[i++];
其中的問題y[i]的地址並不保i之前被求。在某些實現中,是可能的;但在另一些實現中卻不可能。另一情況出於同的原因會失
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];
    在很多言中,具有n個元素的數其元素的號和它的下是從1n對應的。但在C中不是這樣
個具有n個元素的C中沒有下標爲n的元素,其中的元素的下是從0n - 1。因此從其它C言的程序員應該小心地使用數
int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;
    下面的程序段由於兩個原因會失
double s;
s = sqrt(2);
printf("%g/n", s);
    第一個原因是sqrt()需要一個double它的參數,但沒有得到。第二個原因是它返回一個double但沒有這樣聲名。改正的方法只有一個:
double s, sqrt();
s = sqrt(2.0);
printf("%g/n", s);
C中有兩個簡單規則控制着函數參數的轉換(1)int短的整型被轉換爲int(2)double短的浮點轉換爲double。所有的其它不被轉換確保函數參數型的正確性是程序任。
因此,一個程序如果想使用如sqrt()這樣接受一個double型參數的函數,就必須僅傳遞給floatdouble型的參數。常數2是一個int,因此其型是錯誤的。
    當一個函數的被用在表達式中,其會被自轉換爲適當的型。然而,了完成個自動轉換編譯器必知道函數實際返回的型。沒有更聲名的函數被假返回int,因此聲名這樣的函數並不是必的。然而,sqrt()返回double,因此在成功使用它之前必要聲名。
裏有一個更加壯的例子:
main() {
    int i;
    char c;
    for(i = 0; i < 5; i++) {
        scanf("%d", &c);
        printf("%d", i);
    }
    printf("/n");
}
    表面上看,個程序從入中取五個整數並向出寫入0 1 2 3 4實際上,它並不這麼做。譬如在一些編譯器中,它的0 0 0 0 0 1 2 3 4
    ?因c的聲名是char而不是int。當你令scanf()取一個整數,它需要一個指向一個整數的指。但裏它得到的是一個字符的指。但scanf()並不知道它沒有得到它所需要的:它將入看作是一個指向整數的指並將一個整數存到那裏。由於整數佔用比字符更多的內存,這樣做會影響到c附近的內存。
    c附近確切是什編譯器的事;在這種情況下有可能是i的低位。因此,當向c入一個i就被置零。當程序最後到達文件scanf()不再嘗試c中放入新i纔可以正常地增,直到循環結束。
 
C程序通常將一個字符串轉換爲一個以空字符尾的字符數。假有兩個這樣的字符串st,並且我想要將它們連一個獨的字符串r。我通常使用函數strcpy()strcat()來完成。下面這種的方法並不會工作:
char *r;
strcpy(r, s);
strcat(r, t);
是因r沒有被初始化指向任何地方。儘管r可能潛在地表示某一內存,但並不存在,直到你分配它。
    試試r分配一些內存:
char r[100];
strcpy(r, s);
strcat(r, t);
只有在st所指向的字符串不很大的候才能工作。不幸的是,C要求我們爲指定的大小是一個常數,因此無法確定r是否足大。然而,很多C實現帶有一個叫做malloc()函數,它接受一個數字並分配這麼多的內存。通常有一個函數稱strlen(),可以告一個字符串中有多少個字符:因此,我可以寫:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
    然而個例子會因兩個原因而失。首先,malloc()可能會耗盡內存,而個事件靜靜地返回一個空指來表示。
    其次,更重要的是,malloc()並沒有分配足的內存。一個字符串是以一個空字符束的。而strlen()函數返回其字符串參數中所包含字符的數量,但不包括尾的空字符。因此,如果strlen(s)ns需要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);
 法(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.(將全面的位用作不全面的位,或反之;如整體局部或局部整體、一般特殊或特殊一般,等等。)
住的是,制一個指並不能制它所指向的西
將一個整數轉換爲一個指果是實現的(implementation-dependent),除了一個例外。個例外是常數0,它可以保轉換爲一個與其它任何有效指都不相等的指通常這樣
#define NULL 0
但其效果是相同的。要住的一個重要的事情是,當用0針時它決不能被解除引用話說,當你將0賦給一個指針變量後,你就不能訪問它所指向的內存。不能這樣寫:
if(p == (char *)0) ...
也不能這樣寫:
if(strcmp(p, (char *)0) == 0) ...
strcmp()是通其參數來看內存地址的。
如果p是一個空指這樣寫也是無效的:
printf(p);
printf("%s", p);
C於整數操作的上溢或下溢定得非常明確。
    只要有一個操作數是無符號的,果就是無符號的,並且以2n模,其中n。如果兩個操作數都是符號的,則結果是未定的。
例如,假ab是兩個非整型量,你希望測試a + b是否溢出。一個明法是這樣的:
if(a + b < 0)
    complain();
通常,是不會工作的。
    一旦a + b生了溢出,果的任何注都是沒有意的。例如,在某些機器上,一個加法運算會將一個內部寄存器:正、、零或溢出。這樣的機器上,編譯器有將上面的例子實現爲首先將ab加在一起,然後檢查內部寄存器狀是否爲負。如果運算溢出,內部寄存器將於溢出狀測試會失
    使個特殊的測試成功的一個正確的方法是依於無符號算的良好定,即要在有符號和無符號之間進轉換
if((int)((unsigned)a + (unsigned)b) < 0)
    complain();
兩個原因會令使用移位運算符的人感到煩惱
1.    在右移運算中,空出的位是用0填充是用符號位填充?
2.    移位的數量允使用哪些數?
第一個問題的答案很簡單,但有實現的。如果要行移位的操作數是無符號的,會移入0。如果操作數是符號的,則實現決定是移入0是移入符號位。如果在一個右移操作中你很心空位,那unsigned來聲明量。這樣你就有空位被0
    第二個問題的答案同樣簡單:如果待移位的數n移位的數量必大於等於0並且格地小於n。因此,在一次獨的操作中不可能將所有的位從量中移出
例如,如果一個int32位,且n是一個int,寫n << 31n << 0是合法的,但n << 32n << -1是不合法的。
    注意,即使實現將符號移入空位,一個符號整數的右移運算和除以2的某次也不是等價的。一點,考(-1) >> 1是不可能0的。[注:(-1) / 2果是0]
下面的程序:
#include

main() {
    char c;
//int c

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    段程序看起來好像要將制到出。實際上,它並不完全會做些。
    原因是c被聲明字符而不是整數。意味着它將不能接收可能出的所有字符包括EOF
因此裏有兩可能性。有一些合法的入字符會cEOF相同的,有又會使c無法存放EOF。在前一情況下,程序會在文件的中停止制。在後一情況下,程序會陷入一個無限循
    實際上,存在着第三可能:程序會偶然地正確工作。C言參考手冊格地定了表達式
((c = getchar()) != EOF)
果。其6.1中聲明:
當一個較長的整數被轉換爲一個短的整數或一個char,它會被截去左;超出的位被簡單棄。
7.14聲明:
存在着很多賦值運算符,它都是從右至左合的。它都需要一個左的操作數,而賦值表達式的型就是其左的操作數的型。其就是已經賦過值的左操作數的
兩個條款的合效果就是必過丟getchar()果的高位,將其截短字符,之後個被截短的再與EOF行比。作爲這個比的一部分,c一個整數,或者採取將左的位用0填充,或者適當地採取符號展。
然而,一些編譯器並沒有正確地實現這個表達式。它getchar()的低幾位賦給c。但在cEOF的比中,它卻使用了getchar()這樣做的編譯器會使個事例程序看起來能正確地工作。
立即安排出的示通常比將其暫時保存在一大一起出要昂得多。因此,C實現通常允程序控制生多少出後在實際地寫出它
    個控制通常一個稱setbuf()函數。如果buf是一個具有適當大小的字符數
setbuf(stdout, buf);
將告I/O寫入到stdout中的出要以buf一個衝,並且等到buf滿了或程序直接調fflush()實際寫出。衝區的合適的大小在中定義爲BUFSIZ
因此,下面的程序解了通使用setbuf()講標制到出:
#include

main() {
    int c;

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

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    不幸的是,個程序是錯誤的,因一個微的原因。
    要知道毛病出在哪,我需要知道衝區最後一次刷新是在什麼時候。答案;主程序完成之後,將控制交回到操作系之前所行的清理的一部分。在刻,衝區已放了
    有兩方法可以避免問題
    首先,使用靜態緩衝區,或者將其式地聲明
static char buf[BUFSIZ];
或者將整個聲明移到主函數之外。
    另一可能的方法是動態地分配衝區並且從不放它:
char *malloc();
setbuf(stdout, malloc(BUFSIZ));
注意在後一情況中,不必檢查malloc()的返回,因如果它失了,會返回一個空指。而setbuf()可以接受一個空指其第二個參數,將使得stdout成非衝的。會運行得很慢,但它是可以運行的。
由於宏可以象函數那,有些程序就會將它們視爲等價的。因此,看下面的定
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏體中所有的括號。它了防止出ab有比>低的表達式的情況。
    一個重要的問題是,像max()這樣個操作數都會出兩次並且會被求兩次。因此,在個例子中,如果ab大,a就會被求兩次:一次是在比候,而另一次是在max()候。
    是低效的,錯誤
biggest = x[0];
i = 1;
while(i < n)
    biggest = max(biggest, x[i++]);
max()是一個真正的函數會正常地工作,但當max()是一個宏的候會失。譬如,假x[0]2x[1]3x[2]1。我來看看在第一次循環時生什賦值語句會被
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先,biggestx[i++]行比。由於i1x[1]3系是。其副作用是,i2
    由於系是x[i++]賦給biggest。然而,這時i2了,因此賦給biggestx[2],即1
避免問題的方法是保max()宏的參數沒有副作用:
biggest = x[0];
for(i = 1; i < n; i++)
    biggest = max(biggest, x[i]);
有一個危的例子是混合宏及其副作用是來自UNIX第八版的中putc()宏的定
#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc()的第一個參數是一個要寫入到文件中的字符,第二個參數是一個指向一個表示文件的內部數據構的指。注意第一個參數完全可以使用如*z++西,儘管它在宏中兩次出,但只會被求一次。而第二個參數會被求兩次(在宏體中,x了兩次,但由於它的兩次出在一個:的兩,因此在putc()的一個例中它之中有且有一個被求)。由於putc()中的文件參數可能有副作用,會出現問題。不,用手冊文檔中提到:由於putc()實現爲宏,其stream可能會具有副作用。特putc(c, *f++)不能正確地工作。但是putc(*c++, f)實現中是可以工作的。
有些C實現很不小心。例如,沒有人能正確putc(*c++, f)。另一個例子,考很多C中出toupper()函數。它將一個小寫字母轉換爲的大寫字母,而其它字符不。如果我所有的小寫字母和所有的大寫字母都是相的(大小寫之可能有所差距),我可以得到這樣的函數:
toupper(c) {
    if(c >= 'a' && c <= 'z')
        c += 'A' - 'a';
    return c;
}
在很多C實現中,了減少比實際計要多的調開銷,通常將其實現爲宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' - 'a') : (c))
很多比函數要快。然而,當你着寫toupper(*p++),會出奇怪的果。
    另一個需要注意的地方是使用宏可能會生巨大的表達式。例如,繼續max()的定
#define max(a, b) ((a) > (b) ? (a) : (b))
們這個定abcd中的最大。如果我直接寫:
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))))))
出奇的大。我可以通平衡操作數來使它短一些:
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;
好一些。
宏的一個通常的用途是保不同地方的多個事物具有相同的型:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;
程序可以通只改程序中的一行就能改abc型,儘管abc可能聲明在很的不同地方。
    使用這樣的宏定義還有着可移植性的優勢——所有的C編譯器都支持它。很多C編譯器並不支持另一方法:
typedef struct foo FOOTYPE;
FOOTYPE義爲一個與struct foo等價的新型。
    種爲類型命名的方法可以是等價的,但typedef更靈活一些。例如,考下面的例子:
#define T1 struct foo *
typedef struct foo * T2;
兩個定使得T1T2都等價於一個struct foo的指。但看看當我們試圖在一行中聲明多於一個量的候會生什
T1 a, b;
T2 c, d;
第一個聲明被
struct foo * a, b;
a被定義爲一個構指,但b被定義爲一個構(而不是指)。相反,第二個聲明中cd都被定義爲指向構的指,因T2的行好像真正的型一
今天,一個C程序如果想寫出於不同境中的用都有用的程序就必知道很多微的差
一個標識符是一個字符和數字序列,第一個字符必是一個字母。下劃_算作字母。大寫字母和小寫字母是不同的。只有前八個字符是名,但可以使用更多的字符。可以被多種彙編器和加器使用的外部標識符,有着更多的限制:
下面著的函數:
char *Malloc(unsigned n) {
    char *p, *malloc();
    p = malloc(n);
    if(p == NULL)
        panic("out of memory");
    return p;
}
    個函數是保耗盡內存而不會致沒有檢測的一個簡單法。程序可以通過調Mallo()來代替malloc()。如果malloc()不幸失,將調panic()示一個恰當的錯誤消息並止程序。
然而,考函數用於一個忽略大小寫區的系生什這時,名字mallocMalloc是等價的。話說函數malloc()被上面的Malloc()函數完全取代了,當調malloc()調用的是它自己。然,其果就是第一次嘗試分配內存就會陷入一個遞歸並隨之生混亂。但在一些能區分大小寫的實現個函數是可以工作的。
C程序提供三整數尺寸:普通、短和有字符,其行像一個很小的整數。C言定義對整數的大小不作任何保
1.    整數的四尺寸是非減的。
2.    普通整數的大小要足存放任意的數
3.    字符的大小應該特定硬件的本
代機器具有8位字符,不過還有一些具有79位字符。因此字符通常是789位。
    整數通常至少32位,因此一個整數可以用於表示文件的大小。
    普通整數通常至少16位,因太小的整數會更多地限制一個數的最大大小。
    短整數是恰好16位。
更可移植的做法是定一個新的型:
typedef long tenmil;
在你就可以使用型來聲明一個量並知道它的度了,最壞的情況下,你也只要改變這獨的型定就可以使所有量具有正確的型。
問題在將一個char轉換爲一個更大的整數時變得尤重要。於相反的轉換,其果卻是定良好的:多餘的位被簡單棄掉。但一個編譯器將一個char轉換爲一個int卻需要作出選擇:將char視爲帶符號量是無符號量?如果是前者,將charint制符號位;如果是後者,要將多餘的位用0填充。
個決定的於那些在理字符時習慣將高位置1的人來非常重要。決定着8位的字符範是從-128127是從0255又影響着程序員對哈希表和轉換表之西的設計
    如果你心一個字符最高位置一是否被視爲一個數,你應該顯式地將它聲明unsigned char這樣就能保轉換爲整數是基0的,而不像普通char量那在一些實現中是符號的而在另一些實現中是無符號的。
另外,有一種誤解是認爲c是一個字符,可以通(unsigned)c來得到與c等價的無符號整數。錯誤的,因一個char行任何操作(包括轉換)之前轉換爲int這時c會首先轉換爲一個符號整數再轉換爲一個無符號整數,生奇怪的果。
    正確的方法是寫(unsigned char)c
裏再一次重:一個心右移操作如何行的程序最好將所有待移位的量聲明無符號的。
ba得到商q餘數r
q = a / b;
r = a % b;
們暫時b > 0
1.    最重要的,我期望q * b + r == a,因爲這餘數的定
2.    如果a的符號生改,我期望q的符號也生改,但絕對值
3.    希望保r >= 0r < b。例如,如果餘數將作一個哈希表的索引,它必要保證總是一個有效的索引。
    三點清楚地描述了整數除法和求餘操作。不幸的是,它不能同時爲真。
3 / 2,商10(1)這滿足第一點。而-3 / 2呢?根據第二點,商應該-1,但如果是這樣,餘數必也是-1這違反了第三點。或者,我可以通將餘數標記爲1滿足第三點,但這時根據第一點商應該-2反了第二點。
因此C和其他任何實現了整數除法舍入的言必放棄上述三個原中的至少一個。
很多程序設計語言放棄了第三點,要求餘數的符號必和被除數相同。可以保第一點和第二點。很多C實現也是這樣做的。
 儘管有些候不需要靈活性,C是足可以令除法完成我所要做的、提供我所想知道的。例如,假有一個數n表示一個標識符中的字符的一些函數,並且我想通除法得到一個哈希表入口h,其中0 <= h <= HASHSIZE。如果我知道n是非的,我可以簡單地寫:
h = n % HASHSIZE;
然而,如果n有可能是的,這樣寫就不好了,因h可能也是的。然而,我知道h > -HASHSIZE,因此我可以寫:
h = n % HASHSIZE;
if(n < 0)
    h += HASHSIZE;
    ,將n聲明unsigned也可以。
個尺寸是模糊的,庫設計影響。在PDP-11[10]機器上運行的有的C實現中,有一個稱rand()的函數可以返回一個()隨機非整數。PDP-11中整數度包括符號位是16位,因此rand()返回一個0215-1的整數。
    CVAX-11實現時,整數的變爲32。那VAX-11上的rand()函數返回是什呢?
    個系,加利福尼大學的認爲rand()的返回值應該涵蓋所有可能的非整數,因此它rand()版本返回一個0231-1的整數。
    AT&T的人則覺得如果rand()函數仍然返回一個0215可以很容易地將PDP-11中期望rand()返回一個小於215的程序移植到VAX-11上。
    因此,寫出不依賴實現調rand()函數的程序。
toupper()tolower()函數有着似的史。他最初都被實現爲宏:
#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
些宏確有一個缺陷,即:當定的西不是一個恰當的字符,它會返回垃圾。因此,下面個通使用些宏來將一個文件轉爲小寫的程序是無法工作的:
int c;
while((c = getchar()) !=
EOF)
    putchar(tolower(c));
寫:
int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);
一點,AT&T中的UNIX開發組織提醒我toupper()tolower()都是事先經過一些適當的參數測試的。考慮這樣重寫些宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ?
(c) + 'a' - 'A' : (c))
但要知道,c的三次出都要被求會破壞如toupper(*p++)這樣的表達式。因此,可以考toupper()tolower()重寫函數。toupper()看起來可能像這樣
int toupper(int c) {
    if(c >= 'a' && c <= 'z')
        return c + 'A' - 'a';
    return c;
}
tolower()似。
    個改變帶來更多的問題次使用些函數的候都會引入函數調開銷。我的英雄認爲一些人可能不願意支付開銷,因此他個宏重命名
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
就允戶選擇方便或速度。
    裏面其只有一個問題:伯克利的人和其他的C實現者並沒有跟着這麼做。意味着一個在AT&T寫的使用了toupper()tolower()的程序,如果沒有傳遞正確大小寫字母參數,在其他C實現中可能不會正常工作。
    如果不知道史,可能很難對這類錯誤進行跟蹤。
很多C實現爲提供了三個內存分配函數:malloc()realloc()free()調malloc(n)返回一個指向有n個字符的新分配的內存的指個指可以由程序使用。free()傳遞一個指向由malloc()分配的內存的指可以使這塊內存得以再次使用。通一個指向已分配區域的指和一個新的大小調realloc()可以將這塊內存大或小到新尺寸,程中可能要制內存。
    有人會想,真相真是有點微妙啊。下面是System V接口定中出realloc()的描述:
realloc一個由ptr指向的size個字,並返回該塊(可能被移)的指在新舊尺寸中比小的一個尺寸之下的內容不會被改此外,包含了描述realloc()的另外一段:
如果在最後一次調mallocrealloccalloc放了ptr所指向的realloc依舊可以工作;因此,freemallocrealloc序可以利用malloc壓縮找策略。
因此,下面的代片段在UNIX第七版中是合法的:
free (p);
p = realloc(p, newsize);
    一特性保留在從UNIX第七版衍生出來的系中:可以先放一區域,然後再重新分配它。意味着,在些系放的內存中的內容在下一次內存分配之前可以保。因此,在些系中,我可以用下面這種奇特的思想來放一個表中的所有元素:
for(p = head; p != NULL; p = p->next)
    free((char *)p);
而不用擔心調free()p->next不可用。
不用這種是不推薦的,因不是所有C實現都能在內存被放後將它的內容保留足夠長時間。然而,第七版的手冊留了一個未聲明的問題realloc()的原始實現實際上是必要先放再重新分配的。出於個原因,一些C程序都是先放內存再重新分配的,而當些程序移植到其他實現就會出現問題
下面的程序有兩個參數:一個整數和一個函數(的指)。它將整數轉換位十制數,並用代表其中一個數字的字符來調定的函數。
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}
    個程序非常簡單。首先檢查n是否爲負數;如果是,打印一個符號並將n變爲正數。接下來,測試是否n >= 10。如果是,它的十製表示中包含兩個或更多個數字,因此我們遞歸調printnum()來打印除最後一個數字外的所有數字。最後,我打印最後一個數字。
    個程序——由於它的簡單——具有很多可移植性問題。首先是將n的低位數字轉換成字符形式的方法。用n % 10取低位數字的是好的,但它加上'0'得相的字符表示就不好了。個加法假機器中序的數字所對應的字符數序的,沒有隔,因此'0' + 5'5'是相同的,等等。儘管個假設對ASCIIEBCDIC字符集是成立的,但於其他一些機器可能不成立。避免問題的方法是使用一個表:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}
    另一個問題發生在當n < 0這時程序會打印一個號並將n-n賦值生溢出,因在使用2補碼的機器上通常能表示的數比正數要多。例如,一個()整數有k位和一個附加位表示符號,-2k可以表示而2k卻不能。
    解決問題有很多方法。最直的一是將n賦給一個unsigned long。然而,一些C便一起可能沒有實現unsigned long,因此我來看看沒有它怎麼辦
    在第一個實現和第二個實現的機器上,改一個正整數的符號保不會生溢出。問題僅出在改一個數的符號。因此,我可以通避免將n變爲正數來避免問題
    當然,一旦我打印了數的符號,我就能數和正數視爲是一的。下面的方法就制在打印符號之後n爲負數,並且用完成我所有的算法。如果我們這麼做,我就必程序中打印符號的部分只行一次;一個簡單的方法是將個程序劃分兩個函數:
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()在只檢查要打印的數是否爲負數;如果是的話則打印一個符號。否,它以n負絕對值調printneg()。我printneg()的函數體來適n數或零一事
    得到什?我使用n / 10n % 10n的前數字和尾數字(經過適當的符號變換)。調用整數除法的行在其中一個操作數爲負候是實現的。因此,n % 10有可能是正的!這時-(n % 10)數,將會超出我的數字字符數的末尾。
    瞭解決問題,我建立兩個臨時變量來存放商和餘數。作完除法後,我們檢查餘數是否在正確的範內,如果不是的話則調兩個量。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]);
}
    The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具威的C著作。它包含了一個秀的教程,面向那些熟悉其他高級語言程序設計的人,和一個參考手冊,簡潔地描述了整個言。儘管自1978年以來這門語生了不少化,書對於很多主仍然是個定時還包含了本文中多次提到的“C言參考手冊
    The C Puzzle Book》(Feuer, Prentice-Hall, 1982)是一本少的磨文法能力的收集了很多謎題(和答案),它的解決方法能夠測試讀C言精妙之的知
    C: A Referenct Manual》(Harbison and Steele, Prentice Hall 1984)是特意爲實現寫的一本參考料。其他人也會發現它是特有用的——他能從中參考細節
1.是基於圖書C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一個充,有趣的者可以它。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章