前置處理器在編譯之前對原始碼做純文字處理:引入檔案(#include)、
巨集替換(#define)、條件編譯(#if 家族)。
它功能強大但只是「文字取代」——不懂型別、不懂語法,因此陷阱特別多。
本單元教你安全地駕馭它。
學習目標
- 理解
#include <>與""的搜尋差異 - 會定義物件式與函式式巨集,並掌握「參數加括號」等安全守則
- 能辨識巨集的經典陷阱:優先權問題、副作用重複求值
- 會使用
#字串化與##連接運算子 - 熟悉條件編譯(
#if/#ifdef/#ifndef/#elif)與預定義巨集 - 會使用
assert做執行期檢查 - 能判斷什麼時候該用巨集、什麼時候該用函式或
const
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 巨集替換、#、## | §4.11.2 Macro Substitution |
| 檔案引入 | §4.11.1 File Inclusion |
| 條件編譯 | §4.11.3 Conditional Inclusion |
| assert | Appendix B6 |
觀念講解
1. #include
#include <stdio.h> /* <>:只搜尋系統標頭目錄 */
#include "myutils.h" /* "":先搜尋原始檔所在目錄,再退回系統目錄 */
慣例:標準庫用 <>,自己專案的標頭用 ""。
2. 物件式巨集
#define MAXLINE 1000 /* 常數命名:慣例全大寫 */
#define PI 3.14159265358979323846
前置處理器單純把 MAXLINE 換成 1000。
現代 C 中,型別安全的 const 與 enum 常是更好的選擇,
但陣列大小(C89)與條件編譯條件仍需要巨集。
3. 函式式巨集與兩大陷阱
#define SQUARE(x) ((x) * (x)) /* 每個參數、整個本體都要加括號 */
陷阱一:忘了加括號 → 優先權災難
#define BAD_SQUARE(x) x * x
BAD_SQUARE(a + 1) /* 展開成 a + 1 * a + 1,不是 (a+1)^2 ! */
100 / SQUARE(5) /* 若本體沒外層括號:100 / 25 變成 100/5*5 = 100 */
陷阱二:副作用重複求值
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m = MAX(i++, j); /* i++ 出現兩次:i 可能被加兩次! */
守則:巨集參數絕不放 ++、--、函式呼叫等有副作用的運算式。
4. # 字串化與 ## 連接
#define STRINGIFY(x) #x /* 把參數變成字串字面值 */
#define CONCAT(a, b) a ## b /* 把兩個 token 黏成一個 */
STRINGIFY(hello + 1) /* -> "hello + 1" */
CONCAT(var, 3) /* -> var3(一個識別字) */
實用範例——除錯印出「運算式文字 = 值」:
#define DEBUG_INT(expr) printf(#expr " = %d\n", (expr))
DEBUG_INT(x * y + 1); /* 印出:x * y + 1 = 13 */
5. 條件編譯
| 指令 | 意義 |
|---|---|
#if 運算式 |
常數運算式非 0 就編譯 |
#ifdef NAME |
NAME 有定義就編譯(= #if defined(NAME)) |
#ifndef NAME |
NAME 沒定義就編譯(include guard 的基礎) |
#elif / #else / #endif |
分支與收尾 |
用途:平台差異、除錯開關、include guard、暫時停用程式碼:
#if 0
/* 大段暫時停用的程式碼:比 /* */ 好,因為可以巢狀 */
#endif
除錯開關的慣用法(用 -DDEBUG 從編譯指令開啟):
#ifdef DEBUG
#define dprintf(...) fprintf(stderr, __VA_ARGS__)
#else
#define dprintf(...) ((void)0) /* 關閉時完全不產生程式碼 */
#endif
6. 預定義巨集
| 巨集 | 內容 | 範例值 |
|---|---|---|
__FILE__ |
目前檔名(字串) | "main.c" |
__LINE__ |
目前行號(整數) | 42 |
__DATE__ |
編譯日期(字串) | "Aug 1 2026" |
__TIME__ |
編譯時刻(字串) | "19:40:00" |
__func__ |
目前函式名(C99,其實是變數不是巨集) | "main" |
__STDC_VERSION__ |
C 標準版本 | 201112L(C11) |
7. assert
<assert.h> 的 assert(條件):條件為假就印出檔名、行號、條件文字並中止程式。
定義 NDEBUG 後所有 assert 消失(零成本):
#include <assert.h>
assert(n > 0 && "n must be positive"); /* 掛上訊息的小技巧 */
用途:檢查「不可能發生」的內部邏輯錯誤;不要用它驗證使用者輸入 (release 版可能被 NDEBUG 拿掉)。
8. 巨集 vs 函式
| 巨集 | 函式 | |
|---|---|---|
| 型別檢查 | 無(文字取代) | 有 |
| 對任何型別可用 | 是(泛型效果) | 否(C11 前) |
| 副作用安全 | 否 | 是 |
| 除錯難度 | 高(展開後才報錯) | 低 |
| 呼叫負擔 | 無 | 極小(現代編譯器多會 inline) |
原則:預設用函式;需要泛型(任何型別可用)、需要字串化/行號資訊、 或做編譯期常數時才用巨集。
範例程式
| 檔案 | 內容 |
|---|---|
01_macros.c |
物件式/函式式巨集、括號守則的實際災難重演、MAX(i++) 副作用實測 |
02_stringify_conditional.c |
# 與 ## 運算子、DEBUG_INT 除錯巨集、條件編譯(#if/#ifdef/#elif、#if 0) |
03_predefined_assert.c |
預定義巨集全展示、自製 LOG 巨集(__FILE__/__LINE__/__func__)、assert 用法 |
編譯與執行(以第一個為例):
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_macros.c
/tmp/demo
試試開啟除錯巨集:
cc -std=c11 -Wall -Wextra -DDEBUG -o /tmp/demo 02_stringify_conditional.c && /tmp/demo
常見錯誤與陷阱
1. 巨集參數或本體忘了加括號
#define DOUBLE(x) x + x
int y = 3 * DOUBLE(2); /* 3*2 + 2 = 8,不是 12 */
修正:#define DOUBLE(x) ((x) + (x))——參數個個加括號、本體整體加括號。
2. 有副作用的引數
#define MAX(a,b) ((a)>(b)?(a):(b))
m = MAX(i++, j); /* i++ 被求值兩次 */
修正:先存到變數再傳入,或改用函式。
3. 巨集定義結尾加了分號
#define N 100; /* 多了分號 */
int a[N]; /* 展開成 int a[100;]; -> 編譯錯誤 */
修正:#define N 100。巨集是文字取代,分號也會照抄。
4. 多敘述巨集沒有用 do-while(0) 包裹
#define SWAP(a,b) int t = a; a = b; b = t
if (x > y) SWAP(x, y); /* 只有第一句在 if 內! */
修正:#define SWAP(a,b) do { int t = (a); (a) = (b); (b) = t; } while (0)。
5. 用 assert 檢查使用者輸入或有副作用的運算式
assert(scanf("%d", &n) == 1); /* NDEBUG 下 scanf 根本不執行! */
assert 內的運算式在 release 版會整個消失;有副作用的呼叫絕不能放進去。
練習題
- 寫
#define ABS(x)巨集,測試ABS(-5)、ABS(3-7)、10 - ABS(-2)都正確。 - 寫
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])), 用它走訪陣列;再說明為什麼它對「函式參數的陣列」失效(提示:退化)。 - 寫
#define CLAMP(x, lo, hi):把 x 夾在 [lo, hi] 區間內,注意括號。 - 用
#、##寫#define DECLARE_GETTER(name), 展開成int get_##name(void) { return name; },為三個全域變數生成 getter。 - 用條件編譯寫一支程式:
-DLANG_ZH時印中文問候、-DLANG_EN時印英文、 都沒定義時印預設訊息。 - (進階)K&R 習題 4-14:寫
#define swap(t, x, y)交換兩個 t 型別的引數 (提示:用##或區塊變數避免名稱衝突)。
延伸閱讀
- K&R 2nd Edition,§4.11(本單元主軸)、Appendix A12(前置處理器完整規格)
- GCC 文件《The C Preprocessor》:最完整的前置處理器參考
- 下一單元
11-file-io:檔案輸入輸出