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才能夠正常地增,直到循環結束。
相關文章
相關標籤/搜索