宏定义

来源:互联网 发布:mac迅雷没有下载速度 编辑:程序博客网 时间:2024/04/30 02:04

 

 

 

14.3  宏定义

我们从第2章以来使用的宏被称为简单的宏,它们没有参数。预编译器也支持带参数的宏。本节会先讨论简单的宏,然后再讨论带参数的宏。在分别讨论它们之后,我们会研究一下二者共同的特性。

14.3.1  简单的宏

简单的宏定义有如下格式:

[#define指令(简单的宏)]  #define  标识符 替换列表

替换列表是一系列的C语言记号,包括标识符、关键字、数、字符常量、字符串字面量、运算符和标点符号。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在任何位置出现,预处理器都会用替换列表代替它。

不要在宏定义中放置任何额外的符号,否则它们会被作为替换列表的一部分。一种常见的错误是在宏定义中使用 = :

#define N = 100       /*** WRONG ***/

int a[N];            /* 会成为 int a[= 100]; */

在上面的例子中,我们(错误地)把N定义成一对记号(= 和100)。

在宏定义的末尾使用分号结尾是另一个常见错误:

#define N 100;       /*** WRONG ***/

int a[N];            /*    become int a[100;]; */

这里N被定义为100和;两个记号。

在一个宏定义中,编译器可以检测到绝大多数由多余符号所导致的错误。但不幸的是,编译器会将每一处使用这个宏的地方标为错误,而不会直接找到错误的根源——宏定义本身,因为宏定义已经被预处理器删除了。

简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”(manifest constant)的东西。使用宏,我们可以给数值、字符和字符串命名。

#define STE_LEN 80

#define TRUE     1

#define FALSE    0

#define PI       3.14159

#define CR        '/r'

#define EOS       '/0'

使用#define来为常量命名有许多显著的优点:

l 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法数”,使读者难以理解。

l 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码的”常量会更难于修改,特别是有时候当他们以稍微不同的形式出现时。(例如,如果一个程序包含一个长度为100的数组,它可能会包含一个从0到99的循环。如果我们只是试图找到所有程序中出现的100,那么就会漏掉99。)

l 可以帮助避免前后不一致或键盘输入错误。假如数值常量3.14159在程序中大量出现,它可能会被意外地写成3.1416或3.14195。

虽然简单的宏常用于定义常量名,但是它们还有其他应用。

l 可以对C语法做小的修改。实际上,我们可以通过定义宏的方式给C语言符号添加别名,从而改变C语言的语法。例如,对于习惯使用Pascal的begin和end(而不是C语言的{和})的程序员,可以定义下面的宏:

#define BEGIN   {

#define END     }

我们甚至可以发明自己的语言。例如,我们可以创建一个LOOP“语句”,来实现一个无限循环:

#define LOOP    for (;;)

当然,改变C语言的语法通常不是个好主意,因为它会使程序很难被其他程序员所理解。

l 对类型重命名。在5.2节中,我们通过重命名int创建了一个Boolean类型:

#define BOOL int

虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义(7.6节)仍然是定义新类型的最佳方法。

l 控制条件编译。如将在14.4节中看到的那样,宏在控制条件编译中起重要的作用。例如,在程序中出现的宏定义可能表明需要将程序在“调试模式”下进行编译,来使用额外的语句输出调试信息:

#define DEBUG

这里顺便提一下,如上面的例子所示,宏定义中的替换列表为空是合法的。

当宏作为常量使用时,C程序员习惯在名字中只使用大写字母。但是并没有如何将用于其他目的的宏大写的统一做法。由于宏(特别是带参数的宏)可能是程序中错误的来源,所以一些程序员更喜欢使用大写字母来引起注意。其他人则倾向于小写,即按照Kernighan和Ritchie编写的The C Programming Language一书中的样式。

14.3.2  带参数的宏

带参数的宏定义有如下格式:

[#define指令—带参数的宏]  #define 标识符(x1,x2,…,xn)替换列表

其中x1, x2,…, xn是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次。

在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为是在定义一个简单的宏,其中(x1, x2,…, xn)是替换列表的一部分。

当预处理器遇到一个带参数的宏,会将定义存储起来以便后面使用。在后面的程序中,如果任何地方出现了标识符(y1, y2,…, yn)格式的宏调用(其中y1, y2,…, yn是一系列标记),预处理器会使用替换列表替代,并使用y1替换x1y2替换x2,依此类推。

例如,假定我们定义了如下的宏:

#define MAX(x,y)     ((x)>(y) ? (x) :(y))

#define IS_EVEN(n)   ((n)%2==0)

现在如果后面的程序中有如下语句:

i = MAX(j+k, m-n);

if (IS_EVEN(i)) i++;

预处理器会将这些行替换为

i = ((j+k)>(m-n)?(j+k):(m-n));

if (((i)%2==0)) i++;

如这个例子所显示的,带参数的宏经常用来作为一些简单的函数使用。MAX类似一个从两个值中选取较大的值的函数。IS_EVEN则类似于另一种函数,该函数当参数为偶数时返回1,否则返回0。

下面的例子是一个更复杂的宏:

#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))

这个宏检测一个字符c是否在'a'与'z'之间。如果在的话,这个宏会用'c'减去'a'再加上'A',来计算出c所对应的大写字母。如果c不在这个范围,就保留原来的c。像这样的字符处理的宏非常有用,所以C语言库在<ctype.h>(23.4节)中提供了大量的类似的宏。其中之一就是toupper,与我们上面的TOUPPER例子作用一致(但会更高效,可移植性也更好)。

带参数的宏可以包含空的参数列表,如下例所示:

#definegetchar() getc(stdin)

空的参数列表不是一定确实需要,但可以使getchar更像一个函数。(没错,这就是<stdio.h>中的getchar,getchar的确就是个宏,不是函数——虽然它的功能像个函数。)

          使用带参数的宏替代实际的函数有两个优点:

l 程序可能会稍微快些。一个函数调用在执行时通常会有些额外开销——存储上下文信息、复制参数的值等。而一个宏的调用则没有这些运行开销。

l 宏会更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用MAX宏从两个数中选出较大的一个,数的类型可以是int,long int,float,double等等。

但是带参数的宏也有一些缺点。

l 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此导致程序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效果就越明显。当宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思考一下,如果我们用MAX宏来找出3个数中最大的数会怎样?

n = MAX(i, MAX(j, k));

下面是预处理后的这条语句:

n=((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k))));

l 宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们是否是正确的类型。如果不是,或者将参数转换成正确的类型,或者由编译器产生一个出错信息。预处理器不会检查宏参数的类型,也不会进行类型转换。

l 无法用一个指针来指向一个宏。如在17.7节中将看到的,C语言允许指针指向函数。这一概念在特定的编程条件下非常有用。宏会在预处理过程中被删除,所以不存在类似的“指向宏的指针”。因此,宏不能用于处理这些情况。

l 宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生意外的结果。考虑下面的例子,其中MAX的一个参数有副作用:

n = MAX(i++, j);

下面是这条语句在预处理之后的结果:

n = ((i++)>(j)?(i++):(j));

如果i大于j,那么i可能会被(错误地)增加了两次,同时n可能被赋予了错误的值。

由于多次计算宏的参数而导致的错误可能非常难于发现,因为宏调用和函数调用看起来是一样的。更糟糕的是,这类宏可能在大多数情况下正常工作,仅在特定参数有副作用时失效。为了自保护,最好避免使用带有副作用的参数。

带参数的宏不仅适用于模拟函数调用。他们特别经常被作为模板,来处理我们经常要重复书写的代码段。如果我们已经写烦了语句

printf("%d"/n, x);

因为每次要显示一个整数x都要使用它。我们可以定义下面的宏,使显示整数变得简单些:

#define PRINT_INT(x)     printf("%d/n", x)

一旦定义了PRINT_INT,预处理器会将这行

PRINT_INT(i/j);

转换为

printf("%d/n",i/j);

14.3.3  #运算符

宏定义可以包含两个运算符:#和##。编译器不会识别这两种运算符相反,它们会在预处理时被执行。

#运算符将一个宏的参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。(一些C程序员将#操作理解为“stringization(字符串化)”;其他人则认为这实在是对英语的滥用。)

          #运算符有大量的用途,这里只来讨论其中的一种。假设我们决定在调试过程中使用PRINT_INT宏作为一个便捷的方法,来输出一个整型变量或表达式的值。#运算符可以使PRINT_INT为每个输出的值添加标签。下面是改进后的PRINT_INT:

#definePRINT_INT(x) printf(#x " = %d/n", x)

x之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字符串字面量。因此,调用

PRINT_INT(i/j);

会变为

printf("i/j"" = %d/n", i/j);

在C语言中相邻的字符串字面量会被合并,因此上边的语句等价于:

printf("i/j= %d/n", i/j);

当程序执行时,printf函数会同时显示表达式i/j和它的值。例如,如果i是11,j是2的话,输出为

i/j =5

14.3.4  ##运算符

##运算符可以将两个记号(例如标识符)“粘”在一起,成为一个记号。(无需惊讶,##运算符被称为“记号粘合”。)如果其中一个操作数是宏参数,“粘合”会在当形式参数被相应的实际参数替换后发生。考虑下面的宏:

#defineMK_ID(n) i##n

当MK_ID被调用时(比如MK_ID(1)),预处理器首先使用自变量(这个例子中是1)替换参数n。接着,预处理器将i和1连接成为一个记号(i1)。下面的声明使用MK_ID创建了3个标识符:

intMK_ID(1), MK_ID(2), MK_ID(3);

预处理后声明变为:

inti1, i2, i3;

##运算符不属于预处理器经常使用的特性。实际上,想找到一些使用它的情况是比较困难的。为了找到一个有实际意义的##的应用,我们来重新思考前面提到过的MAX宏。如我们所见,当MAX的参数有副作用时会无法正常工作。一种解决方法是用MAX宏来写一个max函数。遗憾的是,往往一个max函数是不够的。我们可能需要一个实际参数是int值的max函数,还需要参数为float值的max函数,等等。除了实际参数的类型和返回值的类型之外,这些函数都一样。因此,这样定义每一个函数似乎是个很蠢的做法。

          解决的办法是定义一个宏,并使它展开后成为max函数的定义。宏会有唯一的参数type,它表示形式参数和返回值的类型。这里还有个问题,如果我们是用宏来创建多个max函数,程序将无法编译。(C语言不允许在同一文件中出现两个同名的函数。)为了解决这个问题,我们是用##运算符为每个版本的max函数构造不同的名字。下面是宏的显示形式:

#define GENERIC_MAX(type)           /

type type##_max(typex,  type y)    /

{                                       /

   return x >y ? x :y;               /

}

请注意宏的定义中是如何将type和_max相连来形成新函数名的。

现在,假如我们需要一个针对float值的max函数。下面是如何使用GENERIC_MAX宏来定义函数:

GENERIC_MAX(float)

预处理器会将这行展开为下面的代码:

floatfloat_max(float x, float y) { return x > y ? x : y; }

14.3.5  宏的通用属性

现在我们已经讨论过简单的宏和带参数的宏了,我们来看一下它们都需要遵守的规则。

l 宏的替换列表可以包含对另一个宏的调用。例如,我们可以用宏PI来定义宏TWO_PI:

#define PI      3.14159

#define TWO_PI  (2*PI)

当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止。

l 预处理器只会替换完整的记号,而不会替换记号的片断。因此,预处理器会忽略嵌在标识符名、字符常量、字符串字面量之中的宏名。例如,假设程序含有如下代码行:

#defineSIZE 256

intBUFFER_SIZE;

if(BUFFER_SIZE > SIZE)

  puts ("Error :SIZE exceeded");

预处理后,这些代码行会变为:

intBUFFER_SIZE;

if(BUFFER_SIZE > 256)

 puts ("Error :SIZEexceeded");

标识符BUFFER_ZISE和字符串"Error: SIZE exceeded"没有被预处理影响,虽然它们都包含SIZE。

l 一个宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,他们不遵从通常的范围规则。一个定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。

l 宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。

l 宏可以使用#undef指令“取消定义”。#undef指令有如下形式:

[#undef指令]  #undef  标识符

其中标识符是一个宏名。例如,指令

#undef N

会删除宏N当前的定义。(如果N没有被定义成一个宏,#undef指令没有任何作用。)#undef指令的一个用途是取消一个宏的现有定义,以便于重新给出新的定义。

14.3.6  宏定义中的圆括号

在我们前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗?答案是绝对需要。如果我们少用几个圆括号,宏可能有时会得到意料之外的——而且是不希望有的——结果。

对于在一个宏定义中哪里要加圆括号有两条规则要遵守。首先,如果宏的替换列表中有运算符,那么始终要将替换列表放在括号中:

#define TWO_PI (2*3.14159)

其次,如果宏有参数,每次参数在替换列表中出现时都要放在圆括号中:

#defineSCALE(x) ((x)*10)

没有括号的话,我们将无法确保编译器会将替换列表和参数作为完整的表达式。编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。

为了展示为替换列表添加圆括号的重要性,考虑下面的宏定义,其中的替换列表没有添加圆括号:

#define TWO_PI 2*3.14159

  /* 需要给替换列表加圆括号 */

在预处理时,语句

conversion_factor= 360/TWO_PI;

变为

conversion_factor= 360/2*3.14159;

除法会在乘法之前执行,产生的结果并不是期望的结果。

当宏有参数时,仅给替换列表添加圆括号是不够的。参数的每一次出现都要添加圆括号。例如,假设SCALE定义如下:

#defineSCALE(x) (x*10)   /* 需要给x添加括号 */

在预处理过程中,语句

j =SCALE(i+1);

变为

j =(i+1*10);

由于乘法的优先级比加法高,这条语句等价于

j =i+10;

当然,我们希望的是

j =(i+1)*10;

在宏定义中缺少圆括号会导致C语言中最让人讨厌的错误。程序通常仍然可以编译通过,而且宏似乎也可以工作,仅在少数情况下会出错。

14.3.7  创建较长的宏

在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字符串显示出来:

#defineECHO(s) (get(s), puts(s))

gets函数和puts函数的调用都是表达式,因此使用逗号运算符连接它们是合法的。我们甚至可以把ECHO宏当作一个函数来使用:

ECHO(str);  /* 替换为 (gets(str), puts(str)); */

除了使用逗号运算符,我们也许还可以将gets函数和puts函数的调用放在大括号中形成复合语句:

#defineECHO(s)  { gets(s);  puts(s);  }

遗憾的是,这种方式并不奏效。假如我们将ECHO宏用于下面的if语句:

if (echo_flag)

  ECHO(str);

else

  gets(str);

将ECHO宏替换会得到下面的结果:

if (echo_flag)

  { gets(str);  puts(str);  };

else

  gets(str);

编译器会将头两行作为完整的if语句:

if (echo_flag)

  { gets(str);  puts(str);  }

编译器会将跟在后面的分号作为空语句,并且对else子句产生出错信息,因为它不属于任何if语句。我们可以通过记住永远不要在ECHO宏后面加分号来解决这个问题。但是这样做会使程序看起来有些怪异。

逗号运算符可以解决ECHO宏的问题,但并不能解决所有宏的问题。假如一个宏需要包含一系列的语句,而不仅仅是一系列的表达式,这时逗号运算符就起不到帮助的作用了。因为它只能连接表达式,不能连接语句。解决的方法是将语句放在do循环中,并将条件设置为假:

do { ...} while (0)

do循环必须始终随跟着一个分号,因此我们不会遇到在if语句中使用宏那样的问题了。为了看到这个技巧(嗯,应该说是技术)的实际作用,让我们将它用于ECHO宏中:

#defineECHO(s)       /

          do{           /

           gets (s) ;      /

           puts (s) ;      /

           } while  (0)

当使用ECHO宏时,一定要加分号:

ECHO(str);

  /* becomes do {  gets(str);puts(str);  } while (0);  */

14.3.8  预定义宏

在C语言中预定义了一些有用的宏,见表14.1。这些宏主要是提供当前编译的信息。宏__LINE__和__STDC__是整型常量,其他3个宏是字符串字面量。我们在本章的后面会用到__STDC__宏,因此这里将重点放在其他的宏上。

表14-1  预定义宏

名  字

描  述

__LINE__

__FILE__

__DATE__

__TIME__

__STDC__

被编译的文件的行数

被编译的文件的名字

编译的日期(格式"Mmm dd yyyy")

编译的时间(格式"hh:mm:ss")

如果编译器接受标准C,那么值为1

__DATE__宏和__TIME__宏指明程序编译的时间。例如,假设程序以下面的语句开始:

printf("Wacky Windows (c) 1996 WackySoftware, Inc./n");

printf("Compiled on %s at %s/n", __DATE__, __TIME__);

每次程序开始执行,程序都会显示下面两行:

Wacky Windows (c) 1996 Wacky Software, Inc.

Compiled on Dec 23 1996 at 22:18:48

这样的信息可以帮助区分同一个程序的不同版本。

我们可以使用__LINE__宏和__FILE__宏来找到错误。考虑下面这个检测被零除的除法的发生位置的问题。当一个C程序因为被零除而导致中止时,通常没有信息指明哪条除法运算导致错误。下面的宏可以帮助我们查明错误的根源:

#define CHECK_ZERO(divisor)  /

  if (divisor == 0)  /

    printf("*** Attemptto divide by zero on line %d  "  /

            "of file %s  ***/n",__LINE__, __FILE__)

CHECK_ZERO宏应该在除法运算前被调用:

CHECK_ZERO(j);

k = i / j;

如果j是0,会显示出如下形式的信息:

*** Attempt to divide by zero on line 9 of file FOO.c ***

类似这样的错误检测的宏非常有用。实际上,C语言库提供了一个通用的、用于错误检测的宏——assert宏(24.1节)

 

原创粉丝点击