天天看點

校長講堂第五講

句法缺陷

要了解 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(!)的時候,但對于其他 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)++。

在接下來是真正的二進制運算符。其中數學運算符具有最高的優先級,然後是移位運算符、關系運算符、

邏輯運算符、指派運算符,最後是條件運算符。需要記住的兩個重要的東西是:

1. 所有的邏輯運算符具有比所有關系運算符都低的優先級。

2. 一位運算符比關系運算符綁定得更緊密,但又不如數學運算符。

在這些運算符類别中,有一些奇怪的地方。乘法、除法和求餘具有相同的優先級,加法和減法具有相同的優先級,以及移位運算符具有相同的優先級。

還有就是六個關系運算符并不具有相同的優先級:==和!=的優先級比其他關系運算符要低。這就允許我們判斷 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 相等。而實際的效果卻大不相同。

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

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 将被定義為具有預設的整型傳回值。