C/C++ #define详解

#define预处理器指令和其他预处理器指令一样,以#号作为一行的开 始。ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和 指令的其余部分之间有空格。但是旧版本的C要求指令从一行最左边开始, 而且#和指令其余部分之间不能有空格。

指令可以出现在源文件的任何地 方,其定义从指令出现的地方到该文件末尾有效。我们大量使用#define指令来定义明示常量(manifest constant)(也叫做符号常量),但是该指令还有许多其他用途。

**预处理器指令从#开始运行,到后面的第1个换行符为止。**也就是说,指令的长度仅限于一行。然而,前面提到过,在预处理开始前,编译器会把多行物理行处理为一行逻辑行。

程序清单16.1 preproc.c程序
/* preproc.c -- 简单的预处理示例 */
#include <stdio.h>
#define TWO 2   /* 可以使用注释 */
#define OW "Consistency is the last refuge of the unimagina\
tive.- Oscar Wilde" /* 反斜杠把该定义延续到下一行 */
#define FOUR TWO*TWO
#define PX printf("X is %d.\n", x)
#define FMT "X is %d.\n"
int main(void)
{
int x = TWO;
PX;
x = FOUR;
printf(FMT, x);
printf("%s\n", OW);
printf("TWO: OW\n");
return 0;
}

每行#define(逻辑行)都由3部分组成。
第1部分是#define指令本身。
第 2部分是选定的缩写,也称为宏。有些宏代表值(如本例),这些宏被称为 类对象宏(object-like macro)。C 语言还有类函数宏(function-like macro),稍后讨论。宏的名称中不允许有空格,而且必须遵循C变量的命 名规则:只能使用字符、数字和下划线(_)字符,而且首字符不能是数字。
第3部分(指令行的其余部分)称为替换列表或替换体(见图16.1)。 一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。注意,可以在#define行使用标准C注释。如前所述,每条注释都会被一个空格代替。
在这里插入图片描述运行该程序示例后,输出如下:

X is 2.
X is 4.
Consistency is the last refuge of the unimaginative.- Oscar Wilde
TWO: OW

下面分析具体的过程。下面的语句:

int x = TWO;

变成了:

int x = 2;

2代替了TWO。而语句:

PX;

变成了:

printf("X is %d.\n", x);

这里同样进行了替换。这是一个新用法,到目前为止我们只是用宏来表 示明示常量。从该例中可以看出,宏可以表示任何字符串,甚至可以表示整 个 C 表达式。但是要注意,虽然 PX 是一个字符串常量,它只打印一个名为 x的变量。
下一行也是一个新用法。读者可能认为FOUR被替换成4,但是实际的 过程是:

x = FOUR;
变成了:
x = TWO*TWO;
即是:
x = 2*2;

宏展开到此处为止。由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的乘法运算,这一过程在 编译时进行。预处理器不做计算,不对表达式求值,它只进行替换。
注意,宏定义还可以包含其他宏(一些编译器不支持这种嵌套功能)。
程序中的下一行:

printf (FMT, x);
变成了:
printf("X is %d.\n",x);

相应的字符串替换了 FMT。如果要多次使用某个冗长的字符串,这种 方法比较方便。另外,也可以用下面的方法:

const char * fmt = "X is %d.\n";

然后可以把fmt作为printf()的格式字符串。
下一行中,用相应的字符串替换OW。双引号使替换的字符串成为字符 串常量。编译器把该字符串储存在以空字符结尾的数组中。因此,下面的指 令定义了一个字符常量:

#define HAL 'Z'
而下面的指令则定义了一个字符串(Z\0):
#define HAP "Z"

在程序示例16.1中,我们在一行的结尾加一个反斜杠字符使该行扩展至 下一行:

#define OW "Consistency is the last refuge of the unimagina\
tive.- Oscar Wilde"

注意,第2行要与第1行左对齐。如果这样做:

#define OW "Consistency is the last refuge of the unimagina\
tive.- Oscar Wilde"

那么输出的内容是:

Consistency is the last refuge of the unimagina tive.- Oscar Wilde

第2行开始到tive之间的空格也算是字符串的一部分。
一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替 换。如果替换的字符串中还包含宏,则继续替换这些宏。唯一例外的是双引号中的宏。因此,下面的语句:

printf("TWO: OW");

打印的是TWO: OW,而不是打印:

2: Consistency is the last refuge of the unimaginative.- Oscar Wilde

要打印这行,应该这样写:

printf("%d: %s\n", TWO, OW);

这行代码中,宏不在双引号内。

那么,何时使用字符常量?对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。
如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数。如果数字是系统代码(如,EOF),用符号常量表示的代码更容易移植(只需改变EOF的定义)。助记、易更改、可移植,这些都是符号常量很有价值的特性。

C语言现在也支持const关键字,提供了更灵活的方法。用const可以创建 在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方 面,宏常量可用于指定标准数组的大小和const变量的初始值。

#define LIMIT 20
const int LIM = 50;
static int data1[LIMIT];    // 有效
static int data2[LIM];     // 无效
const int LIM2 = 2 * LIMIT;  // 有效
const int LIM3 = 2 * LIM;   // 无效

这里解释一下上面代码中的“无效”注释。在C中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的必须是整型常量的组合(如 5)、枚举常量和sizeof表达式,不包括const声明的值(这也是C++和C的区 别之一,在C++中可以把const值作为常量表达式的一部分)。但是有的实 现可能接受其他形式的常量表达式。

从技术角度来看,可以把宏的替换体看作是记号(token)型字符串, 而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。例如:

#define FOUR 2*2

该宏定义有一个记号:2*2序列。但是,下面的宏定义中:

#define SIX 2 * 3

有3个记号:2、*、3。
替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不 同。考虑下面的定义:

#define EIGHT 4 * 8

如果预处理器把该替换体解释为字符型字符串,将用4 * 8替换EIGHT。 即,额外的空格是替换体的一部分。
如果预处理器把该替换体解释为记号型字符串,则用3个的记号4 * 8(分别由单个空格分隔)来替换EIGHT。换而言之,解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。
在实际应用中,一些C编译器 把宏替换体视为字符串而不是记号。在比这个例子更复杂的情况下,两者的 区别才有实际意义。
顺带一提,C编译器处理记号的方式比预处理器复杂。由于编译器理解 C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以 把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符。

重定义常量

假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程 称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义 相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出 警告。ANSI标准采用第1种方案,只有新定义和旧定义完全相同才允许重定 义。具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。因 此,下面两个定义相同:

#define SIX 2 * 3
#define SIX 2 * 3

这两条定义都有 3 个相同的记号,额外的空格不算替换体的一部分。而 下面的定义则与上面两条宏定义不同:

#define SIX 2*3

这条宏定义中只有一个记号,因此与前两条定义不同。如果需要重定义 宏,使用#undef 指令(稍后讨论)。
如果确实需要重定义常量,使用const关键字和作用域规则更容易些。

在#define中使用参数

在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有 参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆 括号中可以有一个或多个参数,随后这些参数出现在替换体中,如图所示。

在这里插入图片描述
下面是一个类函数宏的示例:

#define SQUARE(X) X*X

在程序中可以这样用:

z = SQUARE(2);

这看上去像函数调用,但是它的行为和函数调用完全不同。程序清单 16.2演示了类函数宏和另一个宏的用法。该示例中有一些陷阱,请读者仔细 阅读序。
程序清单16.2 mac_arg.c程序

/* mac_arg.c -- 带参数的宏 */
#include <stdio.h>
#define SQUARE(X) X*X
1206
#define PR(X)  printf("The result is %d.\n", X)
int main(void)
{
int x = 5;
int z;
printf("x = %d\n", 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) X*X

这里,SQUARE 是宏标识符,SQUARE(X)中的 X 是宏参数,XX 是替 换列表。程序清单 16.2 中出现SQUARE(X)的地方都会被XX替换。这与前 面的示例不同,使用该宏时,既可以用X,也可以用其他符号。宏定义中的 X由宏调用中的符号代替。因此,SQUARE(2)替换为2*2,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+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/2
2
根据优先级规则,从左往右对表达式求值:(100/2)2,即502,得 100。把SQUARE(x)定义为下面的形式可以解决这种混乱:
#define SQUARE(x) (xx)
这样修改定义后得100/(2
2),即100/4,得25。
要处理前面的两种情况,要这样定义:

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

因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序。
尽管如此,这样做还是无法避免程序中最后一种情况的问题。 SQUARE(++x)变成了++x*++x,递增了两次x,一次在乘法运算之前,一次 在乘法运算之后:

++x*++x = 6*7 = 42

由于标准并未对这类运算规定顺序,所以有些编译器得 76。而有些编 译器可能在乘法运算之前已经递增了x,所以77得49。在C标准中,对该表 达式求值的这种情况称为未定义行为。无论哪种情况,x的开始值都是5,虽 然从代码上看只递增了一次,但是x的最终值是7。
解决这个问题最简单的方法是,避免用++x 作为宏参数。一般而言,不 要在宏中使用递增或递减运算符。但是,++x可作为函数参数,因为编译器 会对++x求值得5后,再把5传递给函数。

一些函数(如 printf())接受数量可变的参数。stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提 供了这样的工具。虽然标准中未使用“可变”(variadic)这个词,但是它已成为描述这种工具的通用词(虽然,C标准的索引添加了字符串化 (stringizing)词条,但是,标准并未把固定参数的函数或宏称为固定函数和不 变宏)。
通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这 一功能。这样,预定义宏

_ _VA_ARGS_ _可用在替换部分中,表明省略号代表什么。例如,下面 的定义:
#define PR(...) printf(_ _VA_ARGS_ _)
假设稍后调用该宏:
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
对于第1次调用,_ _VA_ARGS_ _展开为1个参数:"Howdy"。
对于第2次调用,_ _VA_ARGS_ _展开为3个参数:"weight = %d, shipping = $%.2f\n"、wt、sp。
因此,展开后的代码是:
printf("Howdy");
printf("weight = %d, shipping = $%.2f\n", wt, sp);

程序清单16.5演示了一个示例,该程序使用了字符串的串联功能和#运 算符。
程序清单16.5 variadic.c程序

// variadic.c -- 变参宏
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}1个宏调用,X的值是1,所以#X变成"1"。展开后成为:
print("Message " "1" ": " "x = %g\n", x);
然后,串联4个字符,把调用简化为:
print("Message 1: x = %g\n", x);
下面是该程序的输出:
Message 1: x = 48
Message 2: x = 48.00, y = 6.9282
记住,省略号只能代替最后的宏参数:
#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y //不能这样做
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值