首页 > 编程笔记 > C语言笔记

C语言宏的定义和宏的使用方法(#define)

在 C 语言中,可以采用命令 #define 来定义宏。该命令允许把一个名称指定成任何所需的文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。

关于宏的一个常见应用就是,用它定义数值常量的名称:
#define         ARRAY_SIZE 100
double   data[ARRAY_SIZE];
这两行代码为值 100 定义了一个宏名称 ARRAY_SIZE,并且在数组 data 的定义中使用了该宏。惯例将宏名称每个字母采用大写,这有助于区分宏与一般的变量。上述简单的示例也展示了宏是怎样让 C 程序更有弹性的。

通常情况下,程序中往往多次用到数组(例如上述 data)的长度,例如,采用数组元素来控制 for 循环遍历次数。当每次用到数组长度时,用宏名称而不要直接用数字,如果程序的维护者需要修改数组长度,只需要修改宏的定义即可,即 #define 命令,而不需要修改程序中每次用到每个数组长度的地方。

在翻译的第三个步骤中,预处理器会分析源文件,把它们转换为预处理器记号和空白符。如果遇到的记号是宏名称,预处理器就会展开(expand)该宏;也就是说,会用定义的文本来取代宏名称。出现在字符串字面量中的宏名称不会被展开,因为整个字符串字面量算作一个预处理器记号。

无法通过宏展开的方式创建预处理器命令。即使宏的展开结果会生成形式上有效的命令,但预处理器不会执行它。

在宏定义时,可以有参数,也可以没有参数。

没有参数的宏

没有参数的宏定义,采用如下形式:
#define 宏名称 替换文本

“替换文本”前面和后面的空格符不属于替换文本中的内容。替代文本本身也可以为空。下面是一些示例:
#define TITLE "*** Examples of Macros Without Parameters ***"
#define BUFFER_SIZE (4 * 512)
#define RANDOM (-1.0 + 2.0*(double)rand() / RAND_MAX)

标准函数 rand()返回一个伪随机整数,范围在 [0,RAND_MAX] 之间。rand()的原型和 RAND_MAX 宏都定义在标准库头文件 stdlib.h 中。

下面的语句展示了上述宏的一种可能使用方式:
#include <stdio.h>
#include <stdlib.h>
/* ... */
// 显示标题
puts( TITLE );

// 将流fp设置成“fully buffered”模式,其具有一个缓冲区,
// 缓冲区大小为BUFFER_SIZE个字节
// 宏_IOFBF在stdio.h中定义为0
static char myBuffer[BUFFER_SIZE];
setvbuf( fp, myBuffer, _IOFBF, BUFFER_SIZE );

// 用ARRAY_SIZE个[-10.0, +10.0]区间内的随机数值填充数组data
for ( int i = 0; i < ARRAY_SIZE; ++i )
  data[i] = 10.0 * RANDOM;

用替换文本取代宏,预处理器生成下面的语句:
puts( "*** Examples of Macros Without Parameters ***" );

static char myBuffer[(4 * 512)];
setvbuf( fp, myBuffer, 0, (4 * 512) );

for ( int i = 0; i < 100; ++i )
data[i] = 10.0 * (-1.0 + 2.0*(double)rand() / 2147483647);

在上例中,该实现版本中的 RAND_MAX 宏值是 2147483647。如果采用其他的编译器,RAND_MAX 的值可能会不一样。

如果编写的宏中包含了一个有操作数的表达式,应该把表达式放在圆括号内,以避免使用该宏时受运算符优先级的影响,进而产生意料之外的结果。例如,RANDOM 宏最外侧的括号可以确保 10.0*RANDOM 表达式产生想要的结果。如果没有这个括号,宏展开后的表达式变成:
10.0 * -1.0 + 2.0*(double)rand() / 2147483647

这个表达式生成的随机数值范围在 [-10.0,-8.0] 之间。

带参数的宏

你可以定义具有形式参数(简称“形参”)的宏。当预处理器展开这类宏时,它先使用调用宏时指定的实际参数(简称“实参”)取代替换文本中对应的形参。带有形参的宏通常也称为类函数宏(function-like macro)。

可以使用下面两种方式定义带有参数的宏:
#define 宏名称( [形参列表] ) 替换文本
#define 宏名称( [形参列表 ,] ... ) 替换文本

“形参列表”是用逗号隔开的多个标识符,它们都作为宏的形参。当使用这类宏时,实参列表中的实参数量必须与宏定义中的形参数量一样多(然而,C99 允许使用“空实参”,下面会进一步解释)。这里的省略号意味着一个或更多的额外形参。

当定义一个宏时,必须确保宏名称与左括号之间没有空白符。如果在名称后面有任何空白,那么命令就会把宏作为没有参数的宏,且从左括号开始采用替换文本。

常见的两个函数 getchar()和 putchar(),它们的宏定义在标准库头文件 stdio.h 中。它们的展开值会随着实现版本不同而有所不同,但不论何种版本,它们的定义总是类似于以下形式:
#define getchar() getc(stdin)
#define putchar(x) putc(x, stdout)

当“调用”一个类函数宏时,预处理器会用调用时的实参取代替换文本中的形参。C99 允许在调用宏的时候,宏的实参列表可以为空。在这种情况下,对应的替换文本中的形参不会被取代;也就是说,替换文本会删除该形参。然而,并非所有的编译器都支持这种“空实参”的做法。

如果调用时的实参也包含宏,在正常情况下会先对它进行展开,然后才把该实参取代替换文本中的形参。对于替换文本中的形参是 # 或 ## 运算符操作数的情况,处理方式会有所不同。下面是类函数宏及其展开结果的一些示例:
#include <stdio.h>             // 包含putchar()的定义
#define DELIMITER ':'
#define SUB(a,b) (a-b)
putchar( DELIMITER );
putchar( str[i] );
int var = SUB( ,10);

如果 putchar(x)定义为 putc(x,stdout),预处理器会按如下方式展开最后三行代码:
putc(':', stdout);
putc(str[i], stdout);
int var = (-10);

如下例所示,替换文本中所有出现的形参,应该使用括号将其包围。这样可以确保无论实参是否是表达式,都能正确地被计算:
#define DISTANCE( x, y ) ((x)>=(y) ? (x)-(y) : (y)-(x))
d = DISTANCE( a, b+0.5 );

该宏调用展开如下所示:
d = ((a)>=(b+0.5) ? (a)-(b+0.5) : (b+0.5)-(a));
如果 x 与 y 没有采用括号,那么扩展后将出现表达式 a-b+0.5,而不是表达式(a)-(b+0.5),这与期望的运算不同。

可选参数

C99 标准允许定义有省略号的宏,省略号必须放在参数列表的后面,以表示可选参数。你可以用可选参数来调用这类宏。

当调用有可选参数的宏时,预处理器会将所有可选参数连同分隔它们的逗号打包在一起作为一个参数。在替换文本中,标识符 __VA_ARGS__ 对应一组前述打包的可选参数。标识符 __VA_ARGS__ 只能用在宏定义时的替换文本中。

__VA_ARGS__ 的行为和其他宏参数一样,唯一不同的是,它会被调用时所用的参数列表中剩下的所有参数取代,而不是仅仅被一个参数取代。下面是一个可选参数宏的示例:
// 假设我们有一个已打开的日志文件,准备采用文件指针fp_log对其进行写入
#define printLog(...) fprintf( fp_log, __VA_ARGS__ )
// 使用宏printLog
printLog( "%s: intVar = %d\n", __func__, intVar );

预处理器把最后一行的宏调用替换成下面的一行代码:
fprintf( fp_log, "%s: intVar = %d\n", __func__, intVar );

预定义的标识符 __func__ 可以在任一函数中使用,该标识符是表示当前函数名的字符串。因此,该示例中的宏调用会将当前函数名和变量 intVar 的内容写入日志文件。

字符串化运算符

一元运算符 # 常称为字符串化运算符(stringify operator 或 stringizing operator),因为它会把宏调用时的实参转换为字符串。# 的操作数必须是宏替换文本中的形参。当形参名称出现在替换文本中,并且具有前缀 # 字符时,预处理器会把与该形参对应的实参放到一对双引号中,形成一个字符串字面量。

实参中的所有字符本身维持不变,但下面几种情况是例外:
(1) 在实参各记号之间如果存在有空白符序列,都会被替换成一个空格符。
(2) 实参中每个双引号(")的前面都会添加一个反斜线(\)。
(3) 实参中字符常量、字符串字面量中的每个反斜线前面,也会添加一个反斜线。但如果该反斜线本身就是通用字符名的一部分,则不会再在其前面添加反斜线。

下面的示例展示了如何使用#运算符,使得宏在调用时的实参可以在替换文本中同时作为字符串和算术表达式:
#define printDBL( exp ) printf( #exp " = %f ", exp )
printDBL( 4 * atan(1.0));           // atan()在math.h中定义

上面的最后一行代码是宏调用,展开形式如下所示:
printf( "4 * atan(1.0)" " = %f ", 4 * atan(1.0));

因为编译器会合并紧邻的字符串字面量,上述代码等效为:
printf( "4 * atan(1.0) = %f ", 4 * atan(1.0));

该语句会生成下列文字并在控制台输出:
4 * atan(1.0) = 3.141593

在下面的示例中,调用宏 showArgs 以演示 # 运算符如何修改宏实参中空白符、双引号,以及反斜线:
#define showArgs(...) puts(#__VA_ARGS__)
showArgs( one\n,       "2\n", three );

预处理器使用下面的文本来替换该宏:
puts("one\n, \"2\\n\", three");

该语句生成下面的输出:
one
, "2\n", three

记号粘贴运算符

运算符是一个二元运算符,可以出现在所有宏的替换文本中。该运算符会把左、右操作数结合在一起,作为一个记号,因此,它常常被称为记号粘贴运算符(token-pasting operator)。如果结果文本中还包含有宏名称,则预处理器会继续进行宏替换。出现在 ## 运算符前后的空白符连同 ## 运算符本身一起被删除。

通常,使用 ## 运算符时,至少有一个操作数是宏的形参。在这种情况下,实参值会先替换形参,然后等记号粘贴完成后,才进行宏展开。如下例所示:
#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);

无论标识符 A 是否定义为一个宏名称,预处理器会先将形参 x 替换成实参 A,然后进行记号粘贴。当这两个步骤做完后,结果如下:
puts( TEXT_A );

现在,因为 TEXT_A 是一个宏名称,后续的宏替换会生成下面的语句:
puts( "Hello, world!" );

如果宏的形参是 ## 运算符的操作数,并且在某次宏调用时,并没有为该形参准备对应的实参,那么预处理使用占位符(placeholder)表示该形参被空字符串取代。把一个占位符和任何记号进行记号粘贴操作的结果还是原来的记号。如果对两个占位符进行记号粘贴操作,则得到一个占位符。

当所有的记号粘贴运算都做完后,预处理器会删除所有剩下的占位符。下面是一个示例,调用宏时传入空的实参:
msg();

这个调用会被展开为如下所示的代码:
puts( TEXT_ );
如果TEXT_不是一个字符串类型的标识符,编译器会生成一个错误信息。

字符串化运算符和记号粘贴运算符并没有固定的运算次序。如果需要采取特定的运算次序,可以将一个宏分解为多个宏。

在宏内使用宏

在替换实参,以及执行完 # 和 ## 运算之后,预处理器会检查操作所得的替换文本,并展开其中包含的所有宏。但是,宏不可以递归地展开:如果预处理器在 A 宏的替换文本中又遇到了 A 宏的名称,或者从嵌套在 A 宏内的 B 宏内又遇到了 A 宏的名称,那么 A 宏的名称就会无法展开。

类似地,即使展开一个宏生成有效的命令,这样的命令也无法执行。然而,预处理器可以处理在完全展开宏后出现 _Pragma 运算符的操作。

下面的示例程序以表格形式输出函数值:
// fn_tbl.c: 以表格形式输出一个函数的值。该程序使用了嵌套的宏
// -------------------------------------------------------------
#include <stdio.h>
#include <math.h>                          // 函数cos()和exp()的原型

#define PI              3.141593
#define STEP    (PI/8)
#define AMPLITUDE       1.0
#define ATTENUATION     0.1                      // 声波传播的衰减指数
#define DF(x)   exp(-ATTENUATION*(x))
#define FUNC(x) (DF(x) * AMPLITUDE * cos(x)) // 震动衰减

// 针对函数输出:
#define STR(s) #s
#define XSTR(s) STR(s)                   // 将宏s展开,然后字符串化
int main()
{
  double x = 0.0;
  printf( "\nFUNC(x) = %s\n", XSTR(FUNC(x)) );          // 输出该函数
  printf("\n %10s %25s\n", "x", STR(y = FUNC(x)) );             // 表格的标题
  printf("-----------------------------------------\n");

  for ( ; x < 2*PI + STEP/2; x += STEP )
    printf( "%15f %20f\n", x, FUNC(x) );
  return 0;
}

该示例输出下面的表格:
FUNC(x) = (exp(-0.1*(x)) * 1.0 * cos(x))
          x                 y = FUNC(x)
-----------------------------------------
              0.000000          1.000000
              0.392699          0.888302
...
          5.890487              0.512619
          6.283186              0.533488

宏的作用域和重新定义

无法再次使用 #define 命令重新定义一个已经被定义为宏的标识符,除非重新定义所使用的替换文本与已经被定义的替换文本完全相同。如果该宏具有形参,重新定义的形参名称也必须与已定义形参名称的一样。

如果想改变一个宏的内容,必须首先使用下面的命令取消现在的定义:
#undef 宏名称

执行上面的命令之后,标识符“宏名称”可以再次在新的宏定义中使用。如果上面指定的标识符并非一个已定义的宏名称,那么预处理器会忽略这个 #undef 命令。

标准库中的多个函数名称也被定义成了宏。如果想直接调用这些函数,而不是调用同名称的宏,可以使用 #undef 命令取消对这些宏的定义。即使准备取消定义的宏是带有参数的,也不需要在 #undef 命令中指定参数列表。如下例所示:
#include <ctype.h>
#undef isdigit          // 移除任何使用该名称的宏定义
/* ... */
if ( isdigit(c) )               // 调用函数isdigit()
/* ... */

当某个宏首次遇到它的 #undef 命令时,它的作用域就会结束。如果没有关于该宏的 #undef 命令,那么它的作用域在该翻译单元结束时终止。

相关文章