C 语言预处理核心(上):预定义符号 + #define 常量与宏全解析

🏠个人主页:黎雁

🎬作者简介: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 语言文件操作高阶:读取结束判定 + 缓冲区原理 + 常见错误

回顾上一阶段的核心知识点,衔接预处理的学习:

  1. 文件操作标准流程:打开(fopen)→ 读写 → 判定结束 → 关闭(fclose),必检查fopen返回值,关闭后指针置空。
  2. 读取结束判定:文本文件看函数返回值(EOF/NULL),二进制文件看实际读写个数,feof仅用于判定结束原因。
  3. 缓冲区核心:程序通过缓冲区间接读写磁盘,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++++aa = 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. 宏替换的三步法则

  1. 参数扫描 :调用宏时,先检查宏的参数,如果参数中包含#define定义的符号(常量或宏),先替换这些符号;
  2. 文本替换:将宏的替换文本插入到代码中,替换宏名和参数;
  3. 再次扫描 :对替换后的代码再次扫描,如果还有#define定义的符号,重复上述步骤。

2. 宏替换的注意事项

  • 不能递归:不能在宏的替换文本中调用自身,预处理阶段无法处理递归;
  • 字符串常量不扫描:宏替换不会处理双引号内的字符串内容;
  • 预处理独立于编译:宏替换发生在预处理阶段,编译阶段处理的是替换后的代码。

写在最后 📝

这一篇我们掌握了预处理的基础知识点:C语言内置的预定义符号,#define定义常量的语法和避坑要点,以及带参数宏的定义、坑点和优化方案。

宏的核心优势是无函数调用开销,执行速度快 ,但缺点也很明显:容易因为优先级和副作用导致问题,且无法调试。下一篇我们将深入学习宏与函数的对比、###运算符的妙用、条件编译和头文件包含的技巧,彻底吃透预处理!

核心要点总结

  1. 预定义符号是C语言内置的调试工具,无需定义直接使用;
  2. #define定义常量时,结尾不要加分号,多行用反斜杠续行;
  3. 定义带参数宏时,必须给参数和整体加括号,避免优先级问题;
  4. 带有副作用的表达式(如a++)尽量不要作为宏参数;
  5. 宏替换是文本替换,发生在预处理阶段,无函数调用开销。
相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜10 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio