帶參宏是 C 語言**編譯期文本替換**機制的核心,外觀像函數、實則無調用開銷、無類型檢查、無作用域隔離。本文系統講解定義/調用語法、六大核心規則 (空格禁忌、無類型/內存、實參表達式直接替換、參數/整體括號必要性、宏 vs 函數語義差異、多語句宏),並給出經典陷阱代碼與修正模式。
/* 定義 */
#define 宏名(形參列表) 替換文本
/* 調用 */
宏名(實參列表);
關鍵:宏名與左括號 **絕對不能有空格**,否則退化為無參宏。
#define MAX(a,b) (a>b?a:b) /* 正確 */
#define MAX (a,b) (a>b?a:b) /* 錯誤:視為無參宏,替換文本為 "(a,b)(a>b?a:b)" */
MAX(x,y) → (a,b)(a>b?a:b)(x,y) /* 明顯語法錯誤 */
#define SQ(y) (y)*(y)
main() {
int a = 3;
int sq = SQ(a+1); // 實參為表達式 a+1
}
/* 展開後:sq = (a+1)*(a+1); → 16 ✓ */
/* 函數調用則會先計算 a+1=4 再傳值,結果同但機制不同 */
| 宏定義 | 調用 | 展開結果 | 值 (a=3) | 評價 |
|---|---|---|---|---|
#define SQ(y) y*y | SQ(a+1) | a+1*a+1 | 7 (3+1*3+1) | ✗ 錯誤 |
#define SQ(y) (y)*(y) | SQ(a+1) | (a+1)*(a+1) | 16 | ✓ 正確 |
#define SQ(y) (y)*(y) | 160/SQ(a+1) | 160/(a+1)*(a+1) | 160 (160/4*4) | ✗ 錯誤 |
#define SQ(y) ((y)*(y)) | 160/SQ(a+1) | 160/((a+1)*(a+1)) | 10 | ✓ 正確 |
鐵律:#define MACRO(p) ((p) op (p)) —— **參數兩側加括號、整體外層加括號**。
/* 函數版 */
int SQ(int y) { return (y)*(y); }
main() { int i=1; while(i<=5) printf("%d\n", SQ(i++)); }
/* 輸出:1 4 9 16 25 (i 每次遞增 1) */
/* 宏版 */
#define SQ(y) ((y)*(y))
main() { int i=1; while(i<=5) printf("%d\n", SQ(i++)); }
/* 展開:((i++)*(i++))
i=1: 1*2=2, i→3
i=3: 3*4=12, i→5
i=5: 5*6=30, i→7 循環結束
輸出:2 12 30 ✗ 邏輯完全不同 */
核心差異表
| 維度 | 函數 | 帶參宏 |
|---|---|---|
| 處理階段 | 編譯/運行 | 預處理 (文本替換) |
| 參數機制 | 值傳遞 (求值一次) | 符號替換 (可能多次求值/零次) |
| 類型檢查 | 嚴格 | 無 |
| 副作用安全 | 安全 (實參求值一次) | **極度危險** (含 ++/--/函數調用的實參會多次執行) |
| 調試可見性 | 符號表可見 | 預處理後消失,調試器不可見 |
| 代碼膨脹 | 無 (單一副本) | 每次調用展開,可能膨脹 |
| 遞歸 | 支援 | 不支援 (無限展開) |
#define SSSV(s1,s2,s3,v) s1=l*w; s2=l*h; s3=w*h; v=w*l*h;
main() {
int l=3,w=4,h=5, sa,sb,sc,vv;
SSSV(sa,sb,sc,vv); // 展開為 4 條賦值語句
printf("sa=%d sb=%d sc=%d vv=%d", sa,sb,sc,vv);
}
注意:多語句宏**不應**用 { } 包裹 (會影響 if/else 配對),推薦用 do { ... } while(0) 包裹 (C99 慣用模式)。
| 陷阱代碼 | 問題 | 修正模式 |
|---|---|---|
#define MAX (a,b) ... | 空格導致無參宏 | #define MAX(a,b) ... |
#define SQR(x) x*x | 缺參數括號 | #define SQR(x) ((x)*(x)) |
#define SQR(x) (x*x) | 缺整體括號 | #define SQR(x) ((x)*(x)) |
SQR(i++) | 副作用多次求值 | **禁用帶副作用實參** 或改用 inline 函數 |
#define PRINT(x) printf("%d",x) | 調試不可見 | 改用函數或 #define PRINT(x) do{printf("%d",x);}while(0) |
inline 函數 / constexpr (C++) 絕不寫宏**。#define MAX 100)、條件編譯 (#ifdef DEBUG)、類型無關的通用代碼片段 (如容器 #define container_of(ptr, type, member) ...)。MAX_SIZE),避免與標識符衝突。do { ... } while(0) 包裹,保證語句級語義。-Wshadow -Wmacro-redefined 捕獲宏濫用。帶參宏是**文本替換**,而非函數調用。牢記「無空格、無類型、實參原樣替換、雙層括號、副作用致命、多語句 do-while(0)」六大鐵律,並以 **inline 函數** 為首選替代方案,方能駕馭這把雙刃劍,避免「宏禍」污染代碼庫。