Week 12: 预处理器
掌握宏定义、条件编译、预定义宏与预处理器技巧.
1. 预处理器概述
1.1 预处理阶段
预处理发生在编译之前:
源代码 (.c) → 预处理器 → 预处理后的代码 (.i) → 编译器# 查看预处理结果
gcc -E main.c -o main.i1.2 预处理指令
所有预处理指令以 # 开头:
#include // 包含头文件
#define // 宏定义
#undef // 取消定义
#if // 条件编译
#ifdef // 如果定义
#ifndef // 如果未定义
#else // 否则
#elif // 否则如果
#endif // 结束条件
#error // 产生错误
#warning // 产生警告
#pragma // 编译器指令
#line // 修改行号1.3 #include 搜索顺序
#include <stdio.h> // 尖括号: 系统头文件
#include "myheader.h" // 引号: 用户头文件搜索顺序:
| 形式 | 搜索顺序 |
|---|---|
#include "file.h" | 1. 当前目录 2. -I 指定目录 3. 系统目录 |
#include <file.h> | 1. -I 指定目录 2. 系统目录 |
# 添加搜索路径
gcc -I./include -I/opt/mylib/include program.c
# 查看搜索路径
gcc -E -v program.c 2>&1 | grep "search starts"1.4 头文件最佳实践
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
// 只放声明, 不放定义
extern int global_var; // 变量声明
void my_function(int x); // 函数声明
struct MyStruct; // 前向声明
typedef struct MyStruct MyStruct;
// 内联函数可以放在头文件 (需要 static 或 extern inline)
static inline int square(int x) {
return x * x;
}
#endif // MYHEADER_H常见错误:
- 在头文件中定义变量 (导致重复定义)
- 缺少头文件保护 (导致重复包含)
- 循环包含 (A 包含 B, B 包含 A)
2. 宏定义
2.1 对象宏
#define PI 3.14159
#define MAX_SIZE 100
#define GREETING "Hello, World!"
double area = PI * r * r;
int buffer[MAX_SIZE];2.2 函数宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int m = MAX(3, 5); // ((3) > (5) ? (3) : (5))
int s = SQUARE(4); // ((4) * (4))2.3 宏的陷阱
// 陷阱 1: 运算符优先级
#define BAD_SQUARE(x) x * x
int s = BAD_SQUARE(1 + 2); // 1 + 2 * 1 + 2 = 5, 不是 9
// 正确: 加括号
#define GOOD_SQUARE(x) ((x) * (x))
// 陷阱 2: 多次求值
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int m = BAD_MAX(x++, 3); // x++ 可能被求值两次!
// 陷阱 3: 分号问题
#define BAD_SWAP(a, b) { int t = a; a = b; b = t; }
if (condition)
BAD_SWAP(x, y); // 编译错误或逻辑错误
else
// ...
// 正确: 使用 do-while
#define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while(0)2.4 多行宏
#define PRINT_DEBUG(msg) \
do { \
printf("DEBUG [%s:%d]: %s\n", __FILE__, __LINE__, msg); \
} while(0)2.5 字符串化 (#)
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
printf("%s\n", STRINGIFY(Hello)); // "Hello"
printf("%s\n", STRINGIFY(1 + 2)); // "1 + 2"
#define VERSION 1.2.3
printf("Version: %s\n", TOSTRING(VERSION)); // "1.2.3"2.6 连接 (##)
#define CONCAT(a, b) a##b
#define MAKE_VAR(name, num) name##_##num
int CONCAT(foo, bar) = 10; // int foobar = 10;
int MAKE_VAR(var, 1) = 20; // int var_1 = 20;
// 常见用法: 生成唯一标识符
#define UNIQUE(name) CONCAT(name, __LINE__)
int UNIQUE(temp); // int temp42; (假设在第 42 行)3. 条件编译
3.1 #if / #endif
#define DEBUG 1
#if DEBUG
printf("Debug mode\n");
#endif
#if DEBUG == 1
printf("Debug level 1\n");
#elif DEBUG == 2
printf("Debug level 2\n");
#else
printf("Release mode\n");
#endif3.2 #ifdef / #ifndef
#ifdef DEBUG
printf("Debug mode\n");
#endif
#ifndef RELEASE
printf("Not release\n");
#endif
// 常用于头文件保护
#ifndef HEADER_H
#define HEADER_H
// 头文件内容
#endif3.3 #pragma once
// 非标准但广泛支持的头文件保护
#pragma once
// 头文件内容3.4 defined 运算符
#if defined(DEBUG) && DEBUG > 0
printf("Debug enabled\n");
#endif
#if !defined(FEATURE_X) || defined(LEGACY_MODE)
// 兼容代码
#endif4. 预定义宏
4.1 标准预定义宏
printf("File: %s\n", __FILE__); // 当前文件名
printf("Line: %d\n", __LINE__); // 当前行号
printf("Date: %s\n", __DATE__); // 编译日期 "Jan 16 2026"
printf("Time: %s\n", __TIME__); // 编译时间 "12:00:00"
printf("Function: %s\n", __func__); // 当前函数名 (C99)4.2 编译器特定宏
#ifdef __GNUC__
printf("GCC version: %d.%d\n", __GNUC__, __GNUC_MINOR__);
#endif
#ifdef __clang__
printf("Clang\n");
#endif
#ifdef _MSC_VER
printf("MSVC version: %d\n", _MSC_VER);
#endif4.3 平台检测
#ifdef _WIN32
#include <windows.h>
#elif defined(__linux__)
#include <unistd.h>
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#endif5. 高级技巧
5.1 X-Macro
// 定义列表
#define ERROR_LIST \
X(OK, "Success") \
X(NOT_FOUND, "Not found") \
X(INVALID, "Invalid input") \
X(TIMEOUT, "Timeout")
// 生成枚举
enum ErrorCode {
#define X(name, msg) ERROR_##name,
ERROR_LIST
#undef X
ERROR_COUNT
};
// 生成字符串数组
const char *error_messages[] = {
#define X(name, msg) msg,
ERROR_LIST
#undef X
};
// 使用
printf("Error: %s\n", error_messages[ERROR_NOT_FOUND]);5.2 静态断言 (C11)
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
_Static_assert(sizeof(void *) == 8, "64-bit platform required");
// 预处理器版本
#if CHAR_BIT != 8
#error "CHAR_BIT must be 8"
#endif5.3 泛型选择 (C11)
#define print_type(x) _Generic((x), \
int: printf("int: %d\n", x), \
double: printf("double: %f\n", x), \
char *: printf("string: %s\n", x), \
default: printf("unknown type\n") \
)
print_type(42); // int: 42
print_type(3.14); // double: 3.14
print_type("hello"); // string: hello5.4 可变参数宏 (C99)
#define LOG(fmt, ...) \
printf("[LOG] " fmt "\n", ##__VA_ARGS__)
LOG("Hello"); // [LOG] Hello
LOG("Value: %d", 42); // [LOG] Value: 42
LOG("x=%d, y=%d", 10, 20); // [LOG] x=10, y=205.5 C23 预处理器新特性
1. #embed (嵌入二进制文件)
// 编译期将文件内容嵌入数组
const unsigned char favicon[] = {
#embed "favicon.ico"
};
// 带参数
const char license[] = {
#embed "LICENSE" limit(1024) // 限制大小
};
// 检测支持
#ifdef __has_embed
#if __has_embed("config.bin")
const char config[] = { #embed "config.bin" };
#endif
#endif2. #warning (标准化)
#warning "This API is deprecated, use new_api() instead"3. __VA_OPT__ (改进的可变参数)
// C99: ##__VA_ARGS__ (GNU 扩展)
#define LOG_OLD(fmt, ...) printf(fmt, ##__VA_ARGS__)
// C23: __VA_OPT__ (标准)
#define LOG(fmt, ...) printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("Hello"); // printf("Hello")
LOG("x=%d", 42); // printf("x=%d", 42)4. #elifdef / #elifndef
// C23 简化语法
#ifdef DEBUG
// debug
#elifdef TESTING
// testing
#elifndef PRODUCTION
// not production
#endif
// 等价于 C11 写法
#if defined(DEBUG)
// debug
#elif defined(TESTING)
// testing
#elif !defined(PRODUCTION)
// not production
#endif6. 练习
6.1 调试宏
实现一个 DEBUG_PRINT 宏, 在调试模式下打印文件名、行号和消息.
6.2 断言宏
实现一个 ASSERT 宏, 条件失败时打印信息并终止程序.
6.3 X-Macro
使用 X-Macro 技术生成状态机的枚举和字符串.
7. 思考题
- 宏和函数有什么区别?
- 为什么要给宏参数加括号?
- 头文件保护的作用是什么?
- X-Macro 有什么优势?
##__VA_ARGS__的作用是什么?
8. 本周小结
- 对象宏: 常量替换.
- 函数宏: 代码替换, 注意括号和副作用.
- 条件编译: #if, #ifdef, #ifndef.
- 预定义宏: FILE, LINE, func.
- 高级技巧: X-Macro, _Generic, 可变参数宏.
预处理器是 C 语言的元编程工具. 正确使用宏, 可以减少重复代码, 但过度使用会降低可读性.