第12章 预处理程序

预处理程序提供了一些工具。使用这些工具能够开发那些更易于开发、阅读、修改以及移植到不同系统的程序。还能使用预处理程序从文字上定制Objective-C语言,以适合特定的编程应用或适应你自己的编程风格。

预处理程序是Objective-C编译过程的一部分,它可以识别散布在程序中的特定语句。顾名思义,预处理程序实际上是在分析Objective-C程序之前处理这些语句。预处理程序语句是以井号(#)标记的,这个符号必须是该行的第一个非空格字符。你将看到,预处理程序语句的语法稍微不同于Objective-C语句。我们将从研究#define语句开始。

12.1 #define语句

define语句的基本用途之一就是给符号名称指派程序常量。预处理程序语句


define TRUE 1


定义了名称TRUE,并使它等于值1。随后,名称TRUE能够用于程序中任何需要常量1的地方。只要出现这个名称,预处理程序自动在该程序中将这个名称替换为预定义的值1。例如,可能遇到下面这条Objective-C语句,该语句使用了预定义的名称TRUE:


gameOver=TRUE;


这条语句向gameOver指派了值TRUE。你自己无需关注为TRUE定义的确切值。但是因为已经知道它定义为1,所以前面语句的作用就是将1赋给gameOver。预处理程序语句


define FALSE 0


定义了名称FALSE,随后在程序中它就等价于0。因此,语句


gameOver=FALSE;


将FALSE的值赋给gameOver,并且语句


if(gameOver==FALSE)

……


比较gameOver的值和FALSE的预定义值。

预定义名称不是变量。因此,不能为它赋值,除非替换指定值的结果实际上是一个变量。只要在程序中使用预定义名称,在#define语句中预定义名称右边的所有字符都会被预处理程序自动替换到程序中。这类似于在文本编辑器中进行搜索和替换,在这种情况下,预处理程序将出现的所有预定义名称替换为相应的文本。

你将发现#define语句的语法很特别:将TRUE赋值为1没有用到等号。而且,在语句末尾也没有出现分号。不过你马上就会明白这种特别语法存在的原因。

define语句经常放在程序的开始,#import或#include语句之后。这并不是必需的,它们可以出现在程序的任何地方。但是,在程序引用这个名称之前,必须先定义它们。预定义的名称和变量的行为方式不同:没有局部定义之类的说法。在定义一个名称之后,随后就可以在程序的任何地方使用它。大多数程序员把定义放在头文件中,以便在多个源文件中使用它们。

举另一个使用预定义名称的例子。假设想要编写两个方法来计算Circle对象的面积和周长。这两个方法都需要使用常量π,但是它不容易记住,因此,合理的情况是,在程序的开始部分定义该常量的值,然后根据需要在每个方法中使用该值。

所以,可以在程序中包含以下代码:


define PI 3.141592654


然后就可以如下所示在两个Circle方法中使用它了(下面假设Circle类中有一个名为radius的实例变量):


-(double)area

{

return PIradiusradius;

}

-(double)circumference

{

return 2.0PIradius;

}


给符号名称指派一个常量,每次想在程序中使用它们时,就不必记住这个特定常量的值。此外,如果需要更改常量的值(例如,你可能发现使用了错误的值),则只需要在程序的一个地方更改这个值:那就是在#define语句中。如果没有这种方式,将不得不从头到尾搜索程序,并在使用该值的地方显式地修改这个常量的值。

你可能已经注意到了,目前为止显示的所有定义(TRUE、FALSE和PI)都是大写字母组合。这是为了从视觉上区分预定义的值和变量。一些程序员有这样的习惯:所有预定义名称都用大写,这样就容易区分一个名称是变量名、对象名、类名,还是预定义名称。另一种常见的惯例是在定义之前加字母k。这种情况下,之后的字符并不用全部大写。kMaximumValues和kSignificantDigits是符合这种惯例的两个预定义名称例子。

对常量值使用预定义名称有助于加强程序的可扩展性。例如。学习如何使用数组时,可以不通过硬编码分配数组大小,而是如下定义:


define MAXIMUM_DATA_VALUES 1000


这样所有引用都可以以这个数组的大小为基础(如在内存中分配该数组的内存),并且根据这个预定义的值确定数组的有效下标。

并且,假设程序在任何用到数组大小的地方都使用MAXIMUM_DATA_VALUES,如果后来需要改变数组的大小,程序中唯一必须改动的语句就是前面的定义。

12.1.1 更高级的定义类型

名称的定义不仅能够包括简单的常量值。你很快就会看到,它可以包括表达式和其他任何东西。

下面的语句将名称TWO_PI定义为2.0与3.141592654的积:


define TWO_PI 2.0*3.141592654


随后就可以在程序中任何表达式2.0*3.141592654有效的地方,使用这个预定义名称。因此,可以使用以下语句替换前面例子中circumference方法的return语句:


return TWO_PI*radius;


在Objective-C程序中遇到预定义的名称时,使用#define语句中预定义名称右边的所有字符字面替换程序中该点的名称。因此,当预处理程序遇到前面所示的return语句中的名称TWO_PI时,它使用#define语句中相应于该名称的全部字符替换这个名称。因此,只要程序中出现TWO_PI,预处理程序就将其字面替换为2.0*3.141592654。

预定义名称一出现,预处理程序就执行文本替换。这可以解释为什么通常不能使用分号结束#define语句。如果使用了分号,只要出现预定义名称,分号也将替换到程序中。如果如下定义PI:


define PI 3.141592654;


然后这样编写代码:


return 2.0PIr;


那么预处理程序将使用3.151592654;替换预定义名称PI。因此在预处理程序完成替换之后,编译器将把这条语句看作:


return 2.03.141592654;r;


这会导致语法错误。记住,除非十分确定需要分号,否则不要在定义语句的末尾添加分号。

预处理程序定义的右面不必是合法的Objective-C表达式,只要使用它的时候,结果表达式正确就可以了。例如,可以如下设置定义:


define AND&&

define OR||


然后可以如下编写表达式:


if(x>0 AND x<10)

……



if(y==0 OR y==value)

……


甚至可以包含如下定义,用于测试相等性:


define EQUALS==


然后,可以编写如下表达式:


if(y EQUALS 0 OR y EQUALS value)

……


这样就消除使用单个等号错误进行等价判断的可能性。

虽然这些例子显示了#define的强大功能,但是应该注意以这种方式重新定义底层语言语法的行为通常是不好的编程习惯。而且会使其他人难以理解你的代码。

要想更有趣,预定义的值本身可以引用另一个预定义的值。所以,以下两个定义:


define PI 3.141592654

define TWO_PI 2.0*PI


是完全合法的。名称TWO_PI是按照前面的预定义名称PI定义的,这样就不必重复拼写值3.141592654。

如果把这两个定义的顺序颠倒一下,如下所示:


define TWO_PI 2.0*PI

define PI 3.141592654


也是合法的。规则就是:只要在程序中使用预定义名称时所有符号都是定义过的,那么就可以在定义中引用其他预定义的值。

合理地使用定义通常可以减少程序中对注释的需要。考虑如下语句:


if(year%4==0&&year%100!=0||year%400==0)

……


这个表达式检测变量year是不是闰年。现在,考虑以下定义以及后续的if语句:


define IS_LEAP_YEAR year%4==0&&year%100!=0\

||year%400==0

……

if(IS_LEAP_YEAR)

……


通常,预处理程序假设定义包含在程序的一行中。如果需要第二行,那么上一行的最后一个字符必须是反斜线符号。这个字符告诉预处理程序这里存在一个后续,否则将被忽略。对于多个后续行,也是如此;每个要继续的行都必须以反斜线结尾。

这条if语句远比前面的if语句更容易理解。因为该语句很清楚,所以无需注释。当然,这个定义只能限于测试变量year来判断该年是不是闰年。最好能够编写一个定义,它能够判定任何一年是否是闰年,而不只是变量year。实际上,可以编写带有一个或多个自变量的定义。这就引出下一个讨论要点。

可以将IS_LEAP_YEAR定义为带有一个名为y的参数:


define IS_LEAP_YEAR(y)y%4==0&&y%100!=0\

||y%400==0


和方法定义不同,这里没有定义参数y的类型,因为此时仅执行字面文本替换,并没有调用函数。注意,在定义带有参数的名称时,预定义名称和参数列表的左半括号之间不允许空格。

依照前面的定义,可以编写如下语句:


if(IS_LEAP_YEAR(year))

……


该语句判断year的值是不是闰年。或者,可以如下编写来测试nextYear的值是不是闰年:


if(IS_LEAP_YEAR(nextYear))

……


在前面的语句中,IS_LEAP_YEAR的定义直接替换到if语句中,同时只要定义中出现y,就使用参数nextYear替换它。这样,编译器实际上将这个if语句看作:


if(nextYear%4==0&&nextYear%100!=0||nextYear%400==0)

……


预定义(definition)通常称作“宏”。这个术语经常用于带有一个或多个参数的定义。这个宏名为SQUARE,它简单地将参数乘方:


define SQUARE(x)x*x


虽然SQUARE的宏定义简单明了,但是在定义宏时有一个有趣的陷阱,必须小心地避开。我们描述过,语句


y=SQUARE(v);


把V2的值赋给y。你认为对于以下语句会发生什么情况?


y=SQUARE(v+1);


这个语句并不像你期望的那样,把(V+1)2的值赋给y。因为预处理程序对宏定义的参数实行文本替换,前面的表达式实际上是如下求值的:


y=v+1*v+1;


这显然不能得到期望的结果。要正确解决这个问题,需要在SQUARE宏的定义中加入括号:


define SQUARE(x)((x)*(x))


虽然这个定义看起来可能有点奇怪,但要记住定义中任何出现x的任何地方都要使用整个表达式进行字面替换,和SQUARE宏定义的一样。使用新的SQUARE宏定义,语句


y=SQUARE(v+1);


将正确地作为以下表达式进行求值:


y=((v+1)*(v+1));


以下的宏允许你方便地根据Fraction类动态地创建新分数:


define MakeFract(x, y)([[Fraction alloc]initWith:x over:y]])


然后,可以如下编写表达式:


myFract=MakeFract(1,3);//Make the fraction 1/3


或者甚至使用


sum=[MakeFract(n1,d1)add:MakeFract(n2,d2)];


把分数n1/d1和n2/d2相加。

定义宏时,使用条件表达式的运算符可以非常方便。以下语句定义了一个名为MAX的宏,它给出两个值的最大值:


define MAX(a, b)(((a)>(b))?(a):(b))


这个宏允许随后写出这些语句:


limit=MAX(x+y, minValue);


这个式子把x+y和minValue的最大值赋给limit。用括号把整个MAX定义括起来是为了确保正确地计算如下表达式:


MAX(x, y)*100


每个自变量都用括号括起来是为了确保正确地计算如下表达式:


MAX(x&y, z)


&运算符是按位AND运算符,它的优先级低于宏中使用的>运算符。如果宏定义中没有括号,>运算符将在按位AND之前求值,从而导致错误的结果。

以下宏测试字符是不是小写字母:


define IS_LOWER_CASE(x)(((x)>=‘a’)&&((x)<=‘z’))


因此,允许编写以下表达


if(IS_LOWER_CASE(c))

……


甚至可以在另一个宏定义中使用这个宏把字符从小写转换为大写,同时不改变非小写字符:


define TO_UPPER(x)(IS_LOWER_CASE(x)?(x)-‘a’+‘A’:(x))


这里再次用到标准ASCII字符集。在第二部分学习Foundation字符串对象时,将看到如何对国际(Unicode)字符集执行大小写转换。