
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- [C 语言预处理核心(上):预定义符号 + #define 常量与宏全解析 📝](#define 常量与宏全解析 📝)
-
- [前景回顾:文件操作核心速记 📂](#前景回顾:文件操作核心速记 📂)
- [一、预处理是什么?------ 编译前的"代码准备"工作 🔧](#一、预处理是什么?—— 编译前的“代码准备”工作 🔧)
- [二、预定义符号 ------ C语言内置的"信息工具" 🛠️](#二、预定义符号 —— C语言内置的“信息工具” 🛠️)
-
- [1. 常用预定义符号汇总](#1. 常用预定义符号汇总)
- [2. 使用举例:调试日志打印](#2. 使用举例:调试日志打印)
- [三、`#define`定义常量 ------ 代码的"常量开关" ⚙️](#define`定义常量 —— 代码的“常量开关” ⚙️)
-
- [1. 基本语法](#1. 基本语法)
- [2. 经典使用示例](#2. 经典使用示例)
- [3. 避坑要点:不要加多余分号](#3. 避坑要点:不要加多余分号)
- [四、`#define`定义宏 ------ 带参数的"代码替换" 🚀](#define`定义宏 —— 带参数的“代码替换” 🚀)
-
- [1. 基本语法](#1. 基本语法)
- [2. 简单示例:计算平方](#2. 简单示例:计算平方)
- [3. 宏的坑点:运算优先级问题](#3. 宏的坑点:运算优先级问题)
- [4. 优化方案:给参数和整体加括号](#4. 优化方案:给参数和整体加括号)
- [五、带有副作用的宏参数 ------ 隐藏的"陷阱" 🕳️](#五、带有副作用的宏参数 —— 隐藏的“陷阱” 🕳️)
-
- [1. 副作用示例:求两数最大值](#1. 副作用示例:求两数最大值)
- [2. 对比函数:无副作用问题](#2. 对比函数:无副作用问题)
- [六、宏替换的规则 ------ 预处理的"执行步骤" 📜](#六、宏替换的规则 —— 预处理的“执行步骤” 📜)
-
- [1. 宏替换的三步法则](#1. 宏替换的三步法则)
- [2. 宏替换的注意事项](#2. 宏替换的注意事项)
- [写在最后 📝](#写在最后 📝)
C 语言预处理核心(上):预定义符号 + #define 常量与宏全解析 📝
在搞定C语言文件操作的核心知识点后,我们迎来编译环节的关键一步------预处理 !预处理是C语言程序编译的第一个阶段,主要负责对代码中的预处理指令(以#开头,如#include、#define)进行处理。这一篇我们聚焦预处理的基础核心:预定义符号的使用、#define定义常量的技巧,以及#define定义宏的玩法与避坑指南,帮你夯实预处理的基础!
前景回顾:文件操作核心速记 📂
C 语言文件操作入门:文件基础认知 + 打开关闭 + 字符字符串读写精讲
C 语言文件操作进阶:格式化读写 + 二进制读写 + 随机读写进阶全解
C 语言文件操作高阶:读取结束判定 + 缓冲区原理 + 常见错误
回顾上一阶段的核心知识点,衔接预处理的学习:
- 文件操作标准流程:打开(fopen)→ 读写 → 判定结束 → 关闭(fclose),必检查fopen返回值,关闭后指针置空。
- 读取结束判定:文本文件看函数返回值(EOF/NULL),二进制文件看实际读写个数,
feof仅用于判定结束原因。 - 缓冲区核心:程序通过缓冲区间接读写磁盘,
fflush手动刷新,fclose自动刷新,避免异常退出导致数据丢失。
一、预处理是什么?------ 编译前的"代码准备"工作 🔧
C语言程序从源代码到可执行程序,需要经历 预处理 → 编译 → 汇编 → 链接 四个阶段。其中预处理阶段的核心任务是:对源代码中的预处理指令进行替换、删除等操作,生成"预处理后的代码"(后缀通常为.i),再交给编译器进行后续处理。
常见的预处理指令:
#include:头文件包含(我们最熟悉的,如#include <stdio.h>)#define:定义常量或宏#undef:移除宏定义#if、#elif、#else、#endif:条件编译- 预定义符号:C语言内置的特殊符号(如
__FILE__、__LINE__)
二、预定义符号 ------ C语言内置的"信息工具" 🛠️
C语言提供了一组内置的预定义符号,这些符号会在预处理阶段被自动替换为对应的值,无需我们手动定义,常用于调试、日志输出等场景。
1. 常用预定义符号汇总
| 符号 | 含义 | 类型 |
|---|---|---|
__FILE__ |
进行编译的源文件的完整路径 | 字符串 |
__LINE__ |
文件当前的行号 | 整数 |
__DATE__ |
文件被编译的日期(格式:Mmm dd yyyy) |
字符串 |
__TIME__ |
文件被编译的时间(格式:hh:mm:ss) |
字符串 |
__STDC__ |
编译器遵循ANSI C则为1,否则未定义 | 整数 |
💡 注意:VS编译器不支持
__STDC__,而GCC编译器支持。
2. 使用举例:调试日志打印
预定义符号最常用的场景就是打印调试信息,快速定位代码位置和编译时间:
c
#include <stdio.h>
int main()
{
printf("当前文件:%s\n", __FILE__);
printf("当前行号:%d\n", __LINE__);
printf("编译日期:%s\n", __DATE__);
printf("编译时间:%s\n", __TIME__);
return 0;
}
运行后会输出类似这样的结果:
当前文件:D:\test.c
当前行号:5
编译日期:Jul 15 2025
编译时间:15:30:20
三、#define定义常量 ------ 代码的"常量开关" ⚙️
#define是预处理指令中最常用的之一,用于定义常量(也叫"宏常量"),在预处理阶段会将代码中所有的常量名替换为对应的内容。
1. 基本语法
c
#define name stuff
name:宏常量名,建议全部大写,区分普通变量stuff:常量的内容,可以是数字、字符串、关键字、表达式等- 结尾不要加分号,否则会导致替换后出现多余分号
2. 经典使用示例
c
// 定义数字常量
#define MAX 1000
// 重定义关键字
#define reg register
// 定义死循环
#define do_forever for(;;)
// 定义switch的case快捷写法
#define CASE break;case
// 定义调试打印宏(多行用反斜杠续行)
#define DEBUG_PRINT printf("file:%s\tline:%d\t\
date:%s\ttime:%s\n", __FILE__, \
__LINE__, __DATE__, __TIME__)
💡 技巧:如果
stuff内容过长,需要分行写时,除了最后一行,每行末尾都要加反斜杠\(续行符),表示下一行是当前行的延续。
3. 避坑要点:不要加多余分号
这是初学者最容易踩的坑!如果在定义常量时加分号,替换后会多出一个分号,导致语法错误或逻辑错误。
c
// 错误写法
#define MAX 100;
// 代码中使用
int a = MAX;
// 预处理后变成:int a = 100;; 多了一个分号
四、#define定义宏 ------ 带参数的"代码替换" 🚀
#define不仅能定义常量,还能定义带参数的宏(也叫"宏函数"),本质是带参数的文本替换,在预处理阶段完成替换,没有函数调用的开销。
1. 基本语法
c
#define name(参数列表) stuff
参数列表:宏的参数,多个参数用逗号分隔name和(之间不能有空格,否则会被当成常量定义
2. 简单示例:计算平方
c
#include <stdio.h>
#define SQUARE(N) N*N
int main()
{
int a = 30;
int r = SQUARE(a);
// 预处理后替换为:r = a*a; 结果为900
printf("%d\n", r);
return 0;
}
3. 宏的坑点:运算优先级问题
当宏参数是表达式时,直接替换会因为运算符优先级导致结果不符合预期。
c
#include <stdio.h>
#define SQUARE(N) N*N
int main()
{
int a = 5;
int r = SQUARE(a+1);
// 替换后变成:r = a+1*a+1; 计算结果为5+5+1=11
// 而我们预期的是:(a+1)*(a+1)=36
printf("%d\n", r);
return 0;
}
4. 优化方案:给参数和整体加括号
解决优先级问题的核心是:给宏的每个参数加括号,再给整个表达式加括号。
c
// 优化1:给参数加括号
#define SQUARE(N) (N)*(N)
// 此时 SQUARE(a+1) 替换为 (a+1)*(a+1),结果正确
// 优化2:给整体加括号(应对更复杂场景)
#define DOUBLE(N) ((N)+(N))
// 比如计算 DOUBLE(a)*10,替换后为 ((a)+(a))*10,避免优先级问题
五、带有副作用的宏参数 ------ 隐藏的"陷阱" 🕳️
"副作用"指的是表达式求值时,会改变变量的值。比如a++、++a、a = b+1都属于带有副作用的表达式。这类表达式作为宏参数时,会因为宏的多次替换导致变量被多次修改,结果超出预期。
1. 副作用示例:求两数最大值
c
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
int a = 10;
int b = 12;
int m = MAX(a++, b++);
// 宏替换后变成:((a++)>(b++)?(a++):(b++))
// 执行过程:
// 1. 比较a++(10) > b++(12) → 不成立
// 2. 执行b++ → b变成14
// 最终m=13,a=11,b=14
printf("m=%d a=%d b=%d\n", m, a, b);
return 0;
}
2. 对比函数:无副作用问题
函数的参数是先求值,再传递给函数,不会出现多次修改的问题:
c
#include <stdio.h>
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 12;
int m = Max(a++, b++);
// 先求值:x=10, y=12 → m=12
// 再执行a++和b++ → a=11, b=13
printf("m=%d a=%d b=%d\n", m, a, b);
return 0;
}
六、宏替换的规则 ------ 预处理的"执行步骤" 📜
宏替换不是简单的文本替换,而是遵循固定的规则执行,确保替换的准确性。
1. 宏替换的三步法则
- 参数扫描 :调用宏时,先检查宏的参数,如果参数中包含
#define定义的符号(常量或宏),先替换这些符号; - 文本替换:将宏的替换文本插入到代码中,替换宏名和参数;
- 再次扫描 :对替换后的代码再次扫描,如果还有
#define定义的符号,重复上述步骤。
2. 宏替换的注意事项
- 宏不能递归:不能在宏的替换文本中调用自身,预处理阶段无法处理递归;
- 字符串常量不扫描:宏替换不会处理双引号内的字符串内容;
- 预处理独立于编译:宏替换发生在预处理阶段,编译阶段处理的是替换后的代码。
写在最后 📝
这一篇我们掌握了预处理的基础知识点:C语言内置的预定义符号,#define定义常量的语法和避坑要点,以及带参数宏的定义、坑点和优化方案。
宏的核心优势是无函数调用开销,执行速度快 ,但缺点也很明显:容易因为优先级和副作用导致问题,且无法调试。下一篇我们将深入学习宏与函数的对比、#和##运算符的妙用、条件编译和头文件包含的技巧,彻底吃透预处理!
核心要点总结
- 预定义符号是C语言内置的调试工具,无需定义直接使用;
#define定义常量时,结尾不要加分号,多行用反斜杠续行;- 定义带参数宏时,必须给参数和整体加括号,避免优先级问题;
- 带有副作用的表达式(如
a++)尽量不要作为宏参数; - 宏替换是文本替换,发生在预处理阶段,无函数调用开销。