校長講堂第十講
- 2020 年 4 月 10 日
- 筆記
預處理器
運行的程式並不是我們所寫的程式:因為 C 預處理器首先對其進行了轉換。出於兩個主要原因(和很多次要原因),預處理器為我們提供了一些簡化的途徑。 首先,我們希望可以通過改變一個數字並重新編譯程式來改變一個特殊量(如表的大小)的所有實例。 其次,我們可能希望定義一些東西,它們看起來象函數但沒有函數調用所需的運行開銷。例如,putchar()和 getchar()通常實現為宏以避免對每一個字元的輸入輸出都要進行函數調用。
6.1 宏不是函數
由於宏可以象函數那樣出現,有些程式設計師有時就會將它們視為等價的。因此,看下面的定義:
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏體中所有的括弧。它們是為了防止出現 a 和 b 是帶有比>優先順序低的表達式的情況。
一個重要的問題是,像 max()這樣定義的宏每個操作數都會出現兩次並且會被求值兩次。因此,在這個例子中,如果 a 比 b 大,則 a 就會被求值兩次:一次是在比較的時候,而另一次是在計算 max()值的 時候。
這不僅是低效的,還會發生錯誤:
biggest = x[0]; i = 1; while(i < n) biggest = max(biggest, x[i++]);
當 max()是一個真正的函數時,這會正常地工作,但當 max()是一個宏的時候會失敗。譬如,假設 x[0]是 2、x[1]是 3、x[2]是 1。我們來看看在第一次循環時會發生什麼。賦值語句會被擴展為:
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先,biggest 與 x[i++]進行比較。由於 i 是 1 而 x[1]是 3,這個關係是「假」。其副作用是,i 增長到 2。
由於關係是「假」,x[i++]的值要賦給 biggest。然而,這時的 i 變成 2 了,因此賦給 biggest 的值是 x[2]的值,即 1。 避免這些問題的方法是保證 max()宏的參數沒有副作用:
biggest = x[0]; for(i = 1; i < n; i++) biggest = max(biggest, x[i]);
還有一個危險的例子是混合宏及其副作用。這是來自 UNIX 第八版的中 putc()宏的定義:
#define putc(x, p) (–(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc()的第一個參數是一個要寫入到文件中的字元,第二個參數是一個指向一個表示文件的內部數據結構的指針。注意第一個參數完全可以使用如*z++之類的東西,儘管它在宏中兩次出現,但只會被求值一次。
而第二個參數會被求值兩次(在宏體中,x 出現了兩次,但由於它的兩次出現分別在一個:的兩邊,因此在putc()的一個實例中它們之中有且僅有一個被求值)。由於 putc()中的文件參數可能帶有副作用,這偶爾會出現問題。不過,用戶手冊文檔中提到:「由於 putc()被實現為宏,其對待 stream 可能會具有副作用。特別是 putc(c, *f++)不能正確地工作。」但是 putc(*c++, f)在這個實現中是可以工作的。
有些 C 實現很不小心。例如,沒有人能正確處理 putc(*c++, f)。另一個例子,考慮很多 C 庫中出現的 toupper()函數。它將一個小寫字母轉換為相應的大寫字母,而其它字元不變。如果我們假設所有的小寫字母和所有的大寫字母都是相鄰的(大小寫之間可能有所差距),我們可以得到這樣的函數:
toupper(c) { if(c >= 'a' && c <= 'z') c += 'A' – 'a'; return c; }
在很多 C 實現中,為了減少比實際計算還要多的調用開銷,通常將其實現為宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' – 'a') : (c))
很多時候這確實比函數要快。然而,當你試著寫 toupper(*p++)時,會出現奇怪的結果。
另一個需要注意的地方是使用宏可能會產生巨大的表達式。例如,繼續考慮 max()的定義:
#define max(a, b) ((a) > (b) ? (a) : (b))
假設我們這個定義來查找 a、b、c 和 d 中的最大值。如果我們直接寫:
max(a, max(b, max(c, d)))
它將被擴展為:
((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ? (a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
這出奇的龐大。我們可以通過平衡操作數來使它短一些:
max(max(a, b), max(c, d))
這會得到:
((((a) > (b) ? (a) : (b))) > (((c) > (d) ? (c) : (d))) ? (((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
這看起來還是寫:
biggest = a; if(biggest < b) biggest = b; if(biggest < c) biggest = c; if(biggest < d) biggest = d;
比較好一些。