预处理阶段的知识还理解不了?本文将深入浅出讲解预处理阶段的核心知识点。期待你的到来 - 指南
2025-09-13 15:17 tlnshuju 阅读(19) 评论(0) 收藏 举报前言:通过上篇文章编译和链接的知识,我们深刻理解了源文件是如何变成可执行目标文件,这篇文章将深入探讨,编译过程中的预处理环节。

一、预处理符号
C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。
① __FILE__ 进⾏编译的源⽂件
② __LINE__ ⽂件当前的⾏号
③ __DATE__ ⽂件被编译的⽇期
④ __TIME__ ⽂件被编译的时间
⑤ __STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
代码示例:通过打印这些标识符,得到文件相关信息
#include
int main()
{
printf("%s\n", __FILE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%d\n", __LINE__);
}
二、 #define 定义常量
基本语法:
#define name(替换名) stuff
这种宏定义通常用于:
①定义常量
②定义简单的函数或代码片段
③条件编译控制
代码示例1:用forever这个名字替换for( ; ;)这个代码语句
#include
#define forever for( ; ; )
int main()
{
forever
;
//相当于  for( ; ; )   { ; }
}代码示例2:用PI 这个名字替换 3.1415962
#include
#define PI 3.1415962
int main()
{
printf("%f",PI);
}
注意事项:在define定义标识符的时候,不要在最后加上 ;
如果加上了; 可能在某些场景中会出现错误
代码示例:因在#define定义常量时,加上 ; 导致出现错误。
#define MAX 1000;
int main()
{
if(true)
max = MAX;
else
max = 0;
return 0;
}
//在预处理截断,#define定义的宏,会替代在代码中。
//如下所示
int main()
{
if(true)
max = 1000;;
//因有两个分号,导致出现语法错误,因为if后没有跟{},只能匹配一句代码,导致else不能正确匹配。
else
max = 0;
return 0;
}三、#define定义带有参数的宏
#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(define macro)
#define name( parament-list ) stuff
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中
例如:用#define 定义一个求和的宏 #define ADD(x,y) x+y
温馨提示:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。
代码示例:通过#define 定义一个求平方数的宏
#include
#define SQUARE(x) x*x
int main()
{
int a = 5;
int c = SQUARE(a);   //经过预处理以后就会变成int c=a*a;
printf("%d\n", c);  //打印结果为25
return 0;
}这样定义的宏是否完全正确呢? 其实不然,在某些情况下会出现bug
代码示例:用SQUARE计算SQUARE(a+1)的结果会是36吗?
#include
#define SQUARE(x) x*x
int main()
{
int a = 5;
int c = SQUARE(a+1);
printf("%d\n", c);
return 0;
}实际打印结果如下:

为什么会是11呢?
因为#define定义的SQURE并不会像函数一样计算好a+1后传入,而是直接进行替换操作,在预编译阶段直接将宏进行替换,实际上的代码如下所示:
int c =a + 1 * a + 1;
这样就导致并不会像我们想象的一样,计算结果为36,而是计算结果为11。所以说在定义宏的时候不要吝啬括号,用括号单独将x括起来,使其符合我们预想结果。
通过修改成如下代码就可以避免这样的问题:
#include
#define SQUARE(x)  ( (x)*(x) )
int main()
{
int a = 5;
int c = SQUARE(a+1);
printf("%d\n", c);
return 0;
}打印结果如下:

四、#define定义副作⽤的宏参数
        当宏参数在宏的定义中出现超过⼀次计算的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可
能出现危险,导致不可预测的后果,副作⽤就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作⽤
x++;//带有副作⽤
代码示例:MAX宏可以证明具有副作⽤的参数所引起的问题
//带有副作用的宏
#define MAX(X,Y) ((X)>(Y) ? (X):(Y))
int main()
{
int a = 3;
int b = 5;
int m = MAX(a++,b++);
//经过预处理后得到的结果为: (a++)>(b++) ? (a++):(b++)
//m=6 a=4 b=7
printf("a=%d b=%d m=%d", a, b, m);
return 0;
}在经过预处理后 int m =(a++)>(b++) ? (a++):(b++)
先计算条件
(a++) > (b++):
- 先使用
a和b的当前值比较:3 > 5?结果为假- 比较后,
a和b各自自增:a=4,b=6
条件为假,执行冒号后的
(b++):
- 使用当前
b的值(6)赋值给m,所以m=6- 赋值后,
b再次自增:b=7这样就会导致变量被多次求值,偏离我们预想的结果,所以在进行定义宏时,要避免这种永久性改变值的代码段。
五、宏的替代规则
在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。
1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
六、宏与函数的对⽐
宏通常被应⽤于执⾏简单的运算,而函数用于执行相较于复杂的代码段。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。
代码如下:
#define MAX(a, b) ((a)>(b)?(a):(b))
写成宏的优点如下:
①⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
②更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。
和函数相⽐宏的劣势:
1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
2. 宏是没法调试的。
3. 宏由于类型⽆关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type )malloc(num  sizeof(type))
#include
int main()
{
//使⽤
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 sizeof(int));
return 0;
}宏和函数在 C/C++ 中都是实现代码复用的方式,但本质和特性有很大差异。以下从多个维度对比两者的核心区别:
| 对比维度 | 宏( #define) | 函数( function) | 
|---|---|---|
| 本质 | 预处理阶段的文本替换(无语法检查,仅做字符串替换) | 编译后生成的可执行代码块(有明确的入口和出口) | 
| 执行阶段 | 预处理时完成替换,运行时无额外操作 | 运行时通过函数调用执行(需经历压栈、跳转、返回等过程) | 
| 参数处理 | 1. 无类型检查,可接受任意类型参数(如 MAX(3, 5.5)合法)2. 参数会被原样代入替换文本,可能被多次求值(如 MAX(a++, b++)中a++被执行 2 次) | 1. 有严格的类型检查,参数类型必须与函数定义匹配(或兼容) 2. 参数仅在调用时求值一次,结果传入函数(无多次执行问题) | 
| 返回值 | 无 “返回值” 概念,替换后的代码直接参与上下文运算 | 有明确的返回值类型,通过 return语句返回结果 | 
| 开销 | 无函数调用开销(无压栈、跳转等操作),但可能导致代码膨胀(多次替换会生成多份代码) | 有函数调用开销(压栈参数、跳转地址等),但代码更紧凑(仅一份函数实现) | 
| 副作用风险 | 高。若参数含副作用表达式(如 a++、printf()),会因多次替换导致意外结果 | 低。参数仅求值一次,副作用仅执行一次(与预期一致) | 
| 调试 | 困难。预处理后宏已被替换,调试时看到的是替换后的代码,而非原始宏定义 | 容易。可通过断点单步调试,直接查看函数内部执行过程 | 
| 递归支持 | 不支持。宏替换是预处理阶段的文本操作,无法实现递归逻辑 | 支持。函数可直接调用自身实现递归(如斐波那契数列计算) | 
| 适用场景 | 1. 简单常量定义(如 #define PI 3.14)2. 短代码片段复用(逻辑简单,无复杂流程) 3. 需要跨类型通用的简单操作(如通用的 MAX逻辑) | 1. 复杂逻辑实现(多步运算、分支循环等) 2. 需要类型安全和可调试性的场景 3. 需递归或频繁调用的逻辑(避免代码膨胀) | 
七、条件编译
在编译⼀个程序的时候,我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的,因为我们有条件编译指令。
代码示例:调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
#include
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}条件编译的作用:
当保留
#define __DEBUG__时:
预处理阶段会保留#ifdef和#endif之间的printf语句,程序运行时会依次打印0~9,方便开发者确认数组赋值是否正确。
当注释掉
#define __DEBUG__时:
预处理阶段会删除#ifdef和#endif之间的代码(相当于从未存在过),程序运行时不会打印任何内容,仅完成数组赋值。因此可以通过注释或则保留
#define __DEBUG__来控制打印操作,简单说,这是一种 “一键开启 / 关闭调试模式” 的技巧,在实际开发中非常实用。
此外常见的条件编译如下所示:
①单个#if语句
#if 常量表达式 //... #endif代码示例:
//例如:定义常量,来控制语句是否进行 #define __DEBUG__ 1 int main() { //在预处理阶段会被替换为 #if 1 #if __DEBUG__ //判断成功输出下面语段 printf("判断成功,进行打印操作。"); #endif return 0; }
②多个#if语句
.多个分⽀的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif代码示例:
#define MAX 10 #include int main() { #if MAX>1 printf("MAX的值大于1"); #elif MAX1 }预处理阶段的判断过程
首先判断
#if MAX > 1:
由于MAX被定义为10,10 > 1的条件为真,因此会保留该分支下的代码printf("MAX的值大于1");。
后续的
#elif MAX < 10和#else分支:
因为第一个条件已经满足,预处理阶段会直接跳过后续所有分支(#elif和#else中的代码会被删除)。
③判断是否被定义
判断是否被定义,若被定义则为执行语句 1.#if defined(symbol) 2.#ifdef symbol判断是否没被定义,若未定义则执行语句 1.#if !defined(symbol) 2.#ifndef symbol代码示例:
#include #define MAX 100 int main() { //下面两个语句是等价表达 #if defined(MAX) printf("MAX已经被定义\n"); #endif #ifdef MAX printf("MAX已经被定义"); #endif return 0; }#include int main() { //下面两个语句是等价表达 #if !defined(MAX) printf("MAX未定义"); #endif #ifndef MAX printf("MAX未定义"); #endif return 0; }
八、头文件的包含
头文件的包含分为两种形式:本地头文件包含和库头文件包含。
1.本地头文件包含
比如:我们自己写了一个头文件:
#include "add.h"
①查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件,如果在标准位置查找不到,则不包含该头文件
②在预处理阶段,#头文件名,会被头文件中的代码所替代。
示例:add.h的头文件
#pragma once
#ifndef __ADD_H__
#define __ADD_H__
int Add(int, int);
#endif示例:add.c头文件的实现
int Add(int x, int y)
{
return x + y;
}示例:在main.c中调用头文件
#include "add.h"    //在预处理阶段会被替代为 int Add(int,int); 相当于了函数的声明
#include
int main()
{
int a = 3;
int b = 5;
int c = Add(a, b);    //调用Add函数
printf("%d", c);
return 0;
}2.库⽂件包含
示例:vs自带库中的头文件:
#include <stdio.h>
温馨提示:查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
Linux环境的标准头⽂件的路径:
/usr/include
VS环境的标准头⽂件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
思考:如果用#include “库头文件”,能够被找到吗?
答案是可以,但是这样做查找的效率就低些,先要查找本地源文件后,再去对应的路径查找,导致浪费了一定的实践。当然这样也不容易区分是库⽂件还是本地⽂件了,所以一般不推荐这样查找库头文件。
3.头文件的重复包含
我们已经知道, #include 指令可以使另外⼀个⽂件被编译,就像它实际出现于 #include 指令的地⽅⼀样,这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
但值得注意的是:
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较大。
那我们能否避免头文件重复包含呢?
显然在一个大型程序中难免会多次包含,如下图所示:

这张图展示了一个C 语言项目的文件依赖关系:
Add.h(头文件)和Add.c(源文件)是 “基础功能模块”,通常用来声明、定义一些基础函数(比如加法相关的函数)。
test1.c和test2.c是 “测试 / 功能模块”,它们会依赖Add.h/Add.c里的功能(比如调用加法函数做测试或实现特定逻辑)。
main.c是 “主程序模块”,它会依赖test1.c和test2.c,把这些模块的代码组织起来,让整个程序运行。这样main.c源文件中,相当于包含了两次头文件,这就导致了编译压力的增大。
能否有办法解决这样的问题,减少编译压力呢?显然是有方法的。
通过使用条件编译,就可以解决这样的问题。
如下列代码所示:
#pragma once #ifndef __ADD_H__ #define __ADD_H__ int Add(int, int); #endif如果没有定义__ADD_H__,则进行定义,如果已经定义__ADD_H__,则会跳过该语句,如果多次包含这个头文件,就只会编译第一个头文件中的代码,其余头文件因为判断失败,在预处理阶段就会被省略。
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

 
                     
                    
                 
                    
                 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号