第六章预处理功能和类型定义6.1预处理功能概述本节主要讲述预处理功能的特点。预处理功能是由很多预处理命令组成,这些命令将在编译时进行通常的编译功能〔包含词法和语法分析、代码生成、优化等〕之前进行处理,故称为预处理.预处理后的结果和源程序一起再进行通常的编译操作,进而得到目标代码。预处理功能主要包括如下三种;宏定义、文件包含和条件编译。这些功能是通过相应的宏定义命令、文件包含命令和条件编译命令来实现。这些命令不同于C语言的语句,因为它们具有如下的特点:(1)多数预处理命令只是一种替代的功能,这种替代是简单的替换,而不进行语法检查。(2)预处理命令都是在通常的编译之前进行的,编译时已经执行完了预处理命令,即对预处理后的结果进行编译,这时进行词法和语法分析等通常的程序编译。(3)预处理命令后面不加分号,这也是在形式_七与语句的区别。(4)为了使预处理命令与一般C语言语句相区别,凡是预处理命令都以井号(#)开头。(5)多数预处理命令根据它的功能而被放在文件开头为宜,但是根据需要,也可以放到文件的其他位置。不要产生错觉,好像所有的预处理命令都必须放在文件开头。学习和掌握预处理功能时,应该了解它的上述规定,以便正确地使用和理解这些预处理命令。6.2.1简单宏定义1.简单宏定义的格式和功能简单宏定义的格式如下:#define标识符)(字符串}其中,define是关键字,它表示该命令为宏定义,悦标识符)是宏名,它的写法同标识符。字符串用来表示标识符所代表的字符串。简单宏定义是定义一个标识符(宏名)来代表一个字符串。前面讲过的符号常量就是用这种简单宏定义来实现的。例如:#definePI3.14159265这是一条宏定义的命令,它的作用是用指定的标识符PI来代替字符串3.14159265.在程序中出现的是PI,在编译前预处理时,将所有的PI都用3.14159265来代替,即使用宏名来代替指定的字符串。这一过程又称为宏替换或称为宏展开.[例6.1]给出半径求圆的面积。执行该程序,出现如下信息:#dfinePI3.14159265main(){fioatr,s,Printf(Inputradius;)scanf(%f',&r);A=PI*r*rPrintf(:a=%.4f\n:,a);}执行该程序,出现如下信息:Inputradius;5输出结果如下s=78.5398说明:该例中,开始率义了符号常量PI,它是用宏定义来实现的。程序中出现的PI,在编译前预处理时将用3.14159265来替换。2.使用简单宏定义时的注意事项(1)宏定义中的标识符)(即宏名)一般习惯用大写字母,以便与变量名区别。这样。在C语言程序的各表达式语句中,凡是大写字母的标识符(指全部大写字母)一般是符号常量。但是,宏定义中的宏名也可以用小写字母。(2)宏定义是预处理功能中的一种命令,它不是语句。因此。行末不需加分号。如果加了分号,则该分号将作为所定义的字符串的一部分,即按字符串的一部分来处理。(3)宏替换是一种简单的代替,替换时不作语法检查。如果所定义的字符申中有错,例如,将数字。,误写为字母。,预处理照样代换,并不报错,而在编译中进行语法检查时才报错。因此,要记住宏替换操作只是简单的代换,用宏定义时的字符串来替换其宏名。(4)宏定义中宏名的作用域为定义该命令的文件中,并从定义时起,到终止宏定义命令(#undef标识符))为止,如果没有终止宏定义命令,则到该文件结束为止。通常放在文件开头,表示在此文件内有效。终止宏定义命令的格式如下:#undef(标识符)其中,undef是关键字,(标识符)表示要终止的宏名,该宏名是在该文件中已定义的标识符。例如:#undefPI表示宏定义的PI到此终止,即终止后的PI不再代表所定义的字符串了。(5)宏定义可以嵌套。所谓嵌套是指在进行宏定义时,可以引用已定义的宏名。例如:#defineWIDTH10#defineLENGTH(WIDTH十10)#defineAREA(LENGTH,WIDTH)在第二个宏定义中引用了第一个宏定义的宏名WIDTH,而在第三个宏定义中引用了第一个宏定义的宏名WIDTH和第二个宏定义的宏名LENGTH,第二个和第三个宏定义便是宏定义的嵌套使用。嵌套的宏定义在替换时,要进行层层替换。例如,在上述宏定义的文件中,出现如下语句:s=AREA+50;则替换步骤如下:先替换AREA,结果如下:s=(LENGTH,WIDTH)十50,再替换LENGTH,结果如下:s=((WIDTH+10)*WIDTH)+50;最后替换WIDTH,结果如下:s=((10+IO),10)十50;(6)一般编译系统对于加有双引号的字符串的宏名不予替换。但是,有的编译系统对字符串的宏名也予替换。使用时应该注意该编译系统对字符串内宏名的处理规则。(7)一般编译系统在宏替换时,隐含一空格符,即用空格符将前后两部分分开。有的编译系将不隐含空格符。下面举几个例子,对宏定义的使用作进一步说明。[例6.2]分析下列程序输出结果,并说明简单宏定义在本程序中的应用。#defineA一a#defineTWOA2*Amain(){inta=1;printf(TWOA=%d\nTWOA)。printf(%d\n,-A);}执行该程序输出结果如下:TWOA=-21说明:(1)该程序开头有两个宏定义命令,并且使用宏定义嵌套的方法。(2)在TurboC编译系统中,对加双引号的字符串内的宏名不予替换'即printf()函数中。控制串TWOA=%d\n中的TWOA不被替换,而参数TWOA被替换为2*A进一步替换为2*一a这里,a为1,上述结果为一2又在第二个printf()函数中,其参数-A被替换为一[」一a其值为1.这里可以看出TurboC编译系统在宏替换时,加有隐含空格,其值才为to否则,替换后为--a其值为0.有的编译系统确有此种结果。[例6.3]分析下列程序的输出结果,并说明宏名的作用域。main(){#defineN5printf(N=%d\n,N);#defineMN+3printf(M=%d\n,M);#undefN#defineN1Oprintf(newM=%d\n,M)}}执行该程序输出结果如下:N=5M=8newM=13说明:(1)宏定义命令不一定必须写在文件开头,可以根据需要写在文件的任何位置。该程序中在不同位置出现了3条宏定义命令和1条终止宏定义命令。(2)宏名的作用域是在定义它的文件内,并从定义时开始,到终止定义时为止。本例中,宏名N从定义时开始起作用,到#undefN命令为止。本例中。又再定义N到文件结束。可见,对一个宏名进行重新定义之前必须先将原定义取消。而本例中宏名M从定义时起作用,直到文件结束。6.2.2带奋数的史定义1.带参数宏定义的格式和功能带参数宏定义的一般格式如下;#define宏名(参数表)宏体其甲,宏名是标识符,一般习惯上用大写字母,参数表是由一个或多个参数组成的,多个参数之间用逗号分隔;(宏体)是一个字符串,其中包含参数表中所指定的参数,它可以是由若干个语句组成的。该命令末尾一般不加分号。带参数的宏定义在宏替换时。不是简单的用宏体替换宏名,而是用实参替换形参.这里所说的实参是指程序中引用宏名的参数,而形参是指宏定义时,宏名后边参数表中的参数。例如,#definesQ(x)x*x在程序中出现下述语句:A=SQ(5);这里,在宏定义中参数x是形参,而程序中SQ(5)的S是实参,宏替换时,将用5来替换x,其结果如下:a=5*5;又例如,#defineADD(x,y)x十Y在程序中出现下述语句;A=ADD(5,3);宏替换后结果如下:a=5十3;如果在上述宏定义下,程序中出现如下语句;b=ADD(a+1,b一1);宏替换后结果如下:B=a+1+b-1由此可见,带参数的宏定义是这样替换的:按照宏定义中所指定的宏体从左至右用程序中出现的宏的实参来替换宏体中的形参,对非参数字符,则保留。宏中的实参可以是常量、变量或表达式。2.使用带参数的宏定义应该注意的事项(1)在宏定义时,宏名与左圆括号之间不能出现空格符,否则空格符后将作为宏体的一部分。例如:#defineADD(x,y)x十Y将认为ADD是不带参数的宏名,而字符串(x,y)x+y作为宏体。显然,这不是原来的含意。因此,宏名后与左圆括号间一定不能加空格符。(2)宏体中,各参数上加括号是十分重要的。例如:#defineSQ(x)x*x当程序中出现下列语句,A=SQ(a十1);替换后,则为;a=a+1*a+1而不能将替换的结果写成a=(a十1)*(a+1);如果要将替换后结果写成上述形式,则需要将宏定义改写为:#defineSQ(x)(x),(x)由此可见,在宏定义中,对宏体内的形参外向加上括号是很重要的,它可以避免在优先级上可能出现的问题。对上述宏定义最好写成下述形式:#defineSQ(x)((.x)*(x))这里的圆括号是很有用的。例如,在有如下语句时,m=50/SQ(b十1);替换后结果如下:m=50/(b十1)*(b+1));(3)带参数的宏定义与函数的区别带参数的宏定义和函数尽管在形式上非常相似,特别是当宏名使用小写字母时,出现在程序中很难区别出来是带参数的宏定义还是函数。但是,这二者是根本不同,它们之间的区别概述如下:①定义形式上不同。带参数的宏定义的定义格式前面讲过了,它是通过预处理命令c}e-fine来定义的。它的作用域是在定义它的文件内,并从定义处开始。而函数的定义格式在本书函数和存储类1'章中描述过了。它的作用域分为程序级的和文件级的两种。②处理时间上不同。宏定义是在编译预处理时处理的,处理后再进行编译。而函数是在执行时处理的。它们二者占用的是不同阶段的时间。③处理方式_卜。不同。带参数的宏定义在进行宏替换时,用实参来代替形参,这里只是简单替换,并不做语法上的检查。而函数调用时,是将实参的值赋给形参,要求对应类型一致。宏定义中在参数替换时,不要求类型一致仁④时间和空间的开销上不同。带参数的宏定义,是在通常的编译之前完成替换的,因此它在该程序的目标代码的形成上并没有影响。函数调用是在执行时进行的,因此,采用函数调用的方式可以减少该程序的目标代码,所以,在空间的开销上可以减少。但是,函数调用要有额外的时间上的开销。因为调用前要保留现场,调用后又要恢复现场,因此函数调用时间开销要比带参数的宏定义大。带参数宏定义在使用中比函数时间开销小是一个重要特征。⑤类型的要求上不同。带参数的宏定义对形参的类型不必说明,它没有类型的限制。而函数的形参在定义时必须进行类型说明。例如,两个数相减的操作用带参数的宏定义和函数分别定义如下:用带参数的宏定义格式如下:抹defineMINU(x,y)(x)一(Y)用函数的定义格式如下:minu(x,y)Intx,y(return(x一y);}该函数只能进行两个lnl型数值相减的运算。而。上述的宏定义可以进行两个。har型量的相减,也可以进行两个int型数的相减。还可以进行两'float型数的相减,因为它没有类型的限制通过上述对于带参数的宏定义和函数之间区别的分析,不难看出两者各有特点。对于同一个间题一可以采用两种不同的表示形式,那么到底选择哪一种更好些呢?一般说来,在功能比较简单的情况一下,选用带参数的玄定义能更好些,特别是在需要反复引用的情况下,用宏定义的时间开销较少。例如,比较两个整数的大小,并输出最大的,可用如下的宏定义来实现:#definefl(a,b)printf(%d\n,ab?;b);又例如,计算一个自然数的立方值,可用如下的宏定义实现:#definef2(a)printi%d\n,a*a*a);功能比较复杂的还是选用函数来定义。6.2.3宏定义的应用在C语言程序中,宏定义主要用于下述几个方面。(1)符号常耸的定义C语言中的常觉一般都用符号常量表示,这样不仅书写简便,而且易于修改、易于移植,还可以使标识符有更明显的含意。例如,#definePI3.14159265#defineE2.71828