文章目录
- 前言
- 本文旨在系统性地介绍C语言中的预处理。
- 一、预定义符号
- 二、#define定义宏
- 2.1、#define定义标识符(无参宏)
- 2.2、宏定义中括号的使用
- 2.3、带有副作用的宏参数
- 2.4、宏替换的规则
- 2.5、宏和函数的对比
- 三、命名约定
- 四、#undef
- 五、条件编译
- 六、头文件包含
- 6.1、头文件被包含的方式
- 6.2、嵌套文件包含
前言
本文旨在系统性地介绍C语言中的预处理。
一、预定义符号
预定义符号是编译器预先定义的宏,可直接使用,无需 #define。它们在预处理阶段被替换为对应的值。
__FILE__//进⾏编译的源⽂件名__LINE__//⽂件当前的⾏号__DATE__//⽂件被编译的⽇期__TIME__//⽂件被编译的时间__STDC__//如果编译器遵循ANSI C,其值为1,否则未定义代码示例:
#include<stdio.h>intmain(){printf("文件:%s\n",__FILE__);printf("行号:%d\n",__LINE__);printf("编译日期:%s\n",__DATE__);printf("编译时间:%s\n",__TIME__);return0;}二、#define定义宏
宏定义允许将参数替换到文本中,其语法形式如下:
#definename(parament-list)stuffname:宏名
(parament-list):参数列表,由逗号分隔的符号表
stuff:替换文本,参数可以出现在其中
注意事项:参数列表的左括号必须紧挨宏名,不能有空格。如果有空格,参数列表被当成替换文本。
2.1、#define定义标识符(无参宏)
基本语法
#definenamestuff代码示例:
#defineMAX1000#defineregregister// 为 register 关键字起简短别名#definedo_foreverfor(;;)// 用形象符号替换无限循环#defineCASEbreak;case// case 语句自动加 break当 stuff 过长时,可以用反斜杠\(续写符)分行编写(最后一行不加)告诉预处理器:下一行是当前行的延续,在预处理阶段会合并成一行。如果没有续行符,预处理后,宏定义只到第一行结束,后面的内容被当作普通代码,导致编译错误。
代码示例:
#defineDEBUG_PRINTprintf("file:%s\tline:%d\t"\"date:%s\ttime:%s\n",\__FILE__,__LINE__,\__DATE__,__TIME__)末尾不建议加分号,否则可能引发语法错误。
代码示例:
#defineMAX1000;if(condition)max=MAX;// 替换后 → max = 1000;;// 替换后if 和 else 之间变成两条语句,导致编译错误。elsemax=0;2.2、宏定义中括号的使用
宏定义中,给每个参数加上括号,再给整个表达式加上括号,可以避免因操作符优先级导致的意外结果。
①参数未加括号(宏是纯文本替换,参数中的操作符优先级影响计算顺序)
#defineSQUARE(x)x*xSQUARE(a+1)// 替换后 → a + 1 * a + 1// 实际计算:a + (1 * a) + 1// 结果与预期不符解决办法:给宏体的每个参数加上括号。
#defineSQUARE(x)(x)*(x)SQUARE(a+1)// 替换后 → (a + 1) * (a + 1)②整个宏体未加括号(宏体本身没有括号,导致外部运算符影响计算顺序)
#defineDOUBLE(x)(x)+(x)10*DOUBLE(a)// 替换后 → 10 * (5) + (5)// 实际计算:10 * 5 + 5 = 55解决办法:给整个宏体加上括号。
#defineDOUBLE(x)((x)+(x))10*DOUBLE(a)// 替换后 → 10 * ((5) + (5)) = 1002.3、带有副作用的宏参数
宏参数在宏体中多次出现时,带副作用的参数会被多次求值,导致不可预测的结果。应避免在宏参数中使用 ++、-- 等操作,或用函数替代宏。
副作用是指表达式求值时永久改变变量值的效果。
x+1;// 无副作用(x 不变)x++;// 有副作用(x 被修改)代码示例:
#include<stdio.h>#defineMAX(a,b)((a)>(b)?(a):(b))intmain(){intx=5;inty=8;// 宏调用:MAX(x++, y++)// 预处理后展开为:((x++) > (y++) ? (x++) : (y++))// 执行过程:// ① 比较 (x++) > (y++),即 5 > 8(假),此时 x=6,y=9// ② 条件为假,执行 (y++),取 y 的当前值 9 赋给 z,然后 y 变为 10// ③ 最终:x=6,y=10,z=9intz=MAX(x++,y++);printf("x=%d y=%d z=%d\n",x,y,z);return0;}2.4、宏替换的规则
① 参数检查:检查宏参数是否包含 #define 定义的符号,如果有则先替换
② 文本插入:将替换文本插入到宏调用的位置,参数名被对应的值替换
③ 再次扫描:对结果文件再次扫描,检查是否还有 #define 定义的符号,如果有则重复上述过程
代码示例:
#definePI3.14#defineAREA(r)(PI*(r)*(r))doubles=AREA(5);替换过程:
①检查参数 5,没有 #define 定义的符号;
②插入替换文本:(PI * (5) * (5));
③再次扫描,发现 PI 是 #define 定义的符号;
④替换 PI:(3.14 * (5) * (5))。
注意事项:
①宏不能递归:宏不能调用自身;
②字符串内容不搜索:字符串字面量中的内容不会被宏替换。
代码示例:
#definePI3.14printf("PI = %f",PI);// 宏被替换printf("PI");// 字符串中的 PI 不会被替换2.5、宏和函数的对比
| 对比项 | 宏(Macro) | 函数(Function) |
|---|---|---|
| 执行时机 | 预处理阶段展开 | 运行时调用 |
| 执行效率 | 高(无调用开销) | 低(有调用和返回开销) |
| 代码长度 | 大型宏多次展开使程序膨胀 | 代码只存一份 |
| 类型检查 | 无 | 有 |
| 调试 | 无法调试 | 可逐语句调试 |
| 类型通用 | 类型无关 | 参数类型固定 |
| 参数为类型 | 可以(如 MALLOC(10, int)) | 不可以 |
| 副作用风险 | 参数多次求值 | 参数只求值一次 |
| 内存开销 | 不占栈空间 | 每次调用占用栈空间 |
| 递归 | 不可递归 | 可递归 |
| 操作符优先级 | 易受邻近操作符影响 | 安全(参数先求值) |
三、命名约定
宏名全部大写,函数名不要全部大写,C语言本身没有区分,目的是让代码可读性更强,便于区分宏和函数。
四、#undef
#undef 指令用于移除一个已存在的宏定义。如果需要重新定义一个宏名,或取消某个宏的作用,可以先用 #undef 移除旧定义。
语法形式
#undefNAME代码示例:
#include<stdio.h>#defineMAX100// 此时 MAX 已定义#undefMAX// 移除 MAX 的宏定义// 此时 MAX 未定义,不能再使用#ifdefMAXprintf("MAX 已定义\n");#elseprintf("MAX 未定义\n");#endif五、条件编译
条件编译用于在预处理阶段根据条件决定哪些代码被保留、哪些被丢弃,常用于调试信息控制、头文件保护和跨平台适配。
常见条件编译指令:
①单分支条件编译
#if常量表达式// 表达式为真时编译#endif#define__DEBUG__1#if__DEBUG__printf("调试模式\n");// 被编译#endif②多分支条件编译
#defineVERSION2#ifVERSION==1printf("版本1\n");#elifVERSION==2printf("版本2\n");#elseprintf("其他版本\n");#endif③判断是否被定义
| 写法 | 含义 |
|---|---|
| #if defined(symbol) | symbol 已定义时为真 |
| #ifdef symbol | 同上(简写) |
| #if !defined(symbol) | symbol 未定义时为真 |
| #ifndef symbol | 同上(简写) |
#defineDEBUG#ifdefDEBUGprintf("调试模式\n");// 被编译#endif#ifndefRELEASEprintf("非发布模式\n");// 不被编译#endif④嵌套条件编译
#ifdefined(OS_UNIX)#ifdefOPTION1unix_version_option1();#endif#ifdefOPTION2unix_version_option2();#endif#elifdefined(OS_MSDOS)#ifdefOPTION2msdos_version_option2();#endif#endif六、头文件包含
6.1、头文件被包含的方式
| 包含方式 | 查找路径 | 适用场景 |
|---|---|---|
| #include <xxx.h> | 直接去标准系统路径查找 | 库文件(标准库、第三方库) |
| #include “xxx.h” | 先在当前源文件所在目录查找,找不到再去标准路径查找 | 本地头文件(项目自定义) |
库文件也可以用 “” 包含,但是查找效率低,不易区分是库还是本地文件。
6.2、嵌套文件包含
头文件被重复包含
// test.c#include"test.h"#include"test.h"#include"test.h"#include"test.h"#include"test.h"预处理后,test.h 的内容会被拷贝 5 次,导致代码膨胀和编译压力增加。
解决方案1:头文件守卫(条件编译)
#ifndef__TEST_H__#define__TEST_H__// 头文件的内容voidtest();structStu{intid;charname[20];};#endif// __TEST_H__解决方案2:#pragma once
#pragmaonce// 头文件的内容voidtest();structStu{intid;charname[20];};