[C陷阱和缺陷] 第2章 語法“陷阱”
第2章 語法陷阱
2.1 理解函數聲明
當計算機啟動時,硬件將調用首地址為0位置的子例程,為了模擬開機時的情形,必須設計出一個C語言,以顯示調用該子例程,經過一段時間的思考,得出語句如下:
( (void() () )0 ) ();
像這樣的表達式看起來很難理解,但只要將其一層一層地剝離,還是能夠理解的。下面我將用幾個例子來幫助大家逐漸理解這個表達式。
void *a();
void (*b) ();
因為()的優先級高於*,所以*a()為*(a()),a是一個函數,該函數的返回類型為void*。而b是一個函數指針,指向返回類型為void的函數。
一旦我們知道了如何聲明一個給定類型的變量,那麽該類型的類型抓換符就很容易得到:只需要把聲明中的變量名和末尾分號去掉,再將剩余的部分用一個括號封裝起來即可。例如,下面的聲明:
void (b) ();
表示b為一個指向返回類型為void的函數的指針,因此
( void (
表示一個“指向返回類型為void的函數的指針”的類型轉換符。
擁有了這些預備知識,我們可以分兩步來分析表達式( (void() () )0 ) ()。
第一步,假定fp為一個函數指針,那麽該如何調用fp指向的函數呢?調用方法如下: [ 聲明為 void (*fp)(); ]
(fp) ();
因為fp是一個函數指針,那麽fp就是所指向的函數,所以(*fp) ()就是調用該函數的方法。ANIC標準允許將上式簡寫為fp(); 但是一定要記住這種寫法只是一種簡寫形式。
第二步,如果C編譯器能夠理解我們大腦中對類型的認識,那麽我們可以這樣寫: (0) ();
但是上式並不能奏效,因為必須要一個指針來作為操作符,而且必須是函數指針,而0不是指針。所以在上式中必須對0進行類型轉換,轉換後的類型可以大致描述為“指向返回類型為void的函數的指針”。
因此將常數0轉換為“指向返回類型為void的函數的指針”,可以這樣寫:
( void ()() )0
若fp為函數指針,要調用fp指向函數,則調用語句為(
( ( void (*)() )0 ) ();
也可以使用typedef來使表述更加清晰:
typedef void (*funcptr) () //聲明funcptr為 函數指針void (*) () 的別名
( *(funcptr)0 ) ();
2.2 運算符的優先級問題
假設要判定char類型的變量value的最高位是否為1,可以這樣寫:
if( value & 0x80 )...
如果要求對表達式的值是否為0能夠顯式地加以說明,可以這樣寫:
if( value & 0x80 != 0 )...
這個語句雖然更加好理解了,但卻是個異常的語句。因為 != 的優先級 高於 & ,所以這個語句實際被編譯器解釋為: if( value & (0x80 != 0) )...
還有下面這個例子,本意是想讓hi先左移4位,再加上low後賦給r:
r = hi<<4 + low;
但這樣寫是錯誤的,由於 + 的優先級高於 << ,所以實際會被解釋為:
r = hi<<(4 + low);
對於上面的這些情況,最簡單的解決辦法是加括號。但是如果表達式中有了太多的括號,反而不容易理解,因此最好記住運算符的優先級。
遺憾的是,C語言運算符的優先級有15之多,記住它們並不容易。完整的C語言操作符的優先級表如表2-1所示:
如果把這些運算符恰當分組,並且理解了各組運算符之間的相對優先級,那麽這張表也不難記住。
- 優先級最高者(前述操作符)並不是真正意義上的運算符,包括:函數調用操作符()、數組下標[]、結構成員選擇操作符.以及->,它們都是自左向右結合,所以a.b.c,實際上是(a.b).c。
- 單目運算符的優先級僅次於前述操作符,在和++的優先級相同的情況下,考慮到單目運算符是自右向左結合,所以p++實際會被解釋為*(p++)。
- 優先級比單目運算符要低的,接下來就是雙目運算符了。在雙目運算符中,算術運算符的優先級最高,移位操作符次之,其次是關系運算符,緊接著是邏輯運算符,最低是條件運算符(本質是三目運算符)。
我們最需要記住的就是下面兩點: - 1 任何一個關系運算符的優先級都要高於邏輯運算符;
2 移位運算符的優先級比算術運算符要低,但比關系運算符要高。
另外要註意的是,同一優先級欄的幾個運算符優先級相同,比如乘法、除法和求余的優先級相同,加法和減法的優先級相同,兩個移位運算符的優先級也相同。註意1/2a的含義是(1/2)a,而不是1/(2*a)。
但是6個關系運算符的優先級不同,<、<=、>、>=的優先級要高於==和!=,所以a<b == c<d會被編譯器解釋為(a<b) == (c<d)。
任意兩個邏輯運算符的優先級不同。所有的按位運算符優先級要比順序運算符的優先級高。2.3 註意作為語句結束標誌的分號
註意不要多寫一個分號,考慮下面這個例子:
if( x[i] > y );
y = x[i];
編譯器在這種情況下不會報錯,上面這個例子實際相當於下面這樣:
if( x[i] > y ) { }
y = x[i];
也要註意不要少些一個分號,比如下面這樣:
if( a < 3 )
return
logrec.data = x[0];
logrec.time = x[1];
此處的return後面遺落了一個分號;然而編譯器仍然不會報錯,只是會把logrec.data = x[0]作為返回值返回。
2.4 switch語句
看下面這個例子:
switch(color)
{
case 1: printf("red");
case 2: printf("yellow");
case 3: printf("blue");
}
又進一步假定變量color的值為2。最後,程序會打印出
yellowblue
因為在執行完第2個print函數後,在沒有break的情況下,即使color不等於3,程序也會繼續往下執行下去。
switch語句的這個特性,即是它的優勢所在,也是它的一大缺點。一大缺點在於,程序員很容易遺漏各個case部分的break語句,造成一些難以理解的程序行為。而優勢在於,如果程序員有意遺漏一個break語句,就能表達出一些采用其它方式
很難實現的程序控制結構。比如下面這個例子,它的作用是一個編譯器在查找字符時,跳過程序中的空白字符。這裏,空格、制表符和換行符的處理都是相同的,除了遇到換行符時程序的代碼行計數器遞增一次:
case ‘\n‘:
lineCount++;
/****註意此處沒有break******/
case ‘\t‘:
case ‘ ‘:
......
2.6 懸掛else引發的問題
考慮下面的程序片段:
if( x == 0 )
if( y == 0 ) error();
else
{
z = x + y;
}
這段代碼的本意應該是:當x==0的情況下,y==0,則執行error()函數,否則在x!=0的情況下,執行z=x+y。然而實際上與編程者的意圖相去甚遠。原因在於C語言中規定,else始終與同一對括號內最近的未匹配的if相結合。
如果按照上面程序實際的執行邏輯來調整縮進,應該是下面這樣:
if( x == 0 )
{
if( y == 0 )
error();
else
{
z = x + y;
}
}
[C陷阱和缺陷] 第2章 語法“陷阱”