在 C 程式進入預處理階段之前,編譯器會先執行一系列翻譯步驟。首先,編譯器將原始碼中的字符映射到原始字符集。接著,它會尋找反斜線(\)後緊跟換行符的情況並將其刪除,這樣就能將跨越多個物理行的程式碼合併為一個邏輯行。例如:
printf("That's wond\
erful!\n");
會被轉換為:
printf("That's wonderful!\n");
隨後,編譯器將文本劃分為預處理語言符號序列、空白字符序列與註釋序列,並用一個空格字符取代每一個註釋。完成這些步驟後,程式才正式進入預處理階段,預處理器開始尋找以 # 符號開頭的預處理指令。
#define 指令以 # 符號開頭,可出現在原始檔的任何位置,其作用域從定義處開始直到檔案結尾。每個 #define 行包含三部分:指令本身、宏名稱(遵循 C 變量命名規則,不含空格)以及替換列表(主體)。當預處理器在程式中發現宏時,會用替換文本進行替換,這個過程稱為「宏展開」。
預處理器將宏主體視為語言符號類型字符串,而非單純字符串。例如 #define FOUR 2*2 只有一個語言符號 2*2;而 #define SIX 2 * 3 則有三個語言符號:2、*、3。當作語言符號處理時,多餘空格會被忽略,最終以單個空格分隔各語言符號進行替換。
若同一個宏被定義多次,且其語言符號序列完全相同(忽略多餘空格),則視為相同定義;否則屬於重定義,會引發警告。可使用 #undef 取消宏定義,之後方可重新定義。
透過參數可建立外觀與行為類似函數的「類函數宏」。例如 #define SQUARE(X) X*X。但需注意:預處理器僅做字符串替換,不進行運算。若呼叫 SQUARE(x+2),會展開為 x+2*x+2 而非 (x+2)*(x+2)。解決方法是在宏定義中加上括號:#define SQUARE(X) ((X)*(X))。
若希望在字符串中包含宏參數,可使用 # 運算符將參數名轉為對應字符串。例如:
#define PR(X) printf("The square of " #X " is %d.\n", (X)*(X))
呼叫 PR(y) 會展開為 printf("The square of " "y" " is %d.\n", (y)*(y)),經字符串連接後輸出 The square of y is 25.。
## 運算符可將兩個語言符號合併為一個。例如 #define XNAME(n) x ## n,呼叫 XNAME(4) 會展開為 x4。此運算符亦可用於類對象宏。
C99 引入 ... 與 __VA_ARGS__ 實現可變參數宏。例如:
#define PR(...) printf(__VA_ARGS__)
PR("Hello"); // 展開為 printf("Hello");
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
PR(1, "x=%g\n", x); // 輸出 Message 1: x=48
宏產生內聯代碼,每次使用都會插入代碼,增加程式體積但省去函數呼叫開銷;函數僅有一份代碼拷貝,節省空間但涉及控制轉移,耗時較多。宏不檢查參數類型,靈活但易產生難以預期的結果。
#include 指令會將指定檔案的內容插入到當前檔案中。慣例使用 .h 後綴表示標頭檔,內容通常為編譯所需的聲明資訊,而非實際可執行語句,因此包含大型標頭檔不一定會顯著增加最終程式大小。標頭檔也常用於聲明多檔案共享的外部變量。
#undef LIMIT 可取消 LIMIT 的定義,即使該宏未定義也可安全使用。
預處理器視角下,唯有透過 #define 定義且未被 #undef 取消的標識符才算「已定義」。普通 C 變量(如 int q;)在預處理器眼中屬於「未定義」。
#ifdef/#else/#endif:根據宏是否定義編譯不同代碼區塊。#ifndef:#ifdef 的反向指令,常用於防止標頭檔重複包含(Include Guard)。#if/#elif:後接常量整數表達式,功能更強大。現代寫法偏好 #if defined(MACRO) 而非 #ifdef MACRO。內聯函數使用 inline 說明符聲明,編譯器會嘗試將函數本體內嵌到呼叫點,結合宏的速度優勢與函數的類型檢查優勢。內聯函數適用於短小函數;定義與呼叫需在同一檔案,C 標準放寬了對內聯函數重複定義的限制。
#include 引入定義檔案。atexit() 註冊程式結束時自動呼叫的函數(類型為 void func(void)),按後進先出(LIFO)順序執行。exit() 執行清理工作:刷新輸出流、關閉打開的流等。
assert(expr) 宏用於調試:若表達式為假,向標準錯誤輸出錯誤訊息並呼叫 abort() 終止程式。定義 NDEBUG 宏可禁用所有斷言。
兩者原型均為 void *func(void *s1, const void *s2, size_t n),功能都是從 s2 複製 n 位元組到 s1 並返回 s1。關鍵差異在於 memcpy() 要求來源與目標記憶體區域不重疊,而 memmove() 允許重疊並正確處理。第三個參數常寫作 num * sizeof(int) 以增強可讀性。