預處理階段核心任務
C 編譯流程:源碼 (.c) → 預處理 (.i) → 編譯 (.s) → 彙編 (.o) → 鏈接。預處理器處理以 # 開頭的指令,完成文本替換、文件包含、條件編譯,不涉及語法語義分析。
宏定義:#define 深度解析
對象宏 vs 函數宏
// 對象宏
#define PI 3.1415926
#define BUFFER_SIZE 4096
// 函數宏 (帶參數)
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x)*(x))
// 可變參數宏 (C99)
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
關鍵規則與陷阱
- 參數加括號:
((a) > (b)) 防止優先級錯誤 (如 MAX(x&y, z))。
- 整體加括號:
((x)*(x)) 防止 SQUARE(a+b) → a+b*a+b。
- 避免副作用:
MAX(i++, j++) 導致雙重求值,**嚴禁在宏參數中使用副作用表達式**。
- 無遞歸展開:宏不遞歸展開自身 (防止無限遞歸)。
- 字符串化
#:#define STR(x) #x → STR(hello) → "hello"。
- 標記粘合
##:#define CONCAT(a,b) a##b → CONCAT(var, 1) → var1。
- 多行宏:反斜桿換行,或用
do{...}while(0) 包裹語句宏。
宏 vs 內聯函數
| 維度 | 宏 | inline 函數 |
| 類型安全 | 無 | 完整類型檢查 |
| 調試可見性 | 展開後代碼 | 可斷點、單步、變量觀察 |
| 代碼膨脹 | 每次展開複製 | 編譯器決定是否內聯 |
| 作用域 | 文件級 (無作用域) | 遵循語言作用域規則 |
現代 C/C++ 原則:能用 constexpr/inline/const 就不用宏;僅用於條件編譯、字符串化、標記粘合、編譯期計算、平台差異抽象。
文件包含:#include
#include // 系統頭文件:搜索標準包含路徑 (-I)
#include "myheader.h" // 用戶頭文件:先搜索當前目錄,再搜索系統路徑
- 護衛宏:
#ifndef HEADER_H / #define HEADER_H / ... / #endif 防止重複包含。
- #pragma once:非標準但廣泛支持,更簡潔。
- 頭文件只放聲明:宏、類型定義、函數聲明、extern 變量聲明。**不放定義** (除非
static/inline/constexpr)。
條件編譯:#if #ifdef #ifndef #elif #else #endif
常見模式
// 平台適配
#if defined(_WIN32) || defined(__CYGWIN__)
#define DLLEXPORT __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#define DLLEXPORT __attribute__((visibility("default")))
#else
#define DLLEXPORT
#endif
// 版本控制
#if VERSION_MAJOR > 2 || (VERSION_MAJOR == 2 && VERSION_MINOR >= 5)
#define HAS_FEATURE_X 1
#else
#define HAS_FEATURE_X 0
#endif
// 調試開關
#ifdef DEBUG
#define ASSERT(x) ...
#else
#define ASSERT(x) ((void)0)
#endif
// 編譯期斷言 (C11 _Static_assert / C++11 static_assert)
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
#define COMPILE_TIME_ASSERT(cond, msg) _Static_assert(cond, msg)
#else
#define COMPILE_TIME_ASSERT(cond, msg) typedef char static_assert_failed_##msg[(cond)?1:-1]
#endif
defined 操作符
#if defined(MACRO) // 等價 #ifdef MACRO
#if defined(A) && defined(B)
#if !defined(C) // 等價 #ifndef C
其他指令
| 指令 | 用途 | 示例 |
#undef | 取消宏定義 | #undef MAX |
#error | 編譯期報錯 | #error "Unsupported platform" |
#warning | 編譯期警告 (GCC/Clang) | #warning "Deprecated API" |
#line | 重置行號/文件名 | #line 100 "generated.c" |
#pragma | 編譯器特定指示 | #pragma pack(1) #pragma GCC diagnostic ignored "-Wunused" |
標準預定義宏 (ISO C / C++ 保證)
| 宏 | 含義 | 示例值 |
__FILE__ | 當前源文件名 (字符串字面量) | "main.c" |
__LINE__ | 當前行號 (整數常量) | 42 |
__DATE__ | 編譯日期 (字符串) | "Jan 15 2025" |
__TIME__ | 編譯時間 (字符串) | "14:30:00" |
__STDC__ | 符合 C 標準 (1) | 1 |
__STDC_VERSION__ | C 標準版本號 | 201710L (C18) |
__cplusplus | C++ 編譯器版本 | 202002L (C++20) |
__func__ | 當前函數名 (C99, 非宏) | "main" |
GCC/Clang 常用擴展預定義宏
// 架構
__x86_64__ __i386__ __arm__ __aarch64__ __mips__ __riscv
// 作業系統
__linux__ __APPLE__ __FreeBSD__ _WIN32 _WIN64 __ANDROID__
// 編譯器版本
__GNUC__ __GNUC_MINOR__ __GNUC_PATCHLEVEL__ __clang__ __clang_major__ __clang_minor__
// 特性檢測
__has_attribute(x) __has_builtin(x) __has_feature(x) __has_include("header.h")
進階技巧:宏元編程
枚舉到字符串
#define ENUM_TO_STR(e) case e: return #e
const char *err_str(int code) {
switch(code) {
ENUM_TO_STR(E_OK);
ENUM_TO_STR(E_NOMEM);
default: return "UNKNOWN";
}
}
編譯期計算 (C11 _Generic / C++ constexpr 更佳)
const char *sql =
"SELECT * FROM users "
"WHERE age > 18 "
"ORDER BY name;"; // 相鄰字符串字面量自動拼接
調試預處理輸出
gcc -E source.c -o source.i // 輸出預處理後 .i 文件
gcc -E -P source.c // -P 抑制 #line 指令,輸出更乾淨
clang -E source.c // 同理
總結
預處理器是 C 語言元編程的基石。掌握宏定義規範 (括號、副作用)、條件編譯模式、標準/編譯器預定義宏、調試技巧,能構建可移植、可配置、可維護的跨平台代碼庫。現代 C/C++ 開發中,儘量用語言特性 (constexpr, inline, 模板, _Generic) 替代宏,將宏保留給條件編譯、字符串化、標記粘合、平台抽象等不可替換場景。