# 4 预处理指令 ## 4.1 完整概念讲解 预处理器(preprocessor)在编译之前对源代码进行文本替换和条件处理。预处理指令以 `#` 开头,由预处理器在编译阶段展开。 ### #define 宏定义 #### 对象宏(Object-like Macro) ```c #define PI 3.14159265358979 #define BUFFER_SIZE 1024 #define NULL ((void *)0) ``` - 纯文本替换,不做类型检查 - 替换发生在编译之前,调试时可能看不到宏的名字 #### 函数宏(Function-like Macro) ```c #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define SQUARE(x) ((x) * (x)) ``` - 参数必须紧跟宏名,中间不能有空格:`MAX(a,b)` 是宏,`MAX (a,b)` 不是 - 每个参数都应加括号,防止运算符优先级问题 #### 宏的副作用 ```c #define MAX(a, b) ((a) > (b) ? (a) : (b)) int x = 5, y = 10; int result = MAX(x++, y++); // 展开为: ((x++) > (y++) ? (x++) : (y++)) // x++ 执行了 2 次,y++ 执行了 2 次(或 1 次) ``` 这是宏最危险的陷阱:参数被多次求值。解决方案:使用 GCC 的语句表达式(`({...})`) 或改用 `inline` 函数。 ### #define 与 const 的区别 | 特性 | `#define` | `const` | | -------- | ---------------------------- | ------------------ | | 处理阶段 | 预处理(文本替换) | 编译(类型检查) | | 类型安全 | 无 | 有 | | 调试 | 可能显示替换后的值 | 保留符号名 | | 作用域 | 定义后到 `#undef` 或文件末尾 | 遵循 C 作用域规则 | | 内存占用 | 不占用(除非取地址) | 占用(有存储地址) | | 适用场景 | 编译时常量、条件编译、位掩码 | 运行时常量 | ### #include ```c #include /* 在系统头文件目录中查找 */ #include "myheader.h" /* 先在当前目录查找,再在系统目录查找 */ ``` - 尖括号:系统/标准库头文件 - 双引号:用户自定义头文件 - `#include` 可以嵌套,但深度有限制(通常 8-16 层) ### 头文件保护宏 #### 方式一:传统 include guard ```c #ifndef MYHEADER_H #define MYHEADER_H /* 头文件内容 */ #endif /* MYHEADER_H */ ``` #### 方式二:#pragma once ```c #pragma once /* 头文件内容 */ ``` 对比: | 特性 | `#ifndef`/`#define`/`#endif` | `#pragma once` | | ----------------- | ---------------------------- | ---------------------------- | | 标准支持 | C89 起所有编译器 | 非标准,但所有主流编译器支持 | | 可移植性 | 完美 | 99% 场景可用 | | 符号冲突风险 | 有(宏名可能冲突) | 无 | | 对符号链接/硬链接 | 可能重复包含 | 正确处理 | ### 条件编译 ```c #ifdef DEBUG /* 调试代码 */ #endif #ifndef NDEBUG /* 非调试代码 */ #endif #if defined(__linux__) /* Linux 特定代码 */ #elif defined(_WIN32) /* Windows 特定代码 */ #else /* 其他平台 */ #endif #if VERSION >= 2 /* 新版本特性 */ #endif ``` 条件编译在 Linux 内核中大量使用,用于支持多平台编译(ARM、x86、MIPS 等)。 ### #undef ```c #define LIMIT 100 /* ... */ #undef LIMIT /* LIMIT 不再定义 */ ``` 用于重新定义宏或限制宏的作用域。 ### #error ```c #if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L #error "This code requires C11 or later" #endif ``` 在编译时产生错误消息,用于编译期条件检查。 ### 预定义宏 | 宏 | 含义 | | ------------------ | -------------------------------------------------------- | | `__FILE__` | 当前源文件名(字符串) | | `__LINE__` | 当前行号(整数) | | `__DATE__` | 编译日期(字符串,如 "Aug 31 2026") | | `__TIME__` | 编译时间(字符串) | | `__func__` | 当前函数名(C99,字符串) | | `__STDC__` | 编译器遵循 ANSI C(1) | | `__STDC_VERSION__` | C 标准版本号(C99: 199901L, C11: 201112L, C17: 201710L) | | `__STDC_HOSTED__` | 实现是否 hosted(1)或 freestanding(0) | ### ## 和 # 运算符 #### # 运算符(Stringification) 将宏参数转换为字符串字面量: ```c #define STRINGIFY(x) #x #define PRINT_VAR(var) printf(#var " = %d\n", var) int x = 42; PRINT_VAR(x); /* 展开为: printf("x" " = %d\n", x); 即 printf("x = %d\n", x); */ ``` #### ## 运算符(Token Pasting) 将两个 token 拼接为一个: ```c #define CONCAT(a, b) a##b #define MAKE_STRUCT(prefix, name) struct prefix##_##name int var10 = 100; printf("%d\n", CONCAT(var, 10)); /* 输出 100 */ MAKE_STRUCT(sensor, data) { /* 展开为: struct sensor_data { ... }; */ int value; }; ``` --- ## 4.2 核心 API/语法 ### 可变参数宏(Variadic Macros) ```c #define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__) #define DEBUG_LOG(fmt, ...) \ fprintf(stderr, "[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__) ``` C99 引入 `__VA_ARGS__`,C23 引入 `__VA_OPT__(,)` 用于处理可变参数为空的情况。 ### 多行宏 ```c #define SWAP(a, b) do { \ typeof(a) _tmp = (a); \ (a) = (b); \ (b) = _tmp; \ } while (0) ``` `do { ... } while (0)` 模式确保宏在 if-else 语句中安全使用。 --- ## 4.3 代码示例 ### 示例 1:宏定义与字符串化、token 拼接 ```c #include #define PI 3.14159265358979 #define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define STRINGIFY(x) #x #define CONCAT(a, b) a##b #define DEBUG_LOG(fmt, ...) \ fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define SWAP(a, b) do { \ __typeof__(a) _tmp = (a); \ (a) = (b); \ (b) = _tmp; \ } while (0) int main(void) { printf("=== 对象宏 ===\n"); printf("PI = %.15f\n", PI); printf("sizeof(int) = %zu\n", sizeof(int)); printf("\n=== 函数宏 ===\n"); int x = 5; printf("SQUARE(%d) = %d\n", x, SQUARE(x)); printf("MAX(3, 7) = %d\n", MAX(3, 7)); printf("MIN(3, 7) = %d\n", MIN(3, 7)); printf("\n=== # 运算符(字符串化)===\n"); int var = 42; printf("STRINGIFY(hello) = \"%s\"\n", STRINGIFY(hello world)); printf("STRINGIFY(1+2) = \"%s\"\n", STRINGIFY(1 + 2)); printf("var 的值: "); printf(#var " = %d\n", var); /* 等价于 printf("var = %d\n", var); */ printf("\n=== ## 运算符(token 拼接)===\n"); int val10 = 100; int val20 = 200; printf("CONCAT(val, 10) = %d\n", CONCAT(val, 10)); printf("CONCAT(val, 20) = %d\n", CONCAT(val, 20)); printf("\n=== SWAP 宏 ===\n"); int a = 10, b = 20; printf("交换前: a=%d, b=%d\n", a, b); SWAP(a, b); printf("交换后: a=%d, b=%d\n", a, b); printf("\n=== DEBUG_LOG ===\n"); DEBUG_LOG("程序启动,x = %d", x); return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o preprocess1 preprocess1.c ./preprocess1 ``` ### 示例 2:#include 与头文件保护 **config.h(自定义头文件):** ```c #ifndef CONFIG_H #define CONFIG_H #define APP_NAME "PreprocessorDemo" #define APP_VERSION "1.0.0" #define MAX_DEVICES 16 typedef struct { int id; char name[32]; } Device; #endif /* CONFIG_H */ ``` **main.c:** ```c #include #include "config.h" int main(void) { printf("App: %s v%s\n", APP_NAME, APP_VERSION); printf("Max devices: %d\n", MAX_DEVICES); Device dev = { .id = 1, .name = "sensor_01" }; printf("Device: [%d] %s\n", dev.id, dev.name); return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o include_demo main.c ./include_demo ``` ### 示例 3:条件编译与预定义宏 ```c #include /* 编译器检测 */ #if defined(__GNUC__) #define COMPILER "GCC " __VERSION__ #elif defined(_MSC_VER) #define COMPILER "MSVC " _MSC_VER #else #define COMPILER "Unknown Compiler" #endif /* 平台检测 */ #if defined(__linux__) #define PLATFORM "Linux" #elif defined(_WIN32) #define PLATFORM "Windows" #elif defined(__APPLE__) #define PLATFORM "macOS" #else #define PLATFORM "Unknown" #endif /* 调试模式开关 */ #ifdef DEBUG #define LOG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__) #else #define LOG(fmt, ...) ((void)0) #endif /* C 标准版本检查 */ #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L #define C_STANDARD "C11" #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L #define C_STANDARD "C99" #else #define C_STANDARD "C89/C90" #endif /* 错误检查 */ #if MAX_DEVICES < 1 #error "MAX_DEVICES must be at least 1" #endif /* 调试与发布模式下不同的数组大小 */ #ifdef DEBUG #define TRACE_SIZE 1024 #else #define TRACE_SIZE 256 #endif void print_info(void) { LOG("print_info 被调用"); printf("Compiler: %s\n", COMPILER); printf("Platform: %s\n", PLATFORM); printf("C Standard: %s\n", C_STANDARD); printf("File: %s\n", __FILE__); printf("Date: %s %s\n", __DATE__, __TIME__); printf("Function: %s\n", __func__); printf("Trace buffer size: %d\n", TRACE_SIZE); } int main(void) { LOG("程序启动"); print_info(); /* 条件编译示例:不同平台不同行为 */ #if defined(__linux__) printf("Linux 特定代码\n"); #elif defined(_WIN32) printf("Windows 特定代码\n"); #else printf("其他平台代码\n"); #endif LOG("程序结束"); return 0; } ``` 编译命令: ```bash # 普通模式 gcc -std=c99 -Wall -Wextra -DDEBUG -o preprocess3 preprocess3.c ./preprocess3 # 发布模式(关闭 DEBUG) gcc -std=c99 -Wall -Wextra -o preprocess3_release preprocess3.c ./preprocess3_release ``` ### 示例 4:#error 与版本检查 ```c #include /* 编译期版本检查 */ #if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L #error "This code requires C99 or later" #endif /* 检查特定头文件是否可用 */ #if defined(__has_include) #if __has_include() #define HAS_STDINT_H 1 #else #define HAS_STDINT_H 0 #endif #else #define HAS_STDINT_H 1 /* 假设有 */ #endif #if HAS_STDINT_H #include #endif /* 用 #undef 重定义宏 */ #define LIMIT 100 #undef LIMIT #define LIMIT 200 int main(void) { printf("LIMIT = %d\n", LIMIT); #if HAS_STDINT_H printf("stdint.h available, sizeof(uint32_t) = %zu\n", sizeof(uint32_t)); #endif return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o preprocess4 preprocess4.c ./preprocess4 ``` --- ## 4.4 注意事项与易错点 1. **宏参数不加括号的危险**: ```c #define SQUARE_BAD(x) x * x SQUARE_BAD(2 + 3) /* 展开为 2 + 3 * 2 + 3 = 11,而非 25 */ ``` 2. **宏中的分号陷阱**: ```c #define SET(x) x = 0; if (cond) SET(val) else /* 语法错误!分号导致 if-else 断裂 */ do_something(); ``` 3. **`#define` 不加分号**:`#define PI 3.14;` 会导致 `PI` 被替换为 `3.14;`,使用时产生语法错误。 4. **头文件保护宏命名冲突**:不同头文件使用相同保护宏名会导致其中一个被静默跳过。使用唯一命名(如 `PROJECT_MODULE_FILENAME_H`)。 5. **`##` 拼接空 token**:C 标准禁止 `##` 产生空 token,但 GCC 扩展允许。可移植代码应避免。 6. **`#if` 中的 `0` 而非 `false`**:`#if` 只接受整数常量表达式,不能用 `true`/`false`(C99 之前没有 `_Bool`)。 7. **多行宏的续行符 `\`**:反斜杠后不能有空格,否则续行失败。续行符后必须紧跟换行符。 --- ## 4.5 面试要点 **Q1:#define 和 inline 函数有什么区别?** A:(1) `#define` 是文本替换,无类型检查;`inline` 是函数,有完整类型检查。(2) `#define` 可能导致参数多次求值(副作用);`inline` 只求值一次。(3) `#define` 在预处理阶段展开,调试困难;`inline` 函数在编译阶段处理,保留符号信息。(4) `#define` 可以用于条件编译、token 拼接等无法用函数替代的场景。 **Q2:`#pragma once` 和 include guard 哪个更好?** A:两者各有优势。`#pragma once` 更简洁、无符号冲突风险、对硬链接正确。include guard 是 C 标准方式,可移植性完美(适用于所有符合标准的编译器)。实际中,主流编译器(GCC、Clang、MSVC)都支持 `#pragma once`,但如果需要极致可移植性,用 include guard。 **Q3:为什么内核代码大量使用条件编译?** A:Linux 内核需要在数百种硬件平台上编译运行。条件编译用于:(1) 平台特定代码(ARM vs x86);(2) 编译配置(`CONFIG_SMP` 多处理器支持);(3) 调试/性能分析开关;(4) 不同编译器版本兼容。这使得同一份代码可以在极不同的环境下编译。 **Q4:`#include` 尖括号和双引号的本质区别是什么?** A:预处理器在不同路径集合中查找。尖括号 `` 在系统指定的 include 路径(如 `/usr/include`)中查找。双引号 `"myheader.h"` 先在当前源文件所在目录查找,找不到再回退到系统路径。用户自定义头文件应始终用双引号。 **Q5:如何安全地定义函数宏?** A:遵循以下规则:(1) 每个参数都加括号;(2) 整个表达式加括号;(3) 使用 `do { ... } while (0)` 包裹多语句宏;(4) 使用双下划线前缀的临时变量名避免命名冲突;(5) 如果可能,优先使用 `static inline` 函数替代函数宏。