首页 > [C 語言] 程式設計教學:如何使用巨集 (macro) 或前置處理器 (Preprocessor) | Michael Chen 的技術文件

[C 語言] 程式設計教學:如何使用巨集 (macro) 或前置處理器 (Preprocessor) | Michael Chen 的技術文件

互联网 2021-04-22 11:49:15
前言

前置處理器是在 C 或 C++ 中所使用的巨集 (macro) 語言。嚴格說來,前置處理器的語法不是 C 語言,而是一個和 C 語言共生的小型語言。在本文中,我們介紹數種常見的前置處理器用法。

閱讀經前置處理器處理過的 C 程式碼

在 C 編譯器中,前置處理器和實質的 C 編譯器是分開的。C 程式碼會經過前置處理器預處理 (preprocessing) 過後,再轉給真正的 C 編譯器,進行編譯的動作。

預處理在本質上是一種字串代換的過程。前置處理器會將 C 程式碼中巨集宣告的部分,代換成不含巨集的 C 程式碼。之後再將處理過的 C 程式碼導給 C 編譯器,進行真正的編譯。

所幸,預處理在一些 C 編譯器中是可獨立執行的步驟。以 GCC 為例,我們可以把前處理這一步獨立出來,觀察預處理後的程式碼。下列的範例指令將程式碼前處理後,用 indent 程式以 K&R 風格重新排版:

$ gcc -E -o file.i file.c$ indent -kr file.i

藉由閱讀整理過的 file.i 文字檔,我們可以了解前置處理器做了什麼事情,有利於除錯。

用 #include 引入函式庫

#include 敘述用來引入外部函式庫,這算是單純的敘述。在引入函式庫時,有兩種語法可用,如下例:

// Include some standard or third-party library.#include // Include some internal library.#include "something.h"

有些程式會將外部函式庫以一對角括號將標頭檔名稱括起來,專案內部的模組用則成對雙引號 " 和 ",在視覺上可簡單地區分。這只是撰碼風格,非強制規範。

用 #define 宣告巨集

#define 敘述用來宣告巨集。這應該是前置處理器中最具可玩性的部分。有些程式人會用巨集寫擬函式,甚至會用巨集創造語法。基本上,用巨集創造語法算是走火入魔了,我們不鼓勵讀者這麼做,知道有這件事即可。

最簡單的巨集是宣告定值:

#define SIZE 10

實際上,在轉換後的 C 程式中,並沒有 SIZE 這個變數。每個 SIZE 所在的位置會經前置處理器代換為 10。

承上,我們來看一個相關的範例:

#include /*1 */#define SIZE 5/*2 */int main(void)/*3 */{ /*4 */int arr[SIZE];/*5 */for (int i = 0; i(b) ? (a) : (b))

實際上,該巨集會代換為三元運算子,所以可以像函式般回傳值。

但巨集是很不牢靠的,像是以下的誤用例:

m = MAX(a++, b++);

該巨集會展開成以下 C 程式碼:

m = ((a++) > (b++) ? (a++) : (b++));

由於遞增運算子會隱微地改變程式的狀態,這行程式會產生預期外的結果。

巨集也可以用來跨越多行,這時候就更像函式了。我們來看一個反例,待會兒會改善該實例:

#include #include // DON'T DO THIS IN PRODUCTION CODE!#define compare(a, b) \bool cmp = 0; \if ((a) > (b)) { \cmp = 1; \} else if ((a) < (b)) { \cmp = -1; \} else { \cmp = 0; \}int main(void){compare(5, 3);assert(cmp > 0);return 0;}

在這個範例中,巨集 COMPARE 竟然強制引入了一個新的變數 cmp,而且無法修改。這樣的巨集汙染了命名空間。此外,這樣的程式會報錯:

assert(COMPARE(5, 3));

因為 COMPARE 本身非表達式,而是由多行敘述組成,無法放入 assert 中。

理想的巨集應該是安全的,不會隨意引入新的變數。透過 GCC extension 中的 statement expression 可以很安全地將變數封裝在巨集內:

#include #include // The GCC way.#define COMPARE(a, b) ({ \int flag = 0; \if (a > b) { \flag = 1; \} else if (a < b) { \flag = -1; \} else { \flag = 0; \} \flag; \})int main(void){assert(COMPARE(5, 3) > 0);return 0;}

雖然這個版本的巨集 COMPARE 很漂亮地將變數封裝起來,但這是 GCC 特有的延伸語法,非標準 C 的一部分。除非很確定專案只會用 GCC 來編譯,否則應避開這樣的特異功能。

當使用標準 C 時,我們退而求其次:

#include #include // The portable way.#define COMPARE(a, b, out) { \if ((a) > (b)) { \out = 1; \} else if ((a) < (b)) { \out = -1; \} else { \out = 0; \} \}int main(void){int out;COMPARE(5, 3, out);assert(out > 0);return 0;}

雖然這個版本的巨集 COMPARE 仍會引入新的變數,但這個變數可由巨集使用者決定,故比原本的版本好一些。

為什麼我們要用巨集寫擬函式呢?因為巨集本質上是字串代換,不受到型別限制,可用來寫擬泛型程式。但用巨集模擬泛型程式並沒有成為主流,因為巨集經過多一次轉換,難以追蹤錯誤真正發生的位置。此外,不當地使用巨集,易產生難以發覺的 bug。所以,我們儘量只用巨集處理簡單的功能。

(無用) 用巨集創造語法

承上節,程式設計者可以利用 #define 為 C 語言創造新語法。這種用法算是經典的反模式 (anti-pattern),所以看看就好,不要深入學習。

例如,我們用巨集將中序運算子「轉換」為前序運算子,就可以在 C 程式碼中「寫」Lisp:

/* DON'T DO THIS IN PRODUCTION CODE. */#include #include typedef unsigned int uint;/* Arithmetic operators. */#define ADD(a, b) ((a) + (b))#define SUB(a, b) ((a) - (b))/* Relational operators. */#define GT(a, b) ((a) > (b))#define LE(a, b) ((a)
免责声明:非本网注明原创的信息,皆为程序自动获取互联网,目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责;如此页面有侵犯到您的权益,请给站长发送邮件,并提供相关证明(版权证明、身份证正反面、侵权链接),站长将在收到邮件12小时内删除。