13.8 定义带参数的宏
在第2章中见到的#define预处理指令始终用于将符号名称与常量数值关联在一起。在第2章中讨论了C预处理器实际上会修改源代码文本:在将源代码转交给编译器之前,它将出现的每个已定义名称用其实际含义代替。本节研究如何定义拥有形参的宏。这样的宏定义的形式为
#define macro_name(parameter list) macro body
像函数一样,宏允许对一个常用的语句或操作给定一个名称。但因为宏是通过原文替换进行处理的,所以宏调用执行时没有与函数相关的栈空间的分配和释放开销。当然,由于宏的含义出现在程序中的每个调用处,由编译器产生的目标文件所需要的内存通常比使用函数而不是宏的同一程序所需内存多一些。
宏(macro) 用于命名常用语句或操作的工具。
图13-13给出了一个简短的程序,它使用一个名为LABEL_PRINT_INT宏来显示一个整型变量或标记(字符串)表达式的值。要注意在定义LABEL_PRINT_INT的指令中,宏名和参数表的左括号之间没有空格。这个细节非常重要,因为如果有空格的话,预处理器会错误解释该宏定义,并且将每个LABEL_PRINT_INT出现的地方替换为
(label, num) printf("%s=%d",(label),(num))
将类似
LABEL_PRINT_INT("rabbit",r)
的宏调用替换为带有恰当参数置换的宏体副本
printf("%s=%d",( "rabbit"),(r))
的过程称为宏扩展。在替换时,C预处理器将每个宏的参数名与相应的实参进行匹配。然后在宏体副本内,每个出现的形参名都由实参来代替。这个已修改的宏体会替换程序文本中的宏调用。图13-14对例程中的最后一个宏调用的宏扩展过程进行了图示。要注意只有宏名及其参数表包含在宏扩展过程中,而宏调用行最后的分号不受影响。在宏体的printf调用后面带一个分号是不正确的。如果那里有一个分号,那么会导致上面两个宏调用的宏扩展产生的语句都将以两个分号结束。
|
853. /* Shows the definition and use of a macro */ 854. 855. #include <stdio.h> 856. 857. #define LABEL_PRINT_INT(label, num) printf("%s = %d", (label), (num)) 858. 859. int 860. main(void) 861. { 862. int r = 5, t = 12; 863. 864. LABEL_PRINT_INT("rabbit", r); 865. printf(" "); 866. LABEL_PRINT_INT("tiger", t + 2); 867. printf("\n"); 868. 869. return(0); 870. } 871. rabbit = 5 tiger = 14 |
图13-13 使用带形参的宏的程序
宏扩展(macro expansion) 将宏调用替换为其含义的过程。
|
LABEL_PRINT_INT("tiger", t + 2)
LABEL_PRINT_INT(label, num) parameter matching "tiger" t + 2
printf("%s = %d", (label), (num)) parameter replacement in body printf("%s = %d", ("tiger"), (t + 2)) result of macro expansion |
图13-14 图13-13程序中的第二个宏调用的宏扩展
13.8.1 在宏体中使用括号
大家可能会注意到在LABEL_PRINT_INT体中,宏的每个形参都出现在括号内。在宏体内使用足够的括号对于正确求值非常重要。图13-15是一个使用宏计算n2的程序片段,其中给出了两种宏定义的版本以及它们导致的不同程序结果。
|
版本1 版本2 #define SQUARE(n) n * n #define SQUARE(n) ((n) * (n)) . . . double x = 0.5, y = 2.0; int n = 4, m = 12; printf("(%.2f + %.2f)squared = %.2f\n\n", x, y, SQUARE(x + y)); printf("%d squared divided by\n", m); printf("%d squared is %d\n", n, SQUARE(m) / SQUARE(n)); (0.5 + 2.0)squared = 3.5 (0.5 + 2.0)squared = 6.25 12 squared divided by 12 squared divided by 4 squared is 144 4 squared is 9 |
图13-15 宏体内的宏调用显示括号的重要性
现在看一下版本1和版本2中出现的不同宏扩展。图13-16的检查揭示了版本1的不正确结果是运算符优先法则造成的。
为了避免出现图13-15和图13-16中的问题,就要在宏体内多多使用括号。具体地说,要将宏体内出现的每个参数都括起来,而且如果产生一个最终数值的话,将整个宏体也用括号括起来。例如,下面是求二次方程的一个实数根的宏:
#define ROOT1(a,b,c) ((-(b)+sqrt((b)*(b)-4*(a)*(c)))/(2*(a)))
黑色的括号是表达式正确求值所需要的,灰色的括号是依照将宏定义括起来的原则而加上的。
在宏调用中作为参数传递的表达式内,应避免使用带有副作用的运算符。这是因为这样的表达式可能会多次求值。例如,语句
r=ROOT1(++n1,n2,n3); /*error: applying ++ in a macro
argument*/
将会扩展为
r=((-(n2)+sqrt((n2)*(n2)-4*(++n1)*(n3)))/(2*(++n1)));
与带有副作用的运算符的对象不能用于表达式的原则相违背。
|
版本1 版本2 SQUARE(x + y) SQUARE(x + y) becomes becomes x + y * x + y ((x + y) * (x + y)) Problem: Multiplication done before addition. SQUARE(m) / SQUARE(n) SQUARE(m) / SQUARE(n) becomes becomes m * m / n * n ((m) * (m)) / ((n) * (n)) Problem: Multiplication and division are of equal precedence; they are performed left to right. |
图13-16 图13-15中宏调用的宏扩展
我们强烈要求大家像前面介绍的那样,在宏体内按常规使用括号,即使你不能想象一对给定的括号会造成任何状况。你只需要对宏花一点时间就会认识到编程者的能力非常有限,不能预见所有可能出现的情况!
我们也鼓励大家在宏名中全部使用大写字母。要记住你调用的是一个宏而不是一个函数,这一点对于帮助你在实参中避免使用带副作用的运算符非常重要。
13.8.2 在两行以上扩展宏
除非程序另有表示,否则预处理器认为宏定义是在一行之内的。为将宏在多行上进行扩展,除了定义的最后一行以外的所有行都必须以反斜杠符\结束。例如,下面是实现一个for语句开头的宏,它从st的值开始计数,直到end的值,但不包括end的值:
#define INDEXED_FOR(ct,st,end) \
for((ct)=(st);(ct)<(end);++(ct))
下面的代码片段使用INDEXED_FOR显示数组x的前X_MAX个元素:
INDEXED_FOR(i,0, X_MAX)
printf("x[%2d]=%6.2f\n",i,x[i]);
在宏扩展后,该语句就变为
for((i)=(0);(i)<( X_MAX);++(i))
printf("x[%2d]=%6.2f\n",i,x[i]);
练习
自测
1. 给定下面的宏定义,写出后面每条语句的宏扩展。如果扩展看起来不是宏定义者所期望的(可以假定宏名是有含义的),指出如何修改宏定义。
#define DOUBLE(x) (x)+(x)
#define DISCRIMINANT (a,b,c) ((b)*(b)-4*(a)*(c))
#define PRINT_PRODUCT(x,y)\
printf("%.2f x %.2f = %.2f\n", (x), (y), (x)*(y));
a. y=DOUBLE(a-b)*c;
b. y=y-DOUBLE(p);
c. if(DISCRIMINANT(a1,b1,c1) == 0)
r1 = -b2 / (2*a1);
d. PRINT_PRODUCT(a+b,a-b);
编程
1. 定义一个名为F_OF_X的宏对下面的多项式求值,并将x的值作为参数传递。可以假定已经包含了数学库。
2. 定义一个宏,用于显示其参数。该参数前有一个美元符号,并且带有两位小数。







