在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中,如下图所示。
C语言之在#define中使用参数
Parts of a function-like macro definition
下面是一个类函数宏的示例:
#define SQUARE(X) X*X
在程序中可以这样用:
z = SQUARE(2);
这看上去像函数调用,但是它的行为和函数调用完全不同。程序macarg.c 演示了类函数宏和另一个宏的用法。该示例中有一些陷阱,请读者仔细阅读序。
The macarg.c Program
/mac_arg.c -- macros with arguments/
#include #define SQUARE(X) X*X
#define PR(X) printf("The result is %d.n", X)
int main(void)
{
int x = 5;
int z;
printf("x = %dn", x);
z = SQUARE(x);
printf("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(x+2): ");
PR(SQUARE(x+2));
printf("Evaluating 100/SQUARE(2): ");
PR(100/SQUARE(2));
printf("x is %d.n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %x.n", x);
return 0;
}
SQUARE宏的定义如下:
#define SQUARE(X) XX
这里,SQUARE是宏标识符,SQUARE(X)中的X是宏参数,XX是替换列表。程序清单16.2中出现SQUARE(X)的地方都会被XX替换。这与前面的示例不同,使用该宏时,既可以用X,也可以用其他符号。宏定义中的X由宏调用中的符号代替。因此,SQUARE(2)替换为22,X实际上起到参数的作用。 然而,稍后你将看到,宏参数与函数参数不完全相同。下面是程序的输出。注意有些内容可能与我们的预期不符。实际上,你的编译器输出甚至与下面的结果完全不同。
x = 5 Evaluating SQUARE(x): The result is 25. Evaluating SQUARE(2): The result is 4. Evaluating SQUARE(x+2): The result is 17. Evaluating 100/SQUARE(2): The result is 100. x is 5. Evaluating SQUARE(++x): The result is 42. After incrementing, x is 7.
前两行与预期相符,但是接下来的结果有点奇怪。程序中设置x的值为5,你可能认为SQUARE(x+2)应该是77,即49。但是,输出的结果是17,这不是一个平方值!导致这样结果的原因是,我们前面提到过,预处理器不做计算、不求值,只替换字符序列。预处理器把出现x的地方都替换成x+2。因此,xx变成了x+2x+2。如果x为5,那么该表达式的值为: xx 变成了 x+2*x+2。
如果x为5,那么该表达式的值为:
5+2*5+2 = 5 + 10 + 2 = 17
该例演示了函数调用和宏调用的重要区别。函数调用在程序运行时把参数的值传递给函数。宏调用在编译之前把参数记号传递给程序。这两个不同的过程发生在不同时期。是否可以修改宏定义让SQUARE(x+2)得36?当然可以,要多加几个圆括号:
#define SQUARE(x) (x)(x)
现在SQUARE(x+2)变成了(x+2)(x+2),在替换字符串中使用圆括号就得到符合预期的乘法运算。但是,这并未解决所有的问题。下面的输出行:
100/SQUARE(2)
将变成:
100/22
根据优先级规则,从左往右对表达式求值:(100/2)2,即50*2,得100。把SQUARE(x)定义为下面的形式可以解决这种混乱:
#define SQUARE(x) (xx)
这样修改定义后得100/(22),即100/4,得25。要处理前面的两种情况,要这样定义:
#define SQUARE(x) ((x)(x))
因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序。尽管如此,这样做还是无法避免程序中最后一种情况的问题。SQUARE(++x)变成了++x++x,递增了两次x,一次在乘法运算之前,一次在乘法运算之后:
SQUARE(++x)
变成了:
++x*++x
递增了两次x,一次在乘法运算之前,一次在乘法运算之后:
++x++x = 67 = 42
由于标准并未对这类运算规定顺序,所以有些编译器得76。而有些编译器可能在乘法运算之前已经递增了x,所以77得49。在C标准中,对该表达式求值的这种情况称为未定义行为。无论哪种情况,x的开始值都是5,虽然从代码上看只递增了一次,但是x的最终值是7。解决这个问题最简单的方法是,避免用++x作为宏参数。一般而言,不要在宏中使用递增或递减运算符。但是,++x可作为函数参数,因为编译器会对++x求值得5后,再把5传递给函数。
1 用宏参数创建字符串:#运算符
下面是一个类函数宏:
#define PSQR(X) printf("The square of X is %d.n", ((X)*(X)));
假设这样使用宏:
PSQR(8);
输出为:
The square of X is 64.
注意双引号字符串中的X被视为普通文本,而不是一个可被替换的记号。C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化(stringizing)。程序清单16.3演示了该过程的用法。
Listing 16.3 The subst.c Program
/subst.c -- substitute in string/
#include #define PSQR(x) printf("The square of " #x " is %d.n",((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
该程序的输出如下:
The square of y is 25. The square of 2 + 4 is 36.
调用第1个宏时,用"y"替换#x。调用第2个宏时,用"2+4"替换#x。ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生成最终的字符串。例如,第1次调用变成:
printf("The square of " "y" " is %d.n",((y)*(y)));
然后,字符串串联功能将这3个相邻的字符串组合成一个字符串:
"The square of y is %d.\n"
2 预处理器黏合剂:##运算符
与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。例如,可以这样做:
#define XNAME(n) x ## n
然后,宏XNAME(4)将展开为x4。程序清单16.4演示了##作为记号粘合剂的用法。
Listing 16.4 The glue.c Program
// glue.c -- use the ## operator
#include #define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %dn", x ## n);
int main(void)
{
int XNAME(1) = 14; // becomes int x1 = 14;
int XNAME(2) = 20; // becomes int x2 = 20;
int x3 = 30;
PRINT_XN(1); // becomes printf("x1 = %dn", x1);
PRINT_XN(2); // becomes printf("x2 = %dn", x2);
PRINT_XN(3); // becomes printf("x3 = %dn", x3);
return 0;
}
该程序的输出如下:
x1 = 14
x2 = 20
x3 = 30
注意,PRINTXN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。
3 变参宏:…和VAARGS
一些函数(如printf())接受数量可变的参数。stdvar.h头文件(本章后面介绍)提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。虽然标准中未使用“可变”(variadic)这个词,但是它已成为描述这种工具的通用词(虽然,C标准的索引添加了字符串化(stringizing)词条,但是,标准并未把固定参数的函数或宏称为固定函数和不变宏)。通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这一功能。这样,预定义宏 _ VAARGS 可用在替换部分中,表明省略号代表什么。例如,下面的定义:
#define PR(...) printf(__VA_ARGS__)
假设稍后调用该宏:
PR("Howdy");
PR("weight = %d, shipping = $%.2fn", wt, sp);
对于第1次调用,_ VAARGS 展开为1个参数:"Howdy"。对于第2次调用, VAARGS 展开为3个参数:
"weight = %d, shipping = $%.2fn", wt, sp
因此,展开后的代码是:
printf("Howdy");
printf("weight = %d, shipping = $%.2fn", wt, sp);
程序variadic.c演示了一个示例,该程序使用了字符串的串联功能和#运算符。
// variadic.c -- variadic macros
#include #include #define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %gn", x);
PR(2, "x = %.2f, y = %.4fn", x, y);
return 0;
}
第1个宏调用,X的值是1,所以#X变成"1"。展开后成为:
print("Message " "1" ": " "x = %gn", x);
然后,串联4个字符,把调用简化为:
print("Message 1: x = %gn", x);
下面是该程序的输出:
Message 1: x = 48
Message 2: x = 48.00, y = 6.9282
记住,省略号只能代替最后的宏参数:
#define WRONG(X, ..., Y) #X #__VA_ARGS__ #y // won't work