讲程序编译的过程分为预处理和正式编译是C++的一大特点;首先根据预处理命令对源程序进行加工,然后再进行正式的编译。 预处理命令不是C++的语句
分类;文件包含,条件编译,宏定义
我们用C++进行编程的时候,可以在源程序中包括一些编译命令,以告诉编译器对源程序如何进行编译。这些命令包括:宏定义、文件包含和条件编译,由于这些命令是在程序编译的时候被执行的,也就是说,在源程序编译以前,先处理这些编译命令,所以,我们也把它们称之为编译预处理,本章将对这方面的内容加以介绍。
实际上,编译预处理命令不能算是C++语言的一部分,但它扩展了C++程序设计的能力,合理地使用编译预处理功能,可以使得编写的程序便于阅读、修改、 移植和调试。
预处理命令共同的语法规则如下:
◇ 所有的预处理命令在程序中都是以"#"来引导 如"#include "stdio.h""。
◇ 每一条预处理命令必须单独占用一行,如"#include "stdio.h" #include <stdlib.h>" 是不允许的。
◇ 预处理命令后不加分号,如"#include "stdio.h";" 是非法的。
◇ 预处理命令一行写不下,可以续行,但需要加续行符"\"。
下面我们对宏定义、文件包含和条件编译三种预处理命令的用法分别进行介绍。
(一)宏定义命令将一个标识符定义为一个字符串。注意它的右值性
#define CUBE_THREE 3*3*3
#表示这是一条预处理命令, define为关键字, CUBE_THREE(标识符)称为宏名,也简称为宏, 3*3*3是被定义的字符串。这样, CUBE_THREE就代表字符串3*3*3。当源程序被编译的时候,遇到标识符CUBE_THREE,均以字符串3*3*3进行替换。由于宏定义命令是用于字符串的替换,我们也常把宏名用指定的字符串进行替换的过程,称之为宏替换。
下面是宏定义的一个例子:
例9-1 | |
#include <iostream.h> | |
编译并运行该程序,结果如下:
a is 27
因为经过宏替换后,语句"a=CUBE_THREE"等价于"a=3*3*3;"。
宏替换的功能为我们编程时可以带来一些方便。因为,如果在程序中,某一个常量出现较多,就可以为该常量定义一个宏。这样,假定我们要修改该常量时,就不必在程序中对该常量用手工一个个地查找、修改,而只要修改其宏定义即可。例如,如果在一个程序里,3的立方出现的次数比较多,我们就可以把它定义成上面的宏的形式。假定现在需要把3的立方修改成3的4次方,只要修改宏定义即可:
#define CUBE_THREE 3*3*3*3
宏定义命令通常有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。
9.1.1 简单的宏定义
上面的实例就是一个简单的宏定义,简单宏定义的一般形式如下:
#define <宏名> <字符串>
其中, define是宏定义命令的关键字,<宏名>是一个标识符,<字符串>可以是常数、表达式、格式串等。
在程序被编译的时侯,如果遇到宏名,先将宏名用指定的字符串替换,然后再进行编译。
下面是另一个包含简单的宏定义的程序实例:
例9-2 | |
#include<iostream.h> | |
The cube's sidecar is 3
The cube's bulk is 27
The cube's area is 54
我们已经看到:简单宏定义只是定义了一个符号常量。在C++中,const也是用来定义符号常量的。例如:
const double PI=3.14159265;
#define PI 3.14159265
上面两行的效果是一样的,都是将标识符PI定义为3.14159265。但是,这两种定义符号常量的方法还是有区别的:
(1)const产生的符号是具有类型的,在
const double PI=3.14159265;
中,PI是一个double型的常量,而#define命令定义的符号则不具有类型,它仅仅被另一个字符串替换,而不管内容是否正确。例如:
#define PI 3.14159,265
这样的简单宏定义在预编译的时候不会产生错误,因为系统仅仅将后面的"3.14159,265"视为一个字符串而不是double型的常量。但是,如果把它作为一个double型的常量,参与表达式的运算,则会出现编译错误,例如:
double r = 10.0;
double d = PI*r*r;
后一个语句经宏替换后,则变为:
double d = 3.14159,265*r*r;
显然是不对的。
(2)const可以定义一个局部常量,在某一个函数体内用const定义的常量是局部常量,其作用域仅限于该函数体。而用#define定义的常量则不一样,即使在某个函数体内,但它的作用域并不仅限于该函数体,而是从定义点开始,直到整个文件结束为止,除非在此过程中使用#undef取消其定义。
(3)使用const定义常量是一个说明语句,以分号结束;而用#define定义常量是一个预处理命令,不能用分号结束。
在C++中,我们一般用const定义符号常量。很显然,用const定义常量比用define定义常量更好。
在使用宏定义时应注意的是:
(a) 在书写#define 命令时,注意<宏名>和<字符串>之间用空格分开,而不是用等号连接。
(b) 使用#define定义的标识符不是变量,它只用作宏替换,因此不占有内存。
(c) 习惯上用大写字母表示<宏名>,这只是一种习惯的约定,其目的是为了与变量名区分,因为变量名通常用小写字母。
如果某一个标识符被定义为宏名后,在取消该宏定义之前,不允许重新对它进行宏定义。取消宏定义使用如下命令:
#undef<标识符>
其中,undef是关键字。该命令的功能是取消对<标识符>已有的宏定义。被取消了宏定义的标识符,可以对它重新进行定义。
宏定义可以嵌套,已被定义的标识符可以用来定义新的标识符。例如:
#define PI 3.14159265
#define R 10
#define AREA (PI*R*R)
单的宏定义将一个标识符定义为一个字符串,源程序中的该标识符均以指定的字符串来代替。前面已经说过,预处理命令不同于一般C++语句。因此预处理命令后通常不加分号。这并不是说所有的预处理命令后都不能有分号出现。由于宏定义只是用宏名对一个字符串进行简单的替换,因此如果在宏定义命令后加了分号,将会连同分号一起进行置换。如:
#define PI 3.14159265;
则此时的PI所代替的字符串是"3.14159265;"而不是我们所期望的"3.14159265"。
例9-3 | |
#include<iostream.h> | |
Input radius:5↙
输出结果为:
1=31.4159
s=78.5398
v=523.599
简单的宏定义和const语句都可以用来定义符号常量,但是两者又有许多不同之处:
(1)#define命令定义的符号不具有类型,它仅仅是被另一个字符串替换罢了,而const产生的符号常量是具有类型的。例如:
const int SIZE=80;
说明SIZE是一个int型的常量,而#define命令仅产生文本替换。
(2)两者定义的符号常量的作用域是不同的。在函数体内用const定义的常量是局部常量,其作用域仅限于该函数体。而用#define定义的常量的作用域是从定义命令之后到本源文件结束,可以用"#undef"命令终止宏定义的作用域。下图可形象的说明用#define定义的常量的作用域:
#define A 10 function() { …… A的有效范围 …… …… } #undef A function2() …… …… | A的有效范围 |
事实上,用const定义常量比用define定义常量要好。C语言中没有用const定义常量的功能,常量要用define定义。C++为了与C语言兼容,也允许用define定义常量。
下面是一些比较常见的宏定义错误:
★#define A=10
不应该用等号连接,而应该用空格分开,正确写法为:
#define A 10
思考:#define A=10的效果是什么?
★#define A 10;
预处理不用分号结束,这样定义的结果是A所定义的符号常量不是整数10,而是内容为"10;"的一个字符串。正确的写法为:
#define A 10
★#define A10 #define B20
每条预处理命令必须单独占用一行,正确的写法应该是:
#define A 10
#define B 20
宏名一旦定义,就可以嵌套使用,成为其它宏定义的一部分。例如,下面代码定义了ONE,TWO以及THREE的值:
#define ONE 1
#define TWO ONE+ONE
#define THREE ONE+TWO
9.1.2 带参数的宏定义
带参数的宏定义的一般形式如下:
#define <宏名>(<参数表>) <宏体>
其中, <宏名>是一个标识符,<参数表>中的参数可以是一个,也可以是多个,视具体情况而定,当有多个参数的时候,每个参数之间用逗号分隔。<宏体>是被替换用的字符串,宏体中的字符串是由参数表中的各个参数组成的表达式。例如:
#define SUB(a,b) a-b
如果在程序中出现如下语句:
result=SUB(2, 3)
则被替换为:
result=2-3;
如果程序中出现如下语句:
result= SUB(x+1, y+2);
则被替换为:
result=x+1-y+2;
在这样的宏替换过程中,其实只是将参数表中的参数代入到宏体的表达式中去,上述例子中,即是将表达式中的a和b分别用2和3代入。
我们可以发现:带参的宏定义与函数类似。如果我们把宏定义时出现的参数视为形参,而在程序中引用宏定义时出现的参数视为实参。那么上例中的a和b就是形参,而2和3以及x+1和y+2都为实参。在宏替换时,就是用实参来替换<宏体>中的形参。
在使用带参数的宏定义时需要注意的是:
(1)带参数的宏定义的<宏体>应写在一行上,如果需要写在多行上时,在每行结束时,使用续行符 "\"结束,并在该符号后按下回车键,最后一行除外。
(2)在书写带参数的宏定义时,<宏名>与左括号之间不能出现空格,否则空格右边的部分都作为宏体。例如:
#define ADD (x,y) x+y
将会把"(x,y)x+y"的一个整体作为被定义的字符串。
(3)定义带参数的宏时,宏体中与参数名相同的字符串适当地加上圆括号是十分重要的,这样能够避免可能产生的错误。例如,对于宏定义:
#define SQ(x) x*x
当程序中出现下列语句:
m=SQ(a+b);
替换结果为:
m=a+b*a+b;
这可能不是我们期望的结果,如果需要下面的替换结果:
m=(a+b)*(a+b);
应将宏定义修改为:
#define SQ(x) (x)*(x)
对于带参的宏定义展开置换的方法是:在程序中如果有带实参的宏(如"SUB(2,3)"),则按"#define"命令行中指定的字符串从左到右进行置换。如果串中包含宏中的形参(如a、b),则将程序语句中相应的实参(可以是常量、变量或者表达式)代替形参,如果宏定义中的字符串中的字符不是参数字符(如a-b中的-号),则保留。这样就形成了置换的字符串。如下图所示:
图9-1 | |
例9-2 | |
#include <iostream.h> //将系统输入输出流头文件包含进来 #define SUB(a,b) a-b //用带参数的宏定义定义了一个宏SUB(a,b) void main () { int x,y,result; x=2; y=3; result=SUB(x+2,y-1); //调用该宏 cout<< "result="<<result<<end1; } //输出result的值,观察宏替换产生的结果 | |
(1)在带参的宏定义中,宏"调用"只是将实参替换形参。而在函数中,形参和实参是完全独立的变量,当调用发生时,实参的值传递给形参。
从发生的时间来说,宏"调用"是在编译时发生的,函数调用是在程序运行时发生的;宏"调用"不存在内存单元分配的问题,而函数调用时,会给形参变量分配内存单元,然后,复制实参的值,函数调用结束后,形参变量占用的内存单元被释放。
(2)在宏定义中的形参是标识符,而宏调用中的实参可以是表达式
条件编译
一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件下才进行编译,即对一部分内容指定编译条件,这就是“条件编译”(conditional compile)。条件编译语句排版时,需考虑以下三种位置:1)条件编译语句块与函数定义体之间不存在相互嵌套(主要在(.h)文件中);2)条件编译语句块嵌套在函数体之外(主要在(.c)文件中);3)条件编译语句嵌套在函数体内 (主要在(.c)文件中)。条件编译指令将决定那些代码被编译,而哪些是不被编译的。可根据表达式的值或某个特定宏是否被定义来确定编译条件。
不相互嵌套
条件编译关键字语句顶格左对齐; 所含的#include语句(块) #define语句(块)甚至是被嵌套下级条件编译语句块,按照语句块嵌套的排版方式进行缩进排版 。函数体外
这种情况下,条件编译语句块不影响函数体 条件编译关键字语句顶格左对齐; 所含的函数体定义无需缩进,依旧按照单个函数体定义的排版方式进行。函数体内
a)当条件编译语句块与被包语句所属的语句块之间没有逻辑路径交叉时,以下两种方式均可 按照语句块嵌套方式进行缩进排版 (推荐); 条件编译语句不影响原先语句块排版,条件编译语句与所包含的关键字语句块左对齐 。 b)当条件编译语句块与被包语句所属的语句块之间存在逻辑路径交叉时 条件编译语句顶格左对齐,其它语句按照正常顺序排版。条件编译的形式如下所示(NNN、MMM等都是在某处已经定义为 1 或者 0 的): #if NNN statement1; #elif MMM statement2; #else statement3; #endif
指令
条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。 1.#if、#else、#elif和#endif指令 一般形式有如下几种 (1)#if 表达式 语句段1 [#else 语句段2] #endif 如果表达式为真,就编译语句段1,否则编译语句段2 (2)#if 表达式1 语句段1 #elif 表达式2 语句段2 #else 语句段3 #endif 如果表达式1真,则编译语句段1,否则判断表达式2;如果表达式2为真,则编译语句段2,否则编译语句段3 2.#ifdef和#ifndef (1)#ifdef的一般形式: #ifdef 宏名 语句段 #endif 作用:如果在此之前已定义了这样的宏名,则编译语句段。 (2)#ifndef的一般形式: #ifndef 宏名 语句段 #endif 作用:如果在此之前没有定义这样的宏名,则编译语句段。 #else可以用于#ifdef和#ifndef中,但#elif不可以。 3.#error 指令将使编译器显示一条错误信息,然后停止编译。 4.#line 指令可以改变编译器用来指出警告和错误信息的文件号和行号。 5.#pragma 指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。