编译预处理是指在进行编译的第 1 遍扫描——词法扫描和语法分析之前所做的工作。
这些预处理命令是由 ANSI C 统一规定的,但它不是 C 语言本身的组成部分。预处理是 C 语言的一个重要功能,它由预处理程序完成。当对一个源文件进行编译时,系统将自动引 用预处理程序对源程序中的预处理部分进行处理,处理完成后自动开始编译,其示意图如 图 8.8 所示。
源程序 预处理程序
预处理 后的 源程序
目标 编译程序 程序
图 8.8 预处理示意图
C 语言提供了多种预处理功能,如宏定义、文件包含及条件编译等。合理地使用预处理功 能来编写程序,可以使程序便于阅读、修改、移植和调试,也有利于进行模块程序设计。本节 将介绍常用的几种编译预处理命令。
8.9.1 宏定义
在 C 语言源程序中允许用一个标识符表示一个字符串,称为“宏”。被定义为“宏”的标 识符称为“宏名”。编译预处理时程序中出现的所有宏名都用宏定义中的字符串来代换,称为
“宏代换”或“宏展开”。宏代换是由预处理程序自动完成的。在 C 语言中,宏分为有参数和 无参数两种,下面分别讨论这两种宏的定义和调用。
1.无参数宏定义
无参宏的宏名后不带参数。其定义的一般形式为:
#define 宏名 替换文本
功能:使用替换文本所表示的字符串来替换从定义的位置开始到该源程序文件结束的程 序中所出现的宏名。
其中,宏名用标识符表示,替换文本可以是任意字符串。前面介绍过的符号常量的定义 就是一种无参宏定义。通常对程序中反复使用的表达式进行宏定义。例如使用了无参宏定义 命令:
#define M 30
那么,预处理程序就将程序中所有的标识符 M 都用文字串 30 来代替。在预编译时将宏名 替换成字符串的过程称为“宏展开”。
宏定义命令在编译预处理中只是一种简单的置换,但是对于程序中用双引号括起来的字 符串不进行替换。替换文本所表示的字符串中可以含有任何字符,可以是常数,也可以是表 达式,预处理程序对它不做任何正确性检查。如有错误,则只能在编译已被宏展开后的源程序 时发现。
例 8.21 “宏展开”的应用举例。
#define M (x*x)
#include "stdio.h"
main() {
int s,x;
printf("Please input:");
scanf("%d",&x);
s=7*M;
printf("s=%d\n",s);
} 运行:
Please input:10↓
s=700
程序中首先进行宏定义,定义 M 为表达式 x*x,然后在 s=7*M;语句中进行宏调用。预处 理时经宏展开后该语句变为:
s=7*(x*x);
因此,当输入整数 10 到变量 x 中时,程序的运行结果为 700。在宏定义时必须十分注意,
应保证宏代换之后不会发生错误。
例 8.22 分析下列程序,写出程序的运行结果。
#define P 60
#include "stdio.h"
main() {
printf("P");
printf("\n");
} 运行:
P
本例中定义宏名 P 表示 60,但在 printf 函数中 P 被双引号括起来了,因此不做宏代换。
因此,程序的运行结果为 P,这表示把 P 当做字符串处理。
宏定义必须写在函数外,其作用域为宏定义命令起到源程序结束。如要终止其作用域,
可使用#undef 命令,其语法为:
#undef 宏名 例如:
#define M 30 /*定义 M*/
#undef M /*取消 M 的定义*/
宏定义允许嵌套,宏定义的字符串中可以使用已定义的宏名。宏展开时由预处理程序层 层代换。
例 8.23 分析下列程序的运行结果。
#define PI 3.1415926
#define R 10
#define L 2*PI*R
#define S PI*R*R
#include "stdio.h"
main() {
printf("L=%f\nS=%f\n",L,S);
}
运行结果为:
L=62.831852 S=314.159260
经过宏展开后,printf 函数中的输出项 L 被展开为:
2*3.1415926*10 S 被展开为:
3.1415926*10*10 则语句
printf("L=%f\nS=%f\n",L,S);
变为
printf("L=%f\nS=%f\n",2*3.1415926*4.5,3.1415926*10*10);
2.带参数宏定义
C 语言允许宏带有参数。宏定义中的参数称为形式参数,宏调用中的参数称为实际参数。
对于带参数的宏,在调用过程中,不仅要宏展开,而且要用实参代换形参。
带参宏定义的一般形式为:
#define 宏名(形参表) 字符串
在字符串中应含有各个形参。带参宏调用的一般形式为:
宏名(实参表)
例如有下列宏定义:
#define PI 3.1415926 /*无参数宏定义*/
#define S(r) PI*r*r /*带参数宏定义*/
使用语句
k=S(4.5); /*宏调用*/
调用这个宏时,用实参 4.5 代替形参 r,则经预处理宏展开后的语句为:
k=3.1415926*4.5*4.5; /*宏展开*/
例 8.24 分析下列程序。
#include "stdio.h"
#define MAX(a,b) (a>b)?a:b main()
{int x,y,max;
printf("Please input x,y:\n");
scanf("%d,%d",&x,&y);
max=MAX(x,y);
printf("max=%d\n",max);
} 运行:
Please input x,y:
8,3↓
max=8
程序的第 2 行是带参宏定义,用宏名 MAX 表示条件表达式(a>b)?a:b,形参 a 和 b 均出现 在条件表达式中。语句 max=MAX(x,y);为宏调用,实参 x 和 y 将代换形参 a 和 b。宏展开后该 语句为:
max=(x>y)?x:y;
该语句用于获得 x、y 中的较大者。
带参数宏定义与函数的含义是不同的,在带参数宏定义中,只是用实参替换形参,不存 在参数的传递。
8.9.2 文件包含
文件包含是 C 语言预处理程序的另一个重要功能。文件包含的一般形式为:
#include "文件名"
或
#include <文件名>
功能:用文件名指定的文件内容来替换命令行,从而将指定文件包含到使用命令行的文 件中来。如果文件名用尖括号括起来,则按系统指定的标准方式查找被包含文件;如果文件名 用双引号括起来,则先在使用包含命令的源文件所在的目录中查找被包含的文件,如果没有找 到,再按系统指定的标准方式检索其他目录。
前面已多次使用#include 命令来包含标准库函数的头文件。例如:
#include "stdio.h"
#include "math.h"
一个#include 命令只能包含一个文件,因此,如果要在一个源文件包含多个文件,则需要 使用多个包含命令。一般在一个源程序文件的开始处会有多个包含命令。通常被包含的文件为 头文件,也可以是 C 语言源程序文件,但是,包含 C 语言源程序文件时,在不同的编译系统 中,其使用方法是不同的,例如在使用 Visual C++与使用 TC 时是不同的。显然,如果对被包 含文件的内容进行了修改,则需要对包含该文件的源文件重新进行编译。另外,文件包含允许 嵌套,即在一个被包含的文件中又可以包含另一个文件。
使用文件包含,可以将一个大的应用系统划分为一个个小模块,由不同的项目小组成员 去同时进行研究与开发工作,便于实现模块化程序设计,提高工作效率。
8.9.3 条件编译
预处理程序提供了条件编译的功能。可以按不同的条件编译不同的程序部分,以及不同 的目标代码文件。这对于程序的移植和调试是很有用处的。为了初学者方便学习,可以将条件 编译概括为以下 3 种形式。
1.形式 1——判断标识符是否已定义
#ifdef 标识符 程序段1
#else 程序段2
#endif
其功能是:如果标识符已被#define 命令定义过,则对程序段1进行编译,否则对程序段2 进行编译。如果只有程序段1而没有程序段2,则形式 1 可简化为:
#ifdef 标识符 程序段
#endif
这里的程序段可以是语句组,也可以是命令行。这种条件编译对于 C 语言源程序的通用 性是很有好处的。如果一个 C 语言源程序在不同的计算机系统上运行,而不同的计算机又有 一定的差异(如有的计算机用 16 位存放一个整数,而有的则用 32 位存放一个整数),则往 往需要对程序做必要的修改,这样就降低了程序的通用性。这时,可以使用以下的条件编译 来处理。
#ifdef COMPUTER_A
#define INTEGER_SIZE 16
#else
#define INTEGER_SIZE 32
#endif
即如果 COMPUTER_A 在前面已被定义过,则编译命令行
#define INTEGER_SIZE 16 否则编译命令行
#define INTEGER_SIZE 32
这样,源程序就可以在不做任何修改的情况下用于不同的计算机系统。
2.形式 2——判断标识符是否未定义
#ifndef 标识符 程序段1
# else 程序段2
# endif
与形式 1 的区别是将 ifdef 改为 ifndef。其功能是:如果标识符未被#define 命令定义过,