C语言基础:预处理指令的使用

本文结合工作经验,研究C语言中常见的预处理指令的用法。

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 总结

本文总结了工作中常用的一些预处理指令,以及使用的范例。

>>返回个人博客总目录

猜你喜欢

转载自blog.csdn.net/u013288925/article/details/131563383
今日推荐