•251•第12章C语言的编译预处理C语言属于高级语言,用C语言编写的程序称为源程序,这种用高级语言编写的源程序计算机是不能直接执行的,必须经过C语言的编译系统把源程序编译成目标程序(机器指令构成的程序)并连接成可执行程序,计算机才可以执行。因此,用C语言来处理问题,必须经过程序的编写→编译及连接→运行三个主要过程。然而,为了减少C源程序编写的工作量,改善程序的组织和管理,帮助程序员编写易读、易改、易于移植、便于调试的程序,C语言编译系统提供了预编译功能。所谓的预编译功能是指:编译器在对源程序正式编译前,可以根据预处理指令先做一些特殊的处理工作,然后将预处理结果与源程序一起进行编译。C语言提供的编译预处理功能主要有三种:文件包含、宏定义、条件编译。这三种功能分别以三条编译预处理命令#include、#define、#if来实现。编译预处理指令不属于C语言的语法范畴,因此,为了和C语句区别开来,预处理指令一律以符号“#”开头,以“回车”结束,每条预处理指令必须独占一行。12.1文件包含预处理“包含”的英文单词为“include”,所谓“文件包含”预处理,就是在源文件中通过“#include”命令指示编译器将另一段源文件包含到本文件中来。例如,源文件f1.c中有一句“#includef2.c”编译预处理命令,如图12-1(a)所示。编译预处理后文件f1.c的完整结构如图12-1(c)所示。图12-1文件包含编译预处理命令编译时先将f2.c(图12-1(b))的内容复制嵌入到f1.c(图12-1(a))中来,即进行“包含”预处理,然后对调整好的完整的f1.c(图12-1(c))进行编译,得到相应的目标代码。换句话说,由f1.c和f2.c组成程序的目标代码(.obj)和用一个源文件(类似于图c)的目标代码(.obj)完全一样。但是用#include包含f2.c的方式编写程序可以使其他的程序重用f2.c的代码,并且使源文件简洁明了。“文件包含”指令有两种使用方式:第一种形式,用尖括号(即小于号、大于号)括起被包含源文件的名称:#include文件名f1.c………………………………………………………………………………………………………………………#includef2.c………………………………………………………………………………………………f1.cf2.c(a)预编译前(b)(c)预编译后………………………•252•第二种形式,用双引号括起被包含源文件的名称:#include文件名文件名按操作系统的要求定义,可以包括路径信息,例如:#includemath.h#includexyz.c#includec:\bc\mydir\head2.h#include文件名方式常用来“包含”系统头文件。系统头文件一般存储在系统指定的目录中,如TurboC的include子目录。当C编译器识别出这条#include文件名命令后,它不搜索当前子目录,而直接到系统指定的包含子目录(即include子目录)中去搜索相应的头文件,并将搜索到的头文件的内容“包含”到“主”文件中来。#include文件名方式常用来“包含”程序员自己建立的头文件。当编译器识别出这条#include文件名命令后,它先搜索“主”文件所在的当前子目录,如果没找到再去搜索相应的系统子目录。注意,所谓文件包含,指的是在“主”文件源程序中嵌入另一些源程序语句,形成一个完整的源程序去进行编译。所以只能包含源文件而不能去包含目标文件。如果源文件1中包含源文件2,而源文件2中又包含源文件3,这就是所谓的嵌套包含。例12.1将若干个系统头文件包含到本文件中来。#includestdio.h#includemath.h#includestdlib.hvoidmain(){…}其中stdio.h头文件含有与标准输入输出操作有关的函数的原型声明等,如getc、putchar函数;math.h中则是一些数学函数的原型声明;而atoi函数、exit函数等的原型声明在stdlib.h头文件中。这些都是系统头文件,存储在系统指定的include子目录中,所以程序中使用包含命令时用括起头文件名,以便C编译器直接搜索系统子目录,快速寻找到这些头文件。一条#include命令只能包含一个文件,若要包含多个文件就必须使用多条#include命令。一个C程序通常由多个源文件组成,每个源文件都是一个可独立编译的程序单位。在将程序分解成多个源文件后,必须计划每个源文件中哪些信息其他文件可见(以源文件形式提供),哪些不可见(以目标代码形式提供)。我们通常的做法是把其他文件可见的信息放在一个称为“头文件(.h)”的源文件中,在需要的文件中用#include预编译指令包括进去。“头文件”中可以包含哪些代码,不能包含哪些代码?C语言的语法没有强行的规定。根据经验的总结,以下内容放在头文件中比较合适:(1)包含指令(嵌套),如:#includestdio.h(2)函数声明,如:externintfn(inta);(3)类型声明,如:enumBOOLEAN{false,true};•253•(4)常量定义,如:constfloatpi3.14159;(5)数据声明,如:externintm;externinta[];(6)宏定义,如:#definePI3.1459而对于函数的定义,数据的定义等代码不宜包含在头文件中。12.2宏定义预处理熟悉汇编语言的读者都知道,宏是宏汇编语言的一个组成部分。C语言借鉴这种特点,同样具有宏的组成。C语言宏定义的简单形式是不带参数的宏定义,即符号常量定义,而带参数的宏定义则是它的复杂形式。12.2.1不带参数的宏定义不带参数的宏定义常用来定义符号常量。程序总是用来处理具体问题的,因此程序中用到的常量一般都有具体的物理含义。但从常数本身却看不出它的物理含义,这显然降低了程序的可读性。因此C语言提供符号常量定义的预处理手段,指定一个有物理含义的名称(标识符)来代表一个具体常量(可以把它看做是一个可替换的正文字串)。不带参数的宏定义的一般形式为:#define标识符具体常量#define宏名替换字串这种方法使得用户能以一个简单易记的常量名称代替一个较长而难记的具体常量,人们把这个标识符(名称)称为“宏名”,预处理时用具体的常量字符串替换宏名,这个过程称为“宏展开”。例:#definePI3.14159它的作用是指定名称PI对应常数“3.14159”,程序中原先需要使用3.14159而其含义又为圆周率的地方都可以改用PI。这样一来,凡是程序中出现PI的地方,经预处理后都会被替换成3.14159。例12.2计算圆周长、圆面积以及同半径的球表面积和球体积。#includestdio.h#definePI3.14159voidmain(){floatr,c,a,s,v;printf(Radius);scanf(f,&r);c2.0*PI*r;aPI*r*r;s4.0*PI*r*r;v4.0/3*PI*r*r*r;•254•printf(Perimeterofcircle.4fAreaofcircle.4f\n,c,a);printf(Surfaceofball.4fVolumeofball.4f\n,s,v);}可以看出,宏定义除了易记之外,还有易改的特点。当程序中多处使用3.14159作为值参与运算时,一旦觉得精度不够,只需要将“#definePI3.14159”改成要求的精度,例如:“#definePI3.1415926”。这时预处理时,程序语句中凡是写PI处全都换成3.1415926去进行编译。请注意,因为宏定义不是C语句,不必在行尾加分号。而为了与变量名区别,人们常常习惯用大写字母表示宏名。又由于宏替换只是用相关的替换字串去代替宏名,预处理时只作简单的置换,不作语法检查(语法错误放在编译过程中检查),所以要注意替换的正确性,特别是在连续逐级层层替换时尤其要小心。例如:#defineONE1#defineTWOONEONE当程序语句中有xTWO时,则替换成xONEONE,即x11。而当语句中有x3*TWO时,则替换成x3*ONEONE,即x3*11。替换结果与语句x3*TWO的本意不符。因此常常将#define中的替换字串表达式用括号括起来,形成一个整体,连括号一块参与替换。如果上例改成:#defineTWO(ONEONE)则x3*TWO替换成x3*(ONEONE)即x3*(11),替换结果与语句本意相符。通常,#define命令写在文件开头,函数之前,作为文件的一部分,此时宏定义的作用域为该文件的整个范围。也可以把宏定义安排在程序中的其他位置上,不过要注意在使用符号常量之前一定要先定义。另外,还可以用#undef命令提前终止宏定义的作用域。例如:#definePI3.141569/*PI开始有效*/voidmain(){…}#undefPI/*PI开始无效*/intfun(){…}12.2.2带参数的宏定义#define还可以定义带参数的宏,其一般形式为:#define宏名(参数表)替换字串在尾部的替换字串中一般都含有宏名括号里所指定的参数,它不仅进行简单的常量字串替换,还要进行参数替换。在预处理时,编译器将程序中的实际参数代替宏中有关的形式参数。例如,定义一个三角形周长的带参宏L为三边a、b、c之和:#defineL(a,b,c)(abc)其定义中的替换字串还不是最终的量,必须根据程序语句中实际使用宏名L时所带的具体参数a、b、c的值来确定替换字串的内容,假如程序中有下面的宏引用:•255•perimeter=L(5,7,9);则宏展开替换时成了:perimeter=(579);这里,我们按习惯将替换正文的表达式abc用括号括起来形成一个整体(abc),那么在替换时就可以避免结果与原意不符的情况。假如宏定义为:#defineL(a,b,c)abc而程序中的宏引用为x6*L(5,7,9),则宏展开替换成了x6*579,与原意不符。显然,从参数替换角度看,宏与函数相似——都要用实际参数代替形式参数,但本质上两者还是不同的。宏展开在预处理时进行,函数则在程序执行调用时才起作用;带参宏只进行简单的字串替换,而没有函数那样的参数运算,既不进行值的传递,也没有“返回值”的概念。12.3条件编译预处理C语言属于计算机高级语言,原理上高级语言的源程序与系统无关,然而,对于不同的系统C语言的源程序还是存在着微小差别的。在用C语言编写程序时,为了提高其应用范围,或者说为了提高它的可移植性,C语言的源程序中的一小部分内容需要针对不同的系统编写不同的代码,使之在确定的系统中选择其中有效的代码进行编译。条件编译预处理指令就是提供这方面功能的预处理指令。12.3.1条件编译预处理命令#ifdef条件编译预处理命令#ifdef是一种特殊形式的条件编译预处理命令,它是通过测试标识符(宏名或常量)是否被定义来决定编译对象。其一般格式如下:格式一:#ifdef标识符语句组1#else语句组2#endif“ifdef标识符”意思为“ifdefined标识符”,其作用是:如果定义了该标识符就将语句组1编译成相应目标代码,否则将语句组2编译成相应目标代码。这种形式还有两种变形。格式二:#ifdef标识符语句组1#endif格式三:#ifndef标识符语句组1#else语句组2#endif•256•#ifndef,意思为“ifnotdefined标识符”,其作用是:如果被测标识符没被定义就将语句组1编译成相应目标代码,否则将语句组2编译成相应目标代码。我们可以看出其测试状态与#ifdef恰恰相反。条件编译有时能够帮助解决程序的可移植问题,提高通用性;同时,也便于程序的调试工作。例如,一个C源程序需要在不同的系统上运行,而不同的计算机系统又有微小的差异(例如,有