9.1 宏定义
宏定义有无参数宏定义和带参数宏定义两种。
无参数的宏定义的一般形式为
# define 标识符 字符序列
其中# define之后的标识符称为宏定义名(简称宏名),要求宏名与字符序列之间用空格符分隔。这种宏定义要求编译预处理程序将源程序中随后所有的定名的出现(注释与字符串常量中的除外)均用字符序列替换之。前面经常使用的定义符号常量是宏定义的最简单应用。如有:
# define TRUE 1
# define FALSE 0
则在定义它们的源程序文件中,凡定义之后出现的单词TRUE将用1替代之;出现单词FALSE将用0替代之。
在宏定义的#之前可以有若干个空格、制表符,但不允许有其它字符。宏定义在源程序中单独另起一行,换行符是宏定义的结束标志。如果一个宏定义太长,一行不够时,可采用续行的方法。续行是在键人回车符之前先键入符号“\”。注意回车要紧接在符号“\”之后,中间不能插入其它符号。
宏定义的有效范围称为宏定义名的辖域,辖域从宏定义的定义结束处开始到其所在的源程序文件末尾。宏定义名的辖域不受分程序结构的影响。可以用预处理命令#undef终止宏定义名的辖域。
在新的宏定义中,可以使用前面已定义的宏名。例如,
# define R 2.5
# define PI 3.1415926
# define Circle 2*PI*R
# define Area PI* R * R
程序中的Circle被展开为2*3.1415926* 2.***rea被展开为3.1415926*2.5*2.5。
如有必要,宏名可被重复定义。被重复定义后,宏名原先的意义被新意义所代替。
通常,无参数的宏定义多用于定义常量。程序中统一用宏名表示常量值,便于程序前后统一,不易出错,也便于修改,能提高程序的可读性和可移植性。特别是给数组元素个数一个宏定义,并用宏名定义数组元素个数能部分弥补数组元素个数固定的不足。
注意:预处理程序在处理宏定义时,只作字符序列的替换工作,不作任何语法的检查。如果宏定义不当,错误要到预处理之后的编译阶段才能发现。宏定义以换行结束,不需要分号等符号作分隔符。如有以下定定义:
# define PI 3.1415926;
原希望用PI求圆的周长的语句
c=2*PI*r;
经宏展开后,变成
c=2*3.1415926*r;
这就不能达到希望的要求。
带参数宏定义进一步扩充了无参数宏定义的能力,在字符序列替换同时还能进行参数替换。带参数定定义的一般形式为
# define 标识符(参数表)字符序列
其中参数表中的参数之间用逗号分隔,字符序列中应包含参数表中的参数。在定义带参数的宏时,宏名标识符与左圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。如有宏定义:
# define MAX(A,B) ((A) > (B)?(A):(B))
则代码 y= MAX( p+q, u+v)将被替换成 y=((p+q) >(u+v)?(p+q):(u+v)。
程序中的宏调用是这样被替换展开的,分别用宏调用中的实在参数字符序列(如p+q和u+V) 替换宏定义字符序列中对应所有出现的形式参数(如用p+q替代所有形式参数A,用u+V替代所有形式参数B),而宏定义字符序列中的不是形式参数的其它字符则保留。这样形成的字符序列,即为宏调用的展开替换结果。宏调用提供的实在参数个数必须与宏定义中的形式参数个数相同。
注意:宏调用与函数调用的区别。函数调用在程序运行时实行,而宏展开是在编译的预处理阶段进行;函数调用占用程序运行时间,宏调用只占编译时间;函数调用对实参有类型要求,而宏调用实在参数与宏定义形式参数之间没有类型的概念,只有字符序列的对应关系。函数调用可返回一个值,宏调用获得希望的C代码。另外,函数调用时,实参表达式分别独立求值在前,执行函数体在后。宏调用是实在参数字符序列替换形式参数。替换后,实在参数字符序列就与相邻的字符自然连接,实在参数的独立性就不一定依旧存在。如下面的宏定义:
# define SQR(x) x*x
希望实现表达式的平方计算。对于宏调用
P=SQR(y)
能得到希望的宏展开p= y*y。但对于宏调用q=SQR(u+v)得到的宏展开是q=u+V*u+V。显然,后者的展开结果不是程序设计者所希望的。为能保持实在参数替换后的独立性,应在宏定义中给形式参数加上括号。进一步,为了保证宏调用的独立性,作为算式的宏定义也应加括
号。如 SQR宏定义改写成:
# define SQR((x)*(x))
才是正确的宏定义。
对于简短的表达式计算函数,或为了提高程序的执行效率、避免函数调用时的分配存储单元、保留现场、参数值传递、释放存储单元等工作。可将函数定义改写成宏定义。所以合理使用宏定义,可以使程序更简洁。
9.2 文件包含
文件包含预处理命令(简称文件包含命令)实现将指定文件的内容作为当前源程序的一部分。文件包含预处理命令的一般形式为
# include “文件名”。
或
# include<文件名>
文件包含命令为组装大程序和程序文件复用提供了一种手段。在编写程序时,习惯将公共的常量定义、数据类型定义和全局变量的外部说明构成一个源文件。称这类没有执行代码的文件为头文件,并以“.h”为文件名的后缀。其它程序文件凡要用到头文件中定义或说明的程序对象时,就用文件包含命令使它成为自己的一部分。这样编程的好处是各程序文件使用统一的数据结构和常量,能保证程序的一致性,也便于修改程序。头文件如同标准零件一样被其它程序文件使用,减少了重复定义的工作量。
9.3 条件编译
条件编译是指在编译一个源程序文件时,其中部分代码段能根据条件成立与否有选择地被编译。即编译程序只编译没有条件或条件成立的代码段,而不编译不满足条件的代码段。
条件编译为组装与环境有关的大程序提供有力的支持,能提高程序的可移植性和可维护性。通常在研制程序系统时,设计者将所有与环境有关的内容编写成独立的程序段,并将它们配上相应的条件编译命令。因环境不同等原因,只要设定相应条件,就能组装出适应环境要求的新程序。
条件编译命令主要有三种相似的形式。
(1) #if 表达式
程序段1
# else
程序段2
# endif
其中表达式为常量表达式,其意义是当指定的表达式值为非零时,程序段1参与编译,程序段2不参与编译;否则,反之。
这种形式的条件编译命令使预处理程序能根据给定的条件确定哪个程序段参与编译,哪个程序段不参与编译。
在上述一般形式中,当程序段2不出现时,也可简写为
#if 表达式
程序段
# endif
条件编译与if语句有重要区别:条件编译是在预处理时判定的,不产生判定代码,其中一个不满足条件的程序段不参与编译,不会产生代码;且语句是在运行时判定的,且编译产生判
定代码和两个分支程序段的代码。因此,条件编译可减少目标程序长度,能提高程序执行速度。但条件编译只能测试常值或常量表达式,而且语句能对表达式作动态测试。
条件编译预处理命令也可呈嵌套结构。特别是为了便于描述# else后的程序段又是条件编译情况,引人须处理命令符# elif。它的意思就是# else #if。所以条件编译须处理命令更一般的形式为
# if 表达式 1
程序段1
# elif 表达式 2
程序段2
# elif 表达式 n
程序段n
# else
程序段 n+l
# endif
(2) # ifdef 标识符
程序段1
# else
程序段2
# endif
其中标识符是一个宏名,上述意义是当宏名已被定义,则程序段里参与编译,程序段2不参与编译;否则,反之。这里的程序段可以是任何C代码行,也可以包含预处理命令行。其中的标识符只要求已定义与否,与标识符被定义成什么是无关的。如标识符只用于这个目的,常用以下形式的宏定义来定义这类标识符:
# define 标识符
即标识符之后为空,直接以换行结束该宏定义命令。
在上述一般形式中,如果程序段2为空,则可简写成如下一般形式:
# ifdef 标识符
程序段
# endif
条件编译主要作用是能提高程序的通用性,应用于不同的计算机系统编译不同的源程序段。条件编译另一种应用是在程序中插入调试状态下输出中间结果的代码。如:
# ifdef DEBUG
printf(“a=%d, b=%d \n”, a, b);
# endif
程序在调试状态下与包含宏定义命令
# define BEBUG
的头文件一起编译;在要获得最终目标程序时,不与包含该宏定义命令的头文件一起编译。这样,在调试通过后,不必再修改程序,已获得了正确的最终程序。为了日后程序维护,将调试时使用的程序代码留在源程序中是专业程序员习惯采用的方法。因为这些代码的存在不影响最终目标码,但有助于日后修改程序时的调试需要。
(3) # ifndef 标识符
程序段1
# else
程序段2
# endif
这种条件编译形式与前面介绍的形式的唯一差异是***行的ifdef改为ifndef。其意义是当标识符末被定义时,程序段1参与编译,程序段2不参与编译;否则,反之。在上述形式中,当程序段2不出现时,可简写成:
# infdef 标识符
程序段
# endif
9.4 带参数的主函数
在操作系统下执行某个C程序,是环境对C程序的启动,可以看作是对该程序的main()函数的调用。main()函数执行结束后,控制返回环境。为能从环境向C程序传递信息,启动C程序的命令行可带有任选的参数。命令行的一般形式为
程序名 参数1 参数2……参数n
其中程序名和各参数之间用空白符分隔。
为能让main()函数读取命令行中的参数,环境将多个参数以两个参数形式传递给main()函数、其中***个参数(习惯记作argc) 表示命令行中参数的个数(包括程序名);第二个参数(习惯记作argv)是一个字符指针数组。其中argv[0] 指向程序名字符串的***个字符,argv[1]指向参数1字符串的***个字符,…,argv[argc-1] 指向***一个参数字符串的***个字符。如果利因山等于1,则表示程序名后面没有参数。下面的例子用于说明main()函数对参数argc
与argv的引用方法。
【例9.l】 回打启动程序时的命令行各参数。
# include
void main(int argc, char *argv[] /* 或char **argv; */)
{ int k;
for( k=l; k
printf(“%c”,argv[k],k printf(“\n\n”);
}
如上述程序的执行程序名为echopro.exe,执行该程序的命令行为:
echopro Hello world!
则程序将输出
Hello world!
在以上命令行中,根据约定,main()函数的参数argc的值为3;argv[0],argv[1],argv[2]分别指向字符串“echopro”、“Hello”、“world!”的***个字符。在程序的printf()函数调用中,字符输出格式%c输出一个字符,若是已输出了命令行***一个参数,该格式将输出一个换行符,若是输出其它参数,则输出一个空白符。
因函数的数组参数是指向数组首元素的指针变量,所以在主函数main()中可对argv施行增量运算。例如,在argv[0]指针指向程序名字符串的***个字符情况下,对argv施增量运算++argv后,argv[0](或*argv)就指向参数1的***个字符c利用argv的这一性质,可改写上述程序为以下形式:
# include
void main(int argc,char **argv)
{ while(--argc>0)
printf(“%s%c”,*++argv,argc>1? '':'\n');
}
这里,++argv使指针argv先加1,让它一开始就指向参数1;逐次增回,使它遍历指向各参数。又利用函数printf()的***个格式参数是字符串表达式,上述程序对printf()的调用可改写成:
printf((argc> l) ? “%s”:“%s\n”,* ++argv);
【例9.2】 假定启动程序时给出的命令行参数是一串整数,程序将全部整数求和后输出。
# include
# include
void main( int argc,char **argv)
{ int k,s;
for(s=0, k=l;)
s+=atoi(*++agv); /* 从数字字符串译出整数 */
printf( “\t%d\n”, s);
}
【编辑推荐】