【C 陷阱與缺陷 】(二)詞法“陷阱”

碼字不易,對你有幫助 點贊/轉發/關注 支持一下作者

微信搜公衆號:不會編程的程序圓

看更多幹貨,獲取第一時間更新

代碼,練習上傳至:https://github.com/hairrrrr/C-CrashCourse

0. 理解函數聲明

請思考下面語句的含義:

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

前面我們說過 C 語言的聲明包含兩個部分:類型和類似表達式的聲明符。

最簡單的聲明符就是單個變量:

float f, g;

由於聲明符和表達式的相似,我們可以在聲明符中任意使用括號:

float ((f));

這個聲明的含義是:當對 f 求值時,((f))的類型爲 float 類型,可以推知 f 也是浮點類型。

同樣的,我們可以聲明函數:

float ff();

這個聲明的含義是:表達式 ff()求值結果是 float 類型,也就是返回 float 類型的函數。

類似的:

float *pf;

這個聲明的含義是:*pf是一個 float 類型的數,也就是說 pf 是指向 float 類型的指針。

以上的聲明可以結合起來:

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

*g()(*h)()是浮點表達式。因爲()(和[])的優先級高於**g()也就是*(g()):g 是一個函數,該函數返回一個指向浮點數的指針。同理,可以得到 h 是一個函數指針,h 所指向的函數返回值爲浮點類型。

一旦我們知道如何聲明一個給定類型的變量,那麼該類型的類型轉換符就很容易得到:只需要把聲明中的變量名和聲明末尾的分號去掉,再用括號整體括起來

比如:

float (*h)();

(float (*)())p;

假定變量 fp 是一個函數指針,那麼如何調用 fp 所指向的函數呢?調用方法如下:

(*fp)();

*fp 就是該指針所指向的函數。ANSI C 標準允許將上式簡寫爲:

fp();

但是要記住這是一種簡寫方法。

注意:(*fp)()*fp()的含義完全不同,不要省略 *fp 兩側的分號。

現在我們聲明一個返回值爲 void 類型的函數指針:

void (*fp)();

如果我們現在要調用存儲位置爲 0 的子例程,我們是否可以這樣寫:

(*0)();

上式並不能生效,因爲運算符 * 需要一個函數指針作爲操作數。我們需要對 0 進行類型轉換:

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

我們可以使用 typedef來使表述更加清晰:

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

1. 運算符優先級問題

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

FLAG 是一個已經定義的常量,FLAG 是一個整數,該數的二進制表示中只有某一位是 1,其餘的位都爲 0 ,也就是 2 的某次冪。爲了判斷整數 flags 的某一位是否也是 1,並且將結果與 0 作比較,我們寫出了上面 if 的判斷表達式。

但是!=的優先級高於&,上面的式子被解釋爲:

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

這顯然不是我們想要的。

high 和 low 是兩個 0 ~ 15 的數,r 是一個八位整數,且 r 的低 4 位與 low 一致,高 4 位與 high 一致,很自然想到:

r = high<<4 + low;

但是,加法的優先級高於移位運算,本例相當於:

r = high<<(4 + low);

對於這種情況,有兩種更正方法:

r = (high<<4) + low;

或利用移位運算的優先級高於邏輯運算:

r = high<<4 | low;

下面我們說幾個比較常見的運算符的用法:

  • a.b.c的含義是(a.b).c而不是a.(b.c)

  • 函數指針要寫成:(*p)(),如果寫成了*p(),編譯器會解釋爲:*(p())

  • *p++會解釋爲:*(p++)而不是(*p)++

  • 記住兩點:

    • 任何一個邏輯運算符的優先級低於任何一個關係運算符。
    • 移位運算符的優先級比算數運算符要低,但是高於關係運算符。
  • 賦值運算符結合方式從右到左,因此:

    a = b = 0;
    

    等價於:

    b = 0;
    a = b;
    
  • 關於涉及賦值運算時優先級的混淆:

    複製一個文件到另一個文件中:

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

    但是上式被解釋爲:

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

    關係運算符的結果只有 0 或 1 兩種可能。最後得到的文件副本中只包含了一組二進制爲 1 的字節流。

2. 注意作爲語句結束標誌的分號

考慮下面的例子:

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

這與:

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

大不相同。

前面的例子相當於:

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

無論 x[i] 是否大於 big,賦值都會被執行。

如果不是多寫了分號,而是遺漏了分號,一樣會招致麻煩:

if( n < 3)
    return
logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2];

遺漏了 return 後的分號,這段程序仍然會順利通過編譯而不會報錯,它等價於:

if( n < 3)
    return logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2];

還有一種情形,也是有分號與沒有分號實際效果相差極爲不同。那就是當一個聲明的結尾緊跟一個函數定義時,如果聲明結尾的分號被省略,編譯器可能會把聲明的類型視作函數的返回值類型。考慮下例:

struct logrec{
    int date;
    int time;
    int code;
}
main(){
    
}

上面代碼段的實際效果是聲明函數 main 返回值是結構 logrec 類型。

如果分號沒有被省略,函數 main 的返回值類型會缺省定義爲 int 類型。

3. switch 語句

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

如果稍作改動:

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

假定 color 的值爲 2,那麼將會輸出:

blueyellow

因爲程序的控制流程在執行了第二個 printf 函數的調用後,會自然地順序執行下去。第三個 printf 函數也會被調用。

switch 的這種特性,即使它的弱點,也是它的優勢所在。

對於兩個操作數的加減運算,我們可以將操作數變號來取代減法:

case SUBTRACT:
	opnd2 = -opnd2;
case ADD:
	...

在這裏,我們是有意省略 break 語句。

4. 函數調用

C 語言要求:在函數調用時,即使函數不帶參數,也應該包含參數列表。如果,f 是一個函數:

f();

是一個函數調用語句,而:

f;

卻是一個什麼也不作的語句,f 表示函數的地址。

5. 懸掛 else 引發的問題

這個相信大家學習 C 的時候老師都會講,在我的 【C 必知必會】系列教程中也有詳細講解,不懂可以去參考相關。

這裏說一點,寫 if 語句時,不要省略括號是一種可以學習的習慣。

參考資料《C 缺陷與陷阱》


以上就是本次的內容,感謝觀看。

如果文章有錯誤歡迎指正和補充,感謝!

最後,如果你還有什麼問題或者想知道到的,可以在評論區告訴我呦,我在後面的文章可以加上。

最後,關注我,看更多幹貨!

我是程序圓,我們下次再見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章