• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • C語言:陷阱和缺陷_2

    發表于:2007-05-26來源:作者:點擊數: 標簽:
    2 句法 缺陷 要理解C語言程序,僅了解構成它的記號是不夠的。還要理解這些記號是如何構成聲明、表達式、語句和程序的。盡管這些構成通常都是定義良好的,但這些定義有時候是有悖于直覺的或混亂的。 在這一節中,我們將著眼于一些不明顯句法構造。 2.1 理解聲
    2 句法缺陷

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

        在這一節中,我們將著眼于一些不明顯句法構造。


    2.1 理解聲明

        我曾經和一些人聊過天,他們那時在書寫在一個小型的微處理器上單機運行的C程序。當這臺機器的開關打開的時候,硬件會調用地址為0處的子程序。

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

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

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

        每個C變量聲明都具有兩個部分:一個類型和一組具有特定格式的期望用來對該類型求值的表達式。最簡單的表達式就是一個變量:

    float f, g;

    說明表達式f和g——在求值的時候——具有類型float。由于待求值的時表達式,因此可以自由地使用圓括號:

    float ((f));

    這表示((f))求值為float并且因此,通過推斷,f也是一個float。

        同樣的邏輯用在函數和指針類型。例如:

    float ff();

    表示表達式ff()是一個float,因此ff是一個返回一個float的函數。類似地,

    float *pf;

    表示*pf是一個float并且因此pf是一個指向一個float的指針。

        這些形式的組合聲明對表達式是一樣的。因此,

    float *g(), (*h)();

    表示*g()和(*h)()都是float表達式。由于()比*綁定得更緊密,*g()和*(g())表示同樣的東西:g是一個返回指float指針的函數,而h是一個指向返回float的函數的指針。

        當我們知道如何聲明一個給定類型的變量以后,就能夠很容易地寫出一個類型的模型(cast):只要刪除變量名和分號并將所有的東西包圍在一對圓括號中即可。因此,由于

    float *g();

    聲明g是一個返回float指針的函數,所以(float *())就是它的模型。

        有了這些知識的武裝,我們現在可以準備解決(*(void(*)())0)()了。 我們可以將它分為兩個部分進行分析。首先,假設我們有一個變量fp,它包含了一個函數指針,并且我們希望調用fp所指向的函數??梢赃@樣寫:

    (*fp)();

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

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

    (*0)();

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

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

    void (*fp)();

    因此,我們需要寫:

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

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

    (void(*)())0

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

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

    結尾處的分號用于將這個表達式轉換為一個語句。

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

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

    2.2 運算符并不總是具有你所想象的優先級

        假設有一個聲明了的常量FLAG是一個整數,其二進制表示中的某一位被置位(換句話說,它是2的某次冪),并且你希望測試一個整型變量flags該位是否被置位。通常的寫法是:

    if(flags & FLAG) ...

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

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

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

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

    這(偶爾)是可以的,如FLAG是1或0(?。┑臅r候,但對于其他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中的優先級。

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

        綁定得最緊密的運算符并不是真正的運算符:下標、函數調用和結構選擇。這些都與左邊相關聯。

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

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

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

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

    a < b == c < d

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

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

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

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

    a = b = c



    b = c; a = b;

    是等價的。

        具有最低優先級的是逗號運算符。這很容易理解,因為逗號通常在需要表達式而不是語句的時候用來替代分號。

        賦值是另一種運算符,通常具有混合的優先級。例如,考慮下面這個用于復制文件的循環:

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

    這個while循環中的表達式看起來像是c被賦以getc(in)的值,接下來判斷是否等于EOF以結束循環。不幸的是,賦值的優先級比任何比較操作都低,因此c的值將會是getc(in)和EOF比較的結果,并且會被拋棄。因此,“復制”得到的文件將是一個由值為1的字節流組成的文件。

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

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

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

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

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

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

    2.3 看看這些分號!

        C中的一個多余的分號通常會帶來一點點不同:或者是一個空語句,無任何效果;或者編譯器可能提出一個診斷消息,可以方便除去掉它。一個重要的區別是在必須跟有一個語句的if和while語句中??紤]下面的例子:

    if(x[i] > big);
        big = x[i];

    這不會發生編譯錯誤,但這段程序的意義與:

    if(x[i] > big)
        big = x[i];

    就大不相同了。第一個程序段等價于:

    if(x[i] > big) { }
    big = x[i];

    也就是等價于:

    big = x[i];

    (除非x、i或big是帶有副作用的宏)。

        另一個因分號引起巨大不同的地方是函數定義前面的結構聲明的末尾[譯注:這句話不太好聽,看例子就明白了]??紤]下面的程序片段:

    struct foo {
        int x;
    }

    f() {
        ...
    }

    在緊挨著f的第一個}后面丟失了一個分號。它的效果是聲明了一個函數f,返回值類型是struct foo,這個結構成了函數聲明的一部分。如果這里出現了分號,則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

        這兩個程序片斷都作相同的事情:根據變量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語句的優點又是它的弱點。說它是弱點,是因為很容易忘記一個break語句,從而導致程序出現隱晦的異常行為。說它是優點,是因為通過故意去掉break語句,可以很容易實現其他方法難以實現的控制結構。尤其是在一個大型的switch語句中,我們經常發現對一個case的處理可以簡化其他一些特殊的處理。

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

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

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

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

    2.5 函數調用

        和其他程序設計語言不同,C要求一個函數調用必須有一個參數列表,但可以沒有參數。因此,如果f是一個函數,

    f();

    就是對該函數進行調用的語句,而

    f;

    什么也不做。它會作為函數地址被求值,但不會調用它腳注[6]。

    2.6 懸掛else問題

        在討論任何語法缺陷時我們都不會忘記提到這個問題。盡管這一問題不是C語言所獨有的,但它仍然傷害著那些有著多年經驗的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的地址作為參數調用f()。

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

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

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

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


    腳注
        1. 本文是基于圖書《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一個擴充,有興趣的讀者可以讀一讀它。
        2. 因為!=的結果不是1就是0。
        3. 感謝Guy Harris為我指出這個問題。
        4. Dennis Ritchie和Steve Johnson同時向我指出了這個問題。
        5. 感謝一位不知名的志愿者提出這個問題。
        6. 感謝Richard Stevens指出了這個問題。
        7. 一些C編譯器要求每個外部對象僅有一個定義,但可以有多個聲明。使用這樣的編譯器時,我們何以很容易地將一個聲明放到一個包含文件中,并將其定義放到其它地方。這意味著每個外部對象的類型將出現兩次,但這比出現多于兩次要好。
        8. 分離函數參數用的逗號不是逗號運算符。例如在f(x, y)中,x和y的獲取順序是未定義的,但在g((x, y))中不是這樣的。其中g只有一個參數。它的值是通過對x進行求值、拋棄這個值、再對y進行求值來確定的。
        9. 預處理器還可以很容易地組織這樣的顯式常量以能夠方便地找到它們。
        10. PDP-11和VAX-11是數組設備集團(DEC)的商標。


    原文轉自:http://www.kjueaiud.com

    評論列表(網友評論僅供網友表達個人看法,并不表明本站同意其觀點或證實其描述)
    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>