碼字不易,對你有幫助 點贊/轉發/關注 支持一下作者
微信搜公衆號:不會編程的程序圓
看更多幹貨,獲取第一時間更新
代碼,練習上傳至: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 缺陷與陷阱》
以上就是本次的內容,感謝觀看。
如果文章有錯誤歡迎指正和補充,感謝!
最後,如果你還有什麼問題或者想知道到的,可以在評論區告訴我呦,我在後面的文章可以加上。
最後,關注我,看更多幹貨!
我是程序圓,我們下次再見。