再上一篇:5.6 具有缺省参数值和参数个数可变的函数
上一篇:5.7 函数的重载
主页
下一篇:5.9 程序的多文件组织
再下一篇:第六章 数组
文章列表

5.8 编译预处理

《VC++程序设计基础》,讲述C++语言的语法和标准库,以及Visual C++ 函数库和MFC类库的使用,并附相关代码示例。

本节简要地介绍C++提供的编译预处理程序所提供的预处理指令及其用法。编译预处理(Precompile)不是C++编译系统的一个组成部分,而是在编译源程序之前,由单独的编译预处理程序对源程序所做的加工处理工作。由于编译预处理不属于C++的语法范畴,因此,为了与C++的语句区分开来,编译预处理指令一律用符号#开头,并以回车符结束,即每一条预处理指令单独占一行。根据编译预处理的功能不同,将预处理分为三种:宏定义、文件包含(嵌入指令)、条件编译。我们分别介绍之。

5.8.1 "包含文件"的处理

包含文件处理是指在一个源程序文件中可以将另一个源程序文件的全部内容包含进来,即将另外的一个文件包含到当前的文件之中。这是通过include编译预处理指令来实现的,其格式为:

#include "文件名"

#include <文件名>

include 编译预处理指令的处理过程为: 编译预处理程序根据“文件名”,把指定的文件的全部内容读到当前处理的文件中, 作为当前文件的一个组成部分,即用文件的内容替代该#include指令行。例如,设文件FILE1.H的内容为:int x=200,y=100;

float x1=25.6,x2=28.9;

设文件FILE2.CPP的内容为:

#include “FILE1.H”

void main(viod)

{

cout<

cout<

}

用文件FILE1.H的内容替换编译预处理指令行后,产生一个临时文件,其内容为:int x=200,y=100;

float x1=25.6,x2=28.9;

void main(viod)

{

cout<

cout<

}

并把这临时文件交给编译程序进行编译。

有关使用include编译预处理指令,说明以下几点:

1)用双引号括起来的文件名表示要从当前工作目录开始查找, 而用<>括起来的文件名表示从C++编译器约定的目录include开始查找。通常, 用双引号括起来的文件名为用户自定义的包含文件, 而用<>括起来的文件是C++语言预定义的包含文件。这些文件在C++语言的include目录或在其子目录中。

2)包含文件的扩展名通常为“.h”, 当然也可以使用其它的扩展名。

3)一个include指令只能指定一个被包含的文件,若要包含n个文件,则要用n个include指令。

4)在一个包含文件中又可以包含其它的包含文件, 即这种文件的包含可以是嵌套的,处理过程完全类同。注意,用包含文件的内容替换include指令行时,是在一个临时文件中进行的,并不改变原文件的内容。

5)include指令可出现在程序中的任何位置。通常放在程序的开头。

在设计一个大的程序时,包含文件是非常很有用的。通常,将程序公用的数据结构定义为头文件,在相应的处理程序文件中,用include指令包含相应的头文件。我们在后面的程序设计中经常使用这种方法。

5.8.2 宏定义

宏定义均用define编译预处理指令来定义。宏定义可分为不带参数的宏定义和带参数的宏定义,我们分别介绍其定义格式和用法。

5.8.2.1 不带参数的宏定义

不带参数的宏定义的格式为:

#define 标识符 字符或字符串

其标识符称为宏名,字符或字符串可以用引号括起来, 也可以不用引号括起来,但括起来与不括起来的作用是不一样的。如

#define PI 3.1415926

其作用是将宏名定义为实数3.1415926。在编译预处理时,将该define指令后,所有出现PI的地方均用3.1415926来代替。这种替换过程称为“宏扩展”或“宏展开”。又如

#define PROMPT "面积为:"

表示将宏名 PROMPT 定义为字符串"面积为:",在编译预处理时,将出现宏名PROMPT的地方均代换为字符串"面积为:"。

例 6.1 宏定义的使用。

#include

#define PI 3.1415926

#define R 2.8

#define AREA PI*R*R //B

#define PROMPT "面积为:"

#define CHAR '!'

void main(void)

{

cout << PROMPT<

}

执行程序时,其输出为:

面积为:24.6301!

不带参数的宏定义及其使用,说明以下几点:

1、通常宏名用大写字母来表示,以便与程序中的变量相区别。从语法上来讲,任一合法的标识符均可用作宏名,即也可用小写字母来表示。

2、宏定义可出现在程序中的任何位置。通常将宏定义放在源程序文件的开始部分。宏名的作用域为: 从宏定义开始到本源程序文件结束或#undef。

3、在宏定义中可以使用已定义的宏名。如上例中的B行,在定义宏AREA时,用到已定义的宏名PI和R。在编译预处理时,先对该行中的PI和R作替换。经替换后,B行为:

#define AREA 3.1415926*2.8*2.8

上面的程序,经宏扩展后,产生的中间文件为:

#include

void main(void)

{

cout << "面积为:"<< 3.1415926*2.8*2.8<<'!'<<’\n’;

}

4、在宏扩展时,只对宏名作简单的替换,不作任何计算,也不作任何语法检查。若宏定义时书写不正确,会得到不正确的结果或编译时出现语法错误。如程序:

#include

#define A 3+5

#define B A*A

void main(void)

{ cout<

执行程序后的输出为23,而不是64。因C行经宏扩展后为:

{ cout<<3+5*3+5<<’\n’; }

又如程序:

#include

#define PI 3.1415;

void main(void)

{ float r,area;

cout<<"输入半径:";

cin>>r;

area=PI*r*r; //A

cout<<"面积为:"<

}

经替代后,编译时,指出A行语法错,这是由于A行经宏扩展后,该行为:area=3.1415;*r*r;

错误的原因请读者自行分析。

4、若要终止宏名的作用域, 可以使用预处理命令:

#undef 宏名

例如:

#define PI 3.1415926

......

#undef PI //B

......

B行终止PI的作用域,其后,不能再使用宏名PI。

5、当宏名出现在字符串中时,编译预处理不进行宏扩展。如程序:#include

#define A "中国"

#define B "A人民共和国"

void main(void)

{ //A

cout<<"A南京"<<'\t';

cout<

}

执行程序时的输出为:

A南京 A人民共和国

6、在同一个作用域内,不允许同一个宏名定义二次或二次以上。这种规定是合理的,否则编译预处理程序在进行替代时,出现不唯一性。

5.8.2.2 带参数的宏定义

一旦定义了带参数的宏定义,在进行宏扩展时就与不带参数的宏不同,不是仅作简单的宏扩展,而是有点类同于函数,先进行参数替换,然后再进行接着的宏替换。定义带参数宏的一般格式为:

#define 宏名(参数表) 使用参数的字符或字符串

当带有多个参数时,在参数之间用逗号隔开。这里的参数仅用标识符来表示,不能指定参数的类型。例如:

#define VOLUMN(a,b ,c ) a * b * c

......

b = VOLUMN(2.0 ,7.8 ,1.215 ); //A

定义了求长方体体积的宏 VOLUMN,它带有三个参数,分别表示长、宽、高。使用带参的宏称为宏调用,在宏定义中的参数也称为形参,在宏调用中给出的参数也称为实参。在对宏调用进行扩展时,先依次用实参替代宏定义中的形参,并将替代后的字符串替代宏调用。如A行中的宏调用经宏扩展后为:

b= 2.0 * 7.8 * 1.215;

即将实参代替宏定义中的形参, 而其余部分不变。注意,宏扩展仅作简单的替代,而不作任何计算。

对带参数的宏说明以下几点:

1、当宏调用中包含的实参有可能是表达式时, 在宏定义中要用括号把形参括起来,以便减少错误。 如:

#define V(a,b) a*b

......

c= V(e+f, d+c); //B

则经过宏扩展后,表达式的值不正确了,因B行扩展成为:

c= e+f * d+c;

若将宏定义改为:

#define V(a,b) (a)*(b)

则B行经宏扩展后,成为:

c= (e+f) * (d+c);

这才是表达式的正确表示。

2、在宏定义时, 宏名与左括号之间不能有空格, 这与函数的定义是不一样的。若在宏名后有空格时, 则将空格后的全部字符都作为无参宏所定义的字符串,而不作为形参。例如:

#define V1 (a,b ,c ) (a) * ( b) * ( c)

则编译预处理程序认为是将无参宏V1定义为“(a,b ,c ) (a) * ( b) * ( c)”,而不将(a,b ,c )作为参数。

3、一个宏定义通常必须在一行内定义完,并以换行符结束。当一个宏定义多于一行时必须使用转义符"\",即在按换行符(Enter键)之前先输入一个“\”。例如:

#define swap(a,b,c,t) t=a; a=b; b=c\

c=a

即在第一行尾部的“\”,表示要跳过其后的回车符。

由于带参宏存在宏定义与宏调用,存在形参与实参,与函数有些类同。但二者有本质上的不同,宏与函数之间的主要区别是:

1、二者的定义形式不一样。在宏定义中只给出形式参数,而不要指明每一个形式参数的类型;而在函数定义时,必须指定每一个形式参数的类型。

2、宏由编译预处理程序来处理的,而函数是由编译程序来处理的。在宏调用时,仅作简单的替换,而不作任何计算,并且是在编译之前,由预处理程序来完成这种替换的;而函数是在编译后,在目标程序执行期间,要依次先求出各个实参的值,然后才执行函数的调用。

3、在函数调用时,编译器要求实参的类型必须与对应的形参类型相一致,即要作类型语法检查;而在宏调用时,不作任何检查,只是作一种简单的替代。

4、函数可以用return语句返回一个值,而宏没有返回值的概念。

5、多次调用同一个宏时,经宏扩展后,要增加源程序的长度;而对同一个函数的多次调用,不会使源程序变长。

5.8.3 条件编译

通常情况下,源程序中的所有行都要被编译程序编译处理,但是有时希望程序中的某几行或某一部分程序行是在满足某种条件时,才要求编译程序对其进行编译;而当条件不成立时,这部分程序行不要编译,其作用与从源程序中删除这部分程序行一样。这种情况称为条件编译。

条件编译指令有二类,第一类是根据是否已定义的宏名来确定是否要编译某些程序行;第二类条件编译的条件是表达式的值。它们共有六种形式,我们分别介绍如下:

1、 宏名作为编译指令的条件

第一种格式是:

#ifdef 宏名

程序段

#endif

当宏名已经被定义, 则要编译该程序段;否则不编译这个程序段。在编写通用的程序或调试程序时,这种条件编译是很有用的。宏名的定义可以使用无参宏的定义格式,也可以简化为:

#define 宏名

例如,在调试程序时常常要输出一些调试信息,而在程序调试完后不要输出这些信息,则可以把输出调试信息的输出语句用条件编译括起来,形式如下:#ifdef DEBUG

cout<<”x=”<

......

#endif

在调试程序期间,在源程序的开头增加宏定义:

#define DEBUG

由于已经定义了宏名DEBUG,所以用以上形式条件编译预处理指令括起来的程序段都要编译,实现了输出调试信息的目的。一旦程序调试好,只要删除DEBUG的宏定义,重新编译程序,则所有的输出调试信息的程序部分均不编译。采用这种方法,比从源程序中删除所有的输出调试信息的程序部分要简单得多。

第二种格式为:

#ifdef 宏名

程序段1

#else

程序段2

#endif

这种格式告诉编译预处理程序,当宏名已经被定义时, 则要编译程序段 1,而不要编译程序段2;否则,不要编译程序1,而要编译程序段2。

第三种格式为:

#ifndef 宏名

程序段

#endif

这种格式表示,如果宏名没有定义, 则要编译程序段;否则不要编译这程序段。

第四种格式为:

#ifndef 宏名

程序段1

#else

程序段2

#endif

这种格式表示,当宏名没有定义时,则要编译程序段1,不要编译程序段2;当定义了宏名时,不编译程序段1,而要编译程序段2。

2、 表达式值作为条件编译的条件

把表达式的值作为编译条件也有二种格式。第一种格式为:

#if 表达式

程序段

#endif

这种格式告诉编译预处理程序,如果表达式的值不等于0时, 则要编译程序段;否则不要编译程序段。

第二种格式为:

#if 表达式

程序段1

#else

程序段2

#endif

这种格式告诉编译预处理程序,如果表达式的值不等于 0 , 则要编译程序段1,不要编译程序段2;否则不要编译程序段1,而要编译程序段2。

对条件编译需要说明二点:

1、条件编译指令也与宏定义一样,可出现在程序中的任何位置。编译预处理程序在处理条件编译时,实际上是将要编译的程序段依次写到一个临时文件中,并将这临时文件作为编译程序的输入文件,即编译程序对这个临时文件进行编译,产生目标程序文件。

2、当把表达式值作为条件编译的条件时,在编译预处理时,必须能求出表达式的值,换言之,该表达式中只能包含一些常量的运算。

条件编译不仅可以用在调试程序或编写通用的程序,另一非常重要的应用是用在包含文件中。如,设文件a2.h的内容为:

#define AA1 6

float area;

文件a1.h的内容为:

#include "a2.h"

#define A1 AA1*16

源程序文件a.cpp的内容为:

#include

#include "a1.h"

#include "a2.h"

#define PI 3.1415926

#define R 2.8

void main(void)

{

area = PI * R * R;

cout << "圆面积="<

cout<<” 长方形面积=”<

}

对源程序文件a.cpp进行编译时出现错误,同一变量重复定义。解决的办法有二种,一种是在程序中保证做到同一个头文件在源程序文件中只包含一次。对于设计较为简单的程序时,容易做到这一点;但当编写一个大程序时,很难做到这一点。另一种方法是在定义头文件时,使用条件编译,以保证同一个头文件不论被包含多少次, 只有第一次的包含指令起作用, 其余的包含指令都不起作用。上面的头文件a2.h可以改写为:

#ifndef _A2_H //A

#define _A2_H //B

#define AA1 6

float area;

#endif //C

头文件a1.h可以改写为:

#include "a2.h"

#ifndef _A1_H

#define _A1_H

#define A1 AA1*16

#endif

重新编译源程序文件 a.cpp 时就没有编译错误了。用这种方法定义头文件时,条件编译指令将头文件的内容括起来。如在头文件a2.h中,A行是条件编译,当宏名_A2_H没有定义时,表明是第一次包含这个头文件,要编译在A行与C行之间的程序段,即这头文件的内容起作用,并在B行定义了宏名_A2_H。当该头文件被第二次包含时,由于宏名_A2_H已在第一次包含时已经定义,A行的条件编译不成立,即在A行与C行之间的程序段不要编译,即使这头文件的内容不起作用。以后不论包含头文件a2.h多少次,这个头文件的内容均不起作用。