本文结合工作经验,研究C语言中常见的预处理指令的用法。
文章目录
- 1 预处理指令概念
- 2 常见的预处理指令
- 2.1 #include包含头文件
- 2.2 #define定义宏
- 2.2.1 类对象宏(object-like macro)
- 2.2.2 类函数宏(function-like macro)
- 2.3 条件编译
- 3 总结
1 预处理指令概念
编译器编译C代码的第一个阶段就是预处理。预处理阶段会对预处理指令进行处理,将C代码“翻译”成另一个样子,为后续的编译、汇编、链接过程做准备。下面每个章节会研究一些常见的预处理指令。
2 常见的预处理指令
2.1 #include包含头文件
#include应该是最常见的预处理指令了,基本上每个C文件都会通过include包含若干头文件,或者头文件嵌套包含头文件。在预处理阶段,编译器会将include包含的头文件展开,写到C文件中。例如下面的C文件包含了一个头文件。
//demo.c
#include "demo_type.h"
uint8 demo(uint8 a,uint8 b)
{
return a + b;
}
//demo_type.h
typedef unsigned char uint8;
经过预处理过程,头文件的内容被展开到了C文件include的地方,这个头文件就不再需要了。
//预处理后的demo.c
typedef unsigned char uint8;
uint8 demo(uint8 a,uint8 b)
{
return a + b;
}
由此,这个C文件就可以使用头文件中定义的类型了。
再进一步思考,增量式编译的编译器会对包含了修改过的头文件的C文件重新编译,因此C文件不要包含多余的头文件,以免增加编译时间。
对于头文件嵌套的情况,会一层一层展开来。
2.2 #define定义宏
2.2.1 类对象宏(object-like macro)
通过#define可以定义一个宏,预处理阶段的时候,如果在代码中遇到一个宏,就会将其替换成宏所对应的内容。首先看一下不用宏定义的代码,例如如下代码:
//circle.c
float cal_area(float radius)
{
return 3.14 * radius * radius;
}
函数输入半径,返回圆的面积。其中用到了圆周率,直接将数值3.14写道代码中。这样的数字被称为“魔法数字”。正确的做法是将其定义为一个宏,然后在函数中使用这个宏。
//circle.c
#define PI 3.14
float cal_area(float radius)
{
return PI * radius * radius;
}
这样做有两个好处,首先,其他人阅读代码的时候,对于数字很难理解其中的含义,但是宏定义是可以从字面上知道意义的,可以增加代码的可读性。其次,如果代码中多处用到一个同样的值,又需要修改这个值(譬如将3.14改成3.1415926),就可以直接修改这个宏定义后面的数值。
2.2.2 类函数宏(function-like macro)
定义类函数宏也是使用#define定义一个看起来类似于函数的宏,使用的时候就像调用函数一样,例如如下代码:
#include <stdio.h>
#define MAX(a, b) (((a) < (b)) ? (b) : (a))
int main()
{
int a = 1;
int b = 2;
int c = MAX(a, b);
printf("c = %d \r\n", c);
}
MAX(a, b)用来判断传入的两个参数a和b,返回较大的值。该代码经过预处理之后的i文件的片段如下:
int main()
{
int a = 1;
int b = 2;
int c = (((a) < (b)) ? (b) : (a));
printf("c = %d \r\n", c);
}
这里直接展开了类函数宏。
从工作经验来看,当实现的需求比较简单时(例如上面比较大小),可以使用类函数宏,这样可以减少系统资源使用;当需要实现比较复杂的算法,还是应该使用函数或者内联函数,这样更有利于程序的debug。
另外,类函数宏使用的时候还可能出现一些没考虑到的问题。例如下面代码参考CPrimerPlus。
#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
int x = 5;
printf("x = %d \r\n", x);
printf("SQUARE(x) = %d \r\n", SQUARE(x));
printf("SQUARE(x+2) = %d \r\n", SQUARE(x+2));
printf("100/SQUARE(x) = %d \r\n", 100/SQUARE(x));
printf("SQUARE(++x) = %d \r\n", SQUARE(++x));
}
打印出来的结果是:
第一个SQUARE(x)的计算结果是正确的,但是后3个都和预期不符合。这是因为预处理器直接将字符替换的缘故。原来的表达式和预处理后的表达式如下表,就可以很容易理解了。
宏 | 预处理后 | 计算结果 |
---|---|---|
SQUARE(x+2) | x+2*x+2 | 5+2*5+2 = 17 |
100/SQUARE(x) | 100/x*x | 100/5*5 = 100 |
SQUARE(++x) | ++x*++x | 7*7 = 47 |
上面第三条的运算首先是做两次++x,将x自加为7,再进行乘法。
解决表格中的前两个问题很简单,只要把宏加上完整的括号就行,例如如下:
#include <stdio.h>
#define SQUARE(X) ((X)*(X))
int main()
{
int x = 5;
printf("x = %d \r\n", x);
printf("SQUARE(x) = %d \r\n", SQUARE(x));
printf("SQUARE(x+2) = %d \r\n", SQUARE(x+2));
printf("100/SQUARE(x) = %d \r\n", 100/SQUARE(x));
printf("SQUARE(++x) = %d \r\n", SQUARE(++x));
}
打印结果为:
但是对于第三条自加的问题还是无法解决,书中推荐不要使用这种方式。
2.3 条件编译
条件编译也是一种常用的预处理指令,预处理过程中可以通过某种条件来决定保留哪些代码块。例如,代码中需要定义一个变量,但是在仿真的过程中将其定义为全局变量,在发布的时候将其定义为局部变量。
#include <stdio.h>
#ifdef Simulation
int a = 5;
#endif
int main()
{
#ifndef Simulation
int a = 10;
#endif
printf("a = %d \r\n", a);
}
上述代码的意思是,当定义过Simulation这个宏的时候,将变量a定义为全局变量,赋值为5;如果没定义过Simulation这个宏,就将a定义为局部变量,并赋值为10.
这样做的好处是将同一版代码中兼容两种定义方式,通过定义一个宏来切换。条件编译还有很多灵活的用法。
3 总结
本文总结了工作中常用的一些预处理指令,以及使用的范例。
>>返回个人博客总目录