C语言 预编译详解
编译C程序涉及很多步骤,第1个步骤就是预编译(预处理)阶段,预编译是在源代码编译之前做一些文本性质的操作。包括删除注释、执行预处理指令。为了观察预编译阶段所做的事,环境使用Linux系统下的GCC编译器
程序编译完整步骤可以查看这篇博客:https://blog.csdn.net/kjl167/article/details/124157077
一、 预定义符号
ANSI C定义了一些预定义符号,它们表示不同含义__FILE__ :进行编译的源文件名__LINE__ :文件当前行号__DATE__ :文件被编译日期__TIME__ :文件被编译时间__STDC__ :如果编译器遵循ANSI C 其值为1,否则未定义

二、 #define 宏定义
#define是 C语言 和 C++ 中的一个预处理指令,其中的“#”表示这是一条预处理命令·。凡是以“#”开头的均为预处理命令,“define”为宏定义命令。建议将宏定义在文件最开始位置。
在 C++ 中,宏不受命名空间的影响,因为宏在预处理阶段,而命名空间是在编译阶段。
下面这个C++代码会编译失败,因为对MAX重复定义。
1 | namespace kjl |
2.1 定义不带参数的宏
用法:#define 标识符(宏名) 对应值
作用:在预编译时,将标识符替换为对应值,对应值可以是任何文本内容
例如:
1 |

注意:不应该在宏定义的尾部加上分号
1 | 例一 |

2.2 定义带参数宏
用法:#define name(parameter-list) stuff
作用:在预编译时,将传入参数列表中参数替换到宏体中对应参数执行某些操作
name :宏名
parameter-list:参数列表
stuff:宏体
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表和宏体会被当作不带参数的宏中的替换值
2.2.1 宏的一些问题
测试1:当传入参数中有表达式
改进:将宏体中参数用小括号包裹起来
测试2:当表达式旁边还有其他运算符

改进:将宏体用小括号包裹起来
结论:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的运算符或邻近运算符之间因为运算符优先级不同产生不可预料的相互作用。
2.2.2 小技巧
C语言支持:相邻字符串常量自动连接成一个字符串
例如:
1 |
|
输出
1 | Hello World |
我们可以将字符串常量作为宏参数,打印指定数据类型值
1 |
|
输出:
1 | The value is 10 |
2.3 宏的替换规则
在程序中使用宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
1 |
|
注意:
- 宏参数和#define 定义中可以包含其他#define定义的宏。但是宏不能出现递归。
- 当预处理器搜索#define定义的宏时候,字符串常量的内容并不被搜索。
1 |
|
2.3.1 # 作用
# :在宏中,#可以将宏参数名(不是参数值)转换为一个字符串
#x 替换为: “x”
#y 替换为: “y”
1 |
|
输出
1 | The value of x is 10 |
2.3.2 ## 作用
## :在宏中, ##可以把自己两边的符号连接成一个符号
p##f 替换为: pf
1 |
|
输出
1 | 10 |
2.4 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果
1 | x+1 //不带副作用 |
MAX宏可以证明具有副作用的参数所引起的问题,观察下面代码,你认为它将打印什么
1 |
|
输出
1 | x=6 y=10 z=9 |
1 | 说明: MAX(x++,y++) 预编译替换为: ((x++) > (y++) > (x++) : (y++)) , x++和y++都是后缀++所以先比较后++, |
2.5 宏与函数区别
宏常用于执行简单的计算,比如在两个表达式(数)中寻找其中较大(较小)一个
1 |
相较于函数完成这个功能,宏有2个优势:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型以及其他任何可以用>运算符来比较的类型,即宏是类型无关的
有些任务函数不能完成,函数参数无法是类型,而宏的参数可以出现类型
1 |
|

2.6 宏的命名约定
#define宏的行为和函数相比存在一些不同的地方,上文做了总结。由于这些不同之处,因此让程序员知道一个标识符究竟是宏还是一个函数非常重要。不幸的是,使用函数或宏的语法是完全一样的,所以语法本身并不能帮助你区分这两者
一个常见的约定是把宏名字全部大写
1 |
|
2.7 #define 与 typedef区别
C语言支持用typedef关键字对各种数据类型定义新名字
1 | typedef int int_t; //int_t是一个类型 |
有的人喜欢使用#define方式
1 |
|
强烈不推荐使用#define方式,因为它不能正确处理指针类型
1 | typedef int * int_p; |
三、 #undef
宏的作用域从定义位置开始,到文件结束。可以使用#undef移除一个宏定义
用法:#undef 宏名
四、命令行定义
许多C编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。当根据同一个源文件要编译一个程序的不同版本的时候,这个特性很有用。假定某个程序中声明了一个某个长度的数组,如果机器内存有限,这个数组必须很小,但是另外一个内存充沛机器上,数组能够大些。如果数组是用类似下面的形式进行声明“
1 | int array[ARRAY_SIZE]; |
在GCC编译器下可以使用 -D name=stuff,将name的值定义为stuff
五、 条件编译
在编译一个程序的时候,选定或忽略源文件中某条语句(某组语句)是很常见的。只用于调试程序的语句就是一个很明显的例子。它们不应该出现在程序的产品版本中,但我们可能并不想把这些语句从源代码中删除,因为在需要一些维护性修改时,可能需要重新调试这个程序,此时还需要这些语句,条件编译可以实现这个目的。
条件编译:满足条件编译某些代码,不满足条件不编译某些代码
5.1 #if #endif
语法:
#if constant-expression
statements
#endif
说明:constant-expression必须为常量表达式,如果它的值为非0,statements部分正常编译,否则预编译阶段删除它们。
之所以要常量表达式,因为这个是在预编译阶段进行条件判断,如果是变量,变量只有在程序执行阶段才赋值的。
5.2 #if #elif #else #endif
语法:
#if constant-expression
statements
#elif constant-expression
statements
#else
statements
#endif
说明:#elif子句出现次数不限,constant-expression(常量表达式)值非0编译下面statements部分,如果都不满足编译else对应statements部分,#else是可选的
1 |
|
5.3 是否被定义
#if defined(symbol)
#ifdef symbol
说明:上面两条语句等价
功能:当symbol符号存在,则编译#if与#endif之间语句
#if !defined(symbol)
#ifndef symbol
说明:上面两条语句等价
功能:当symbol符号不存在,则编译#if与#endif之间语句
5.4 嵌套指令
前面提到的指令可以嵌套定义在另一个指令内部,如下面代码所示
1 |
|
说明:这个例子中,根据操作系统类型选择不同的处理方案,在预处理指令前面加空白符,形成缩进,有利于提高可读性
六、 文件包含
#include 指令可以使另外一个文件的内容被包含到本文件内编译,就像它实际出现于#include指令的位置一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容取而代之。一个头文件被包含10次,那就实际被编译10次
6.1 文件包含两种方式
编译器支持两种不同方式#include文件包含:库函数头文件和自定义头文件
库函数头文件:#include <filename>
编译器直接去标准位置去查找,如果找不到就预编译错误,在UNIX(Linux)系统下标准位置为:/usr/include
自定义头文件:#include "filename"
编译器先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就预编译错误
对于库函数头文件和自定义头文件使用双括号或尖括号方式都可以,它们区别在于:1. 查找方式不同 2. 通过库函数头文件<> 自定义头文件” “ 这种约定可以判断一个头文件是库函数头文件还是自定义头文件
6.2 嵌套文件包含

头文件a.h和b.h都包含x.h文件,test.c文件又分别包含a.h、b.h文件,当预编译test.c文件时,x.h文件被包含2次
这种嵌套包含在绝大多数情况下出现在大型程序中,它往往需要很多头文件,因此发现这种情况并不容易,为了解决这个问题,可以使用条件编译。如果所有头文件都像下面这样编写,就可以解决问题:
1 |
|
当头文件第一次被包含时,_HEADERNAME_H未定义,条件判断为真,使用宏定义_HEADERNAME_H,并包含头文件内容。如果头文件再次被包含,条件判断为假,头文件内容不会再次被包含。
_HEADERNAME_H :按照头文件名进行取名,以避免其他头文件使用相同的名字而引起冲突。如头文件add.h, 则 _ADD_H