一、什么是预处理指令?
介绍预处理指令之前,我们先来回顾一下C程序的编译过程,下图描述了C程序的编译过程。

图 1 C程序编译过程
C程序编译过程分为四个阶段:第一阶段是预处理阶段;第二阶段是编译阶段;第三阶段是汇编阶段;第四阶段是链接阶段。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了C语言的编译系统。
预处理阶段主要用于处理源文件中的预处理指令,并根据指令对源代码进行修改和补充。例如:“#include”指令将头文件的内容补充到源文件内;“#define”指令用于宏定义,通过宏定义可以进行条件编译和内容替换,条件编译指令将决定那些代码被编译,而哪些是不被编译的。
二、文件包含指令
“#include”是C语言提供的包含文件的预处理指令,指令后面为要包含的文件名称,用来引入C语言的头文件。例如:
#include <stdio.h>
“stdio.h”文件在C语言中被称为头文件,该文件的扩展名为“.h”,“stdio.h”为C语言函数库的头文件,它定义了开发者可以使用的输入输出函数。文件名称使用一对英文尖括号将文件名称括起来,编译器会在系统路径下查找要包含的文件。开发者自己定义的头文件一般放置在项目目录下,这种情况下就不适合使用一对英文尖括号将文件名称括起来,而是使用一对英文双引号将文件名称括起来,编译器会在当前项目目录下查找要包含的头文件。例如:
#include "my.h"
一个 #include指令只能包含一个头文件,当一个C文件需要包含多个头文件时,需要使用多个#include指令。
例:#include练习
编写一个计算矩形面积的程序,要求计算矩形面积的函数声明放置在area.h头文件,函数定义放置在area.c文件,程序主文件为main.c。程序结构如下图所示。

程序清单 area.h
/*
函数功能:计算矩形面积
**/
int rect_area(int w,int h);
程序清单 area.c
/*
函数定义
**/
#include "area.h" // 包含函数声明头文件
int rect_area(int w,int h) {
return w * h;
}
程序清单 main.c
#include <stdio.h> //包含系统输入输出头文件
#include "area.h" //包含计算面积函数头文件
int main() {
printf("area:%d\n",rect_area(30,20));
}
案例在area.h声明了rect_area函数,在 area.c定义了rect_area函数。把函数的声明和定义分开实现,可以将一组相关的函数和变量的声明集中在一个地方,以便在多个源文件中共享和重复使用。
三、宏定义
宏定义主要用于源代码代内的文本替换,它允许用一个标识符(和C语言语法的标识符有所不同,该标识符可以包含一些特殊符号)来表示一个字符串、表达式或数值, 该表示称为“宏”。被定义为“宏”的标识符称为“宏名”,对应的字符串、表达式或数值为宏定义的值。在C代码编译过程的预处理阶段,编译器会把源代码内的所有“宏名”替换为宏定义的值。使用宏定义可以提高程序的通用性、易读性,减少因为输入错误产生的各种问题,同时还便于修改。
例1:应用宏定义定义常量
#include <stdio.h>
// 定义常量PI的值为3.1415926
#define PI 3.1415926
int main() {
printf("PI = %f\n", PI); // 输出结果为PI = 3.141593
return 0;
}
案例1给出了应用宏定义来定义常量的示例。语句:
#define PI 3.1415926
为定义常量PI的宏定义,其中“#define”是宏定义指令,“PI”是标识符,“3.1415926”是宏定义的值。
C代码编译过程的预处理阶段,会进行宏定义替换,将源代码内所有标识符“PI”替换为数值“3.1415926”。替换后的代码如下:
int main() {
printf("PI = %f\n", 3.1415926);
return 0;
}
例2:应用宏定义定义表达式
#include <stdio.h>
#define MAX(X,Y) X>Y?X:Y
int main()
{
printf("%d", MAX(3, 5));
return 0;
}
案例2给出了应用宏定义来定义表达式的示例。语句:
#define MAX(X,Y) X>Y?X:Y
为定义表达式“X>Y?X:Y”的宏定义,其中“#define”为宏定义指令,“MAX(X,Y)”是标识符,该标识符表达的意义类似C语言函数声明,“MAX”是函数名称,“(X,Y)”是传入的参数,“X>Y?X:Y”是表达式。
C代码编译过程的预处理阶段,会进行宏定义替换,将源代码内所有标识符“MAX(3, 5)”替换为数值“3>5?3:5”。替换后的代码如下:
int main()
{
printf("%d", X>Y?X:Y);
return 0;
}
例3:应用宏定义模拟printf函数
#include <stdio.h>
#define PRINTF(value) printf("value=%d",value)
int main() {
PRINTF(10);
}
案例3给出了应用宏定义来模拟printf函数的示例。语句:
#define PRINTF(value) printf("value=%d",value)
“PRINTF(value)”是宏名,其中value是需要传入的参数,宏定义的值为printf("value=%d",value)。在替换过程过程中,value(不包含引号内的value)被传入的参数替换。替换后的代码如下:
int main() {
printf("value=%d",10);
}
例4:应用宏定义可能出现的问题
#include <stdio.h>
#define MIN(A,B) A < B ? A:B
int main() {
printf("%d",MIN(3,4));
printf("%d",2 * MIN(3,4));
}
案例4定义了宏名为“MIN”的宏定义,用于比较两数的大小,返回较小的数。main()函数代码段调用了两次宏定义,第一次调用没有问题,替换后的代码如下:
printf("%d",3 < 4 ? 3:4);
第二次调用就有问题了,没有得到我们预期的结果,替换后的代码如下:
printf("%d",2 * 3 < 4 ? 3:4);
观察替换后的语句,输出是4,而不是预期的6,问题在于没有保证宏体被替换后整体的优先级最高,要确保宏体替换后优先级最高,可以如下定义宏:
#define MIN(A,B) (A < B ? A:B)
替换后的代码如下:
printf("%d",2*(3 < 4 ? 3:4));
再观察替换后的语句,可以得到我们预期的结果。
四、条件编译
我们在编写程序的过程中,难免会出现一些语法错误、逻辑错误或程序缺陷,这些问题统称为程序BUG。语法错误在编译阶段可以被编译器发现,但逻辑错误和程序缺陷只有在运行阶段才能发现,程序员排查这些问题的主要方法之一是输出程序调试信息,即在程序运行过程中输出程序的状态信息,如变量的取值、运行中的数据变化是否符合预期等待,通过观察这些调试信息,可以比较容易地定位出现问题的代码段。
要输出调试信息,需要在代码中添加调试语句,用于输出程序调试信息。这些调试语句仅用于调试程序,不会影响到程序的运行,但会降低程序的运行效率。因此程序调试完成后,需要及时移除这些调试语句。直接从代码中删除调试语句,并不是最好的方法,因为这些调试语句还有可能被再次使用。
最好的方法就是采用条件编译,使用宏定义来定义一个常量,在代码中判断常量是否被定义,若常量被定义,编译器对调试语句进行编译,否则编译器会忽略调试语句。
例1:使用条件编译控制输出
#include <stdio.h>
#define DEBUG
int main() {
sum(10,2);
}
int sum(int a,int b){
#ifdef DEBUG
printf("a=%d b=%d",a,b);
#endif
return a/b;
}
例1代码段的宏定义“DEBUG”没有值,类似“DEBUG”没有值的宏定义,一般用到条件编译语句,不进行文本的替换。
sum(int a,int b)是自定义的函数,函数将一段完成特定功能的代码封装起来,函数通过参数引入外部数据,函数内代码执行完成后,可以返回值,也可以不返回值。sum函数计算传入参数a和b的商,并返回计算结果,若“DEBUG”被定义,则输出参数a和b的值,用户可以通过控制台观察a和b的值;若“DEBUG”未被定义,则不输出参数a和b的值。
代码段:
#ifdef DEBUG
printf("a=%d b=%d",a,b);
#endif
属于条件编译。
“#ifdef”和“#endif”是条件编译指令,“#ifdef”指令后面为宏定义,若该宏定义存在,则编译“#ifdef”和“#endif”之间的代码,否则该段代码被忽略。
与指令“#ifdef”相反的指令为“#ifndef”,即指令后面的宏定义若不存在,则编译“#ifdef”和“#endif”之间的代码,否则该段代码被忽略。
条件编译还提供了类似条件结构的指令:
“#if”、“#elif”和“#else”,这些指令的使用方法和条件结构基本相同。常用格式如下:
#if 常量表达式1
// ... some codes
#elif 常量表达式2
// ... other codes
#elif 常量表达式3
// ...
...
#else
// ... statement
#endif
常量表达式可以是包含宏、算术运算、逻辑运算等等的合法C常量表达式,如果常量表达式为一个未定义的宏, 那么它的值被视为0。条件编译语句的结尾必须为指令“#endif”。
例2:条件编译指令使用案例
#include <stdio.h>
#define OP 1
int main()
{
#if( OP == 1 )
printf("This is first printf...\n");
#else
printf("This is second printf...\n");
#endif
return 0;
}
例2给出了条件编译指令的用法,若OP的值为1,则编译指令“#if”下的语句,否则编译指令“#else”下的语句。