预处理概述
基本概念
预处理是C语言编译过程的第一步,所有以#
开头的指令都由预处理器处理,这些指令不属于C语言语法本身。
预处理指令类型
- 头文件包含:
#include
- 宏定义:
#define
- 宏取消:
#undef
- 条件编译:
#if
,#ifdef
,#ifndef
,#else
,#elif
,#endif
- 错误提示:
#error
- 行号控制:
#line
- 编译器指令:
#pragma
编译过程
-E -s -c
源代码 (.c) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接 → 可执行文件
预处理操作命令
gcc example.c -o example.i -E# 只进行预处理,生成.i文件
宏的基本概念
什么是宏?
宏是一段特定的文本字符串,在预处理阶段会被替换为指定的表达式或值。
无参宏定义
#define PI 3.14#define SCREEN_SIZE 800*480*4#define NULL ((void *)0)
无参宏使用示例
#include <stdio.h>
#include <sys/mman.h>
#define PI 3.1415926
#define SCREEN_SIZE 800*480*4
#define WELCOME_MSG "Hello, World!"
int main() {printf("圆周率: %.6f\n", PI);// 替换为 printf("圆周率: %.6f\n", 3.1415926);// 系统调用中使用宏void* addr = mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);printf("%s\n", WELCOME_MSG);return 0;
}
预处理后的代码效果
// 预处理后,宏被直接替换
int main() {printf("圆周率: %.6f\n", 3.1415926);void* addr = mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, fd, 0);printf("%s\n", "Hello, World!");return 0;
}
带参宏
带参宏定义
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))#define ABS(x) (((x) >= 0) ? (x) : (-(x)))
带参宏使用示例
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int main() {int x = 10, y = 20;printf("最大值: %d\n", MAX(x, y));// 替换为: ((x) > (y) ? (x) : (y))printf("最小值: %d\n", MIN(x, y));// 替换为: ((x) < (y) ? (x) : (y))printf("平方值: %d\n", SQUARE(x));// 替换为: ((x) * (x))return 0;
}
宏的优缺点
优点
- 可读性强:使用有意义的名称代替魔术数字
- 易于修改:只需修改宏定义即可影响所有使用的地方
- 高效执行:直接文本替换,无函数调用开销
- 类型无关:可用于各种数据类型
缺点
- 调试困难:调试器看到的是替换后的代码
- 代码膨胀:在每个使用处都会展开,增加代码量
- 副作用风险:参数可能被多次求值
宏的副作用与解决方案
问题示例:多次求值
#define SQUARE(x) (x * x)// 错误写法!
int main() {int a = 5;printf("%d\n", SQUARE(a++));// 展开为: (a++ * a++)// 结果不可预测,可能输出25或30等return 0;
}
正确写法:充分使用括号
#define SQUARE(x) ((x) * (x))
// 正确:每个参数和整个表达式都用括号括起来
// 但仍然有多次求值的问题,对于a++这样的参数仍然不安全
更安全的做法:避免副作用参数
// 对于可能产生副作用的参数,最好先求值再传递
int main() {int a = 5;int temp = a++;printf("%d\n", SQUARE(temp));// 安全的使用方式return 0;
}
实用宏技巧
多语句宏
#define SWAP(a, b) do { \typeof(a) _temp = (a); \(a) = (b); \(b) = _temp; \
} while (0)// 使用do-while(0)结构可以确保宏在使用时像单个语句一样工作
字符串化操作符
#define STRINGIFY(x) #x //字符串化操作符 (#) :将宏参数转换为字符串常量
#define TO_STRING(x) STRINGIFY(x)
int main() {printf("变量名: %s\n", STRINGIFY(MAX_VALUE));// 输出: MAX_VALUEprintf("行号: %s\n", TO_STRING(__LINE__));// 输出: 15return 0;
}
连接操作符
#define CONCAT(a, b) a##b //连接操作符 (##):将两个标记连接成一个新的标记
int main() {int xy = 100;printf("%d\n", CONCAT(x, y));// 输出: 100return 0;
}
系统预定义宏
#include <stdio.h>int main() {printf("当前文件名: %s\n", __FILE__);printf("当前行号: %d\n", __LINE__);printf("编译日期: %s\n", __DATE__);printf("编译时间: %s\n", __TIME__);printf("是否遵循C标准: %d\n", __STDC__);#ifdef __linux__printf("这是在Linux系统上编译\n");#endifreturn 0;
}
最佳实践建议
- 宏名全大写:区分于变量和函数
- 充分使用括号:每个参数和整个表达式都要括起来
- 避免副作用参数:不要传递类似
a++
的参数 - 多语句使用do-while:确保宏行为像单个语句
- 注释宏的作用:说明宏的功能和注意事项
- 优先考虑函数:除非有性能要求,否则优先使用函数
调试宏技巧
查看预处理结果
gcc -E program.c -o program.i# 生成预处理后的文件
使用#error检查配置
#ifndef CONFIG_VALUE#error "CONFIG_VALUE must be defined!"#endif
条件编译
条件编译 是一种通过预处理指令 动态控制代码参与编译 的技术,常用于 跨平台适配、调试开关、功能模块化 等场景。
条件编译指令(基础指令)
指令 | 作用 |
---|---|
#if |
判断条件是否为真(支持表达式) |
#ifdef |
判断宏是否已定义(等价于 #if defined ) |
#ifndef |
判断宏是否未定义 |
#elif |
前一个条件不满足时判断新条件 |
#else |
所有条件均不满足时执行的代码块 |
#endif |
结束条件编译块 |
语法示例
#if defined(PLATFORM_WIN) && (VERSION > 2020)
// Windows 平台且版本大于 2020 的代码
#elif defined(PLATFORM_LINUX)
// Linux 平台的代码
#else
// 其他情况
#endif