预处理器与宏
00:00
C预处理器指令、宏定义与展开、条件编译、文件包含与常见陷阱详解。
1. 预处理器概述
1.1 什么是预处理器
C预处理器(Preprocessor)是编译器在正式编译之前执行的一个文本替换工具。它不进行语法检查,只是按照指令对源代码进行文本变换。
预处理阶段在编译流程中的位置:
源代码(.c) → [预处理] → 预处理后的代码(.i) → [编译] → 汇编代码(.s) → [汇编] → 目标文件(.o) → [链接] → 可执行文件
使用 gcc -E 可以只执行预处理,查看预处理结果:
gcc -E source.c -o source.i
1.2 预处理器的功能
- 文件包含:
#include将头文件内容插入源文件 - 宏定义与展开:
#define定义常量和代码片段 - 条件编译:
#if、#ifdef等根据条件选择编译代码 - 行控制:
#line修改行号和文件名 - 错误指令:
#error在编译时产生错误信息 - 编译指示:
#pragma向编译器传递特殊指令
2. 宏定义
2.1 无参宏(对象式宏)
#include <stdio.h>
// 基本常量定义
#define PI 3.14159265
#define MAX_SIZE 100
#define NEWLINE '\n'
#define GREETING "Hello, World!"
// 使用宏
int main(void) {
double area = PI * 5 * 5;
int arr[MAX_SIZE];
printf("Area = %f%c", area, NEWLINE);
printf("%s\n", GREETING);
return 0;
}
注意事项:
- 宏定义末尾不要加分号,分号会成为替换内容的一部分
- 宏名通常全大写,以区分变量名
- 宏定义不分配内存,只是文本替换
2.2 带参宏(函数式宏)
#include <stdio.h>
// 基本带参宏
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) >= 0 ? (x) : -(x))
// 多语句宏 - 使用 do-while(0)
#define SWAP(a, b) do { \
typeof(a) _temp = (a); \
(a) = (b); \
(b) = _temp; \
} while(0)
// 字符串化与拼接
#define STR(x) #x // 将参数转为字符串
#define CONCAT(a, b) a##b // 拼接两个token
int main(void) {
int a = 5, b = 10;
printf("SQUARE(5) = %d\n", SQUARE(5)); // 25
printf("MAX(3, 7) = %d\n", MAX(3, 7)); // 7
printf("ABS(-3) = %d\n", ABS(-3)); // 3
SWAP(a, b);
printf("After swap: a=%d, b=%d\n", a, b); // a=10, b=5
printf("%s\n", STR(hello world)); // "hello world"
int CONCAT(var, 1) = 42; // 等价于 int var1 = 42;
printf("var1 = %d\n", var1); // 42
return 0;
}
2.3 宏的括号陷阱
宏定义中括号的使用至关重要,缺少括号会导致意想不到的结果:
// 错误示例:缺少括号
#define SQUARE_BAD(x) x * x
SQUARE_BAD(3 + 1) // 展开为 3 + 1 * 3 + 1 = 7,而非16
// 正确写法
#define SQUARE_GOOD(x) ((x) * (x))
SQUARE_GOOD(3 + 1) // 展开为 ((3 + 1) * (3 + 1)) = 16
// 另一个陷阱:参数有副作用
#define SQUARE_SIDE(x) ((x) * (x))
int i = 3;
SQUARE_SIDE(i++) // 展开为 ((i++) * (i++)),行为未定义!
宏定义括号规则:
- 整个宏体用括号包围
- 每个参数出现的地方都用括号包围
- 避免在宏参数中使用自增/自减运算符
2.4 可变参数宏
#include <stdio.h>
#include <stdarg.h>
// __VA_ARGS__ 代表可变参数部分
#define DEBUG_LOG(fmt, ...) \
fprintf(stderr, "[DEBUG] %s:%d: " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
// GCC扩展:##__VA_ARGS__ 允许可变参数为空
#define LOG(fmt, ...) \
printf("[LOG] " fmt "\n", ##__VA_ARGS__)
int main(void) {
int value = 42;
DEBUG_LOG("value = %d", value); // [DEBUG] test.c:12: value = 42
LOG("simple message"); // [LOG] simple message(无额外参数)
return 0;
}
3. 文件包含
3.1 #include 的两种形式
// 尖括号:在系统目录中搜索头文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 双引号:先在当前目录搜索,找不到再到系统目录
#include "myheader.h"
#include "utils/math_utils.h"
3.2 头文件保护(Include Guard)
防止头文件被重复包含:
// 方式1:传统宏保护(推荐,可移植性好)
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
int my_function(int x);
typedef struct {
int x;
int y;
} Point;
#endif /* MYHEADER_H */
// 方式2:#pragma once(编译器扩展,简洁但非标准)
#pragma once
int my_function(int x);
3.3 头文件组织原则
/* mymodule.h - 头文件 */
#ifndef MYMODULE_H
#define MYMODULE_H
/* 1. 包含必要的头文件 */
#include <stddef.h>
/* 2. 宏定义 */
#define MYMODULE_VERSION "1.0"
#define MAX_ITEMS 256
/* 3. 类型定义 */
typedef enum {
MYMODULE_OK = 0,
MYMODULE_ERROR = -1
} MyModuleStatus;
typedef struct MyModule MyModule; // 不透明指针模式
/* 4. 函数声明 */
MyModule *mymodule_create(void);
void mymodule_destroy(MyModule *mod);
MyModuleStatus mymodule_process(MyModule *mod, const char *input);
#endif /* MYMODULE_H */
/* mymodule.c - 实现文件 */
#include "mymodule.h"
#include <stdlib.h>
#include <string.h>
struct MyModule {
char buffer[MAX_ITEMS];
size_t len;
};
MyModule *mymodule_create(void) {
MyModule *mod = calloc(1, sizeof(MyModule));
return mod;
}
void mymodule_destroy(MyModule *mod) {
free(mod);
}
MyModuleStatus mymodule_process(MyModule *mod, const char *input) {
if (!mod || !input) return MYMODULE_ERROR;
strncpy(mod->buffer, input, MAX_ITEMS - 1);
mod->len = strlen(mod->buffer);
return MYMODULE_OK;
}
4. 条件编译
4.1 基本条件编译指令
// #if / #elif / #else / #endif
#define VERSION 3
#if VERSION == 1
const char *server = "v1.example.com";
#elif VERSION == 2
const char *server = "v2.example.com";
#elif VERSION == 3
const char *server = "v3.example.com";
#else
const char *server = "default.example.com";
#endif
// #ifdef / #ifndef - 检查宏是否定义
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg) ((void)0)
#endif
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif
4.2 defined 运算符
// defined 用于在 #if 中检查宏是否定义
#if defined(DEBUG) && defined(VERBOSE)
// 调试和详细模式都启用时的代码
#define DUMP_STATE(state) dump_full_state(state)
#elif defined(DEBUG)
// 仅调试模式
#define DUMP_STATE(state) dump_summary(state)
#else
#define DUMP_STATE(state) ((void)0)
#endif
// 多条件组合
#if defined(__linux__)
#define PLATFORM "Linux"
#elif defined(_WIN32)
#define PLATFORM "Windows"
#elif defined(__APPLE__)
#define PLATFORM "macOS"
#else
#define PLATFORM "Unknown"
#endif
4.3 条件编译的实际应用
// 1. 跨平台代码
#ifdef _WIN32
#include <windows.h>
#define SLEEP(ms) Sleep(ms)
#define PATH_SEPARATOR '\\'
#else
#include <unistd.h>
#define SLEEP(ms) usleep((ms) * 1000)
#define PATH_SEPARATOR '/'
#endif
// 2. 调试与发布版本
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
#condition, __FILE__, __LINE__); \
abort(); \
} \
} while(0)
#endif
// 3. 功能开关
#define FEATURE_NETWORK 1
#define FEATURE_CRYPTO 1
#if FEATURE_NETWORK
void init_network(void);
void send_data(const char *data);
#endif
#if FEATURE_CRYPTO
void encrypt_data(char *data, const char *key);
#endif
5. 预定义宏
5.1 标准预定义宏
#include <stdio.h>
int main(void) {
printf("源文件名: %s\n", __FILE__); // 当前源文件名
printf("当前行号: %d\n", __LINE__); // 当前行号
printf("编译日期: %s\n", __DATE__); // 编译日期 "Jun 13 2026"
printf("编译时间: %s\n", __TIME__); // 编译时间 "14:30:00"
printf("C标准版本: %ld\n", __STDC_VERSION__); // C标准版本号
// __func__ 是C99关键字,不是宏,返回当前函数名
printf("当前函数: %s\n", __func__);
return 0;
}
5.2 编译器扩展预定义宏
// GCC/Clang
#ifdef __GNUC__
printf("GCC版本: %d.%d.%d\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#endif
#ifdef __clang__
printf("Clang版本: %d.%d.%d\n", __clang_major__, __clang_minor__, __clang_patchlevel__);
#endif
// MSVC
#ifdef _MSC_VER
printf("MSVC版本: %d\n", _MSC_VER);
#endif
6. #pragma 指令
6.1 常用 pragma
// 结构体对齐
#pragma pack(push, 1) // 保存当前对齐,设为1字节对齐
struct PackedData {
char a;
int b;
short c;
};
#pragma pack(pop) // 恢复之前的对齐
// 禁用特定警告(MSVC)
#pragma warning(disable: 4996) // 禁用"不安全函数"警告
// 循环优化提示
#pragma GCC optimize("O3") // GCC优化级别
// 未使用参数
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
void callback(int event, void *data) {
// 不使用 event 参数
}
#pragma GCC diagnostic pop
7. 常见问题与解决方案
7.1 宏展开的副作用
问题:带副作用的参数导致未定义行为
#define SQUARE(x) ((x) * (x))
int i = 3;
int result = SQUARE(i++); // 未定义行为!
// 解决方案:使用内联函数代替
static inline int square(int x) {
return x * x;
}
int result = square(i++); // 安全,i只自增一次
7.2 多语句宏的if-else陷阱
问题:多语句宏在if-else中使用时出错
// 错误写法
#define BAD_SWAP(a, b) { \
int temp = a; a = b; b = temp; \
}
if (condition)
BAD_SWAP(x, y); // 展开后多了一个分号,else匹配错误
else
do_something();
// 正确写法:do-while(0)
#define GOOD_SWAP(a, b) do { \
int temp = a; a = b; b = temp; \
} while(0)
7.3 头文件循环依赖
问题:A.h包含B.h,B.h又包含A.h
解决方案:
- 使用前向声明代替包含
- 重新组织头文件结构,提取公共部分
- 使用不透明指针模式隐藏实现细节
7.4 宏与函数的选择
| 特性 | 宏 | 内联函数 |
|---|---|---|
| 类型检查 | 无 | 有 |
| 调试 | 困难 | 容易 |
| 副作用 | 可能有 | 无 |
| 代码膨胀 | 可能 | 可能 |
| 递归 | 不支持 | 支持 |
| 指针 | 不能取地址 | 可以 |
建议:优先使用内联函数,仅在需要文本替换(如字符串化、拼接)时使用宏。
8. 总结与最佳实践
8.1 宏定义最佳实践
- 宏名全大写,区分变量和函数
- 宏体加括号,参数加括号
- 多语句宏用 do-while(0)
- 避免参数副作用
- 复杂逻辑用内联函数替代宏
8.2 头文件最佳实践
- 始终使用 Include Guard
- 头文件只放声明,不放定义
- 最小化包含,能用前向声明就不用包含
- 自包含:头文件自身能编译通过
8.3 条件编译最佳实践
- 使用 defined() 检查宏,而非直接 #ifdef
- 提供默认值,#ifndef + #define 模式
- 保持条件逻辑清晰,避免深层嵌套
- 注释 #endif,标明对应的 #if