C 语言宏定义简介

目录汇总:C 语言零基础入门教程

C 语言零基础入门教程 的第二部分开始使用的宏被称为简单的宏,它们没有参数。预编译器还支持带参数的宏。本文先讨论简单的宏,然后再讨论带参数的宏。在分别讨论它们之后,我们会研究一下二者共同的特性。

一、简单的宏

简单的宏(C 标准中称为对象式宏)的定义有如下格式

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

替换列表是一系列的预处理记号,类似于 C 语言程序的书写规范 中讨论的记号。本文中提及“记号”时均指 “预处理记号”。

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

注意

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

1
2
3
#define N = 100   /*** WRONG ***/
...
int a[N];         /* becomes int a[= 100]; */

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

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

1
2
3
#define N 100;    /*** WRONG ***/
...
int a[N];         /* becomes int a[100;]; *

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

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

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

1
2
3
4
5
6
7
#define STE_LEN 80
#define TRUE    1
#define FALSE   0
#define PI      3.14159
#define CR      '\r'
#define EOS     '\0'
#define MEM_ERR "Error: not enough memory"

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

  • 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法数”,很容易迷惑读者。
  • 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码”的常量会更难于修改,特别是当它们以稍微不同的形式出现时。(例如,如果程序包含一个长度为 100 的数组,它可能会包含一个 0~99 的循环。如果我们只是试图找到程序中出现的所有 100,那么就会漏掉 99。)
  • 可以帮助避免前后不一致或键盘输入错误。假如数值常量 3.14159 在程序中大量出现,它可能会被意外地写成 3.14163.14195

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

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

    1
    2
    
    #define BEGIN {
    #define END   }
    

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

    1
    
    #define LOOP for (;;)
    

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

  • 对类型重命名。在 C 语言 if 语句简介 中,我们通过重命名 int 创建了一个布尔类型:

    1
    
    #define BOOL int
    

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

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

    1
    
    #define DEBUG
    

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

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

二、参数的宏

带参数的宏(也称为函数式宏)的定义有如下格式:

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

其中 $x_1,x_2,\cdots,x_n$ 是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次。

注意

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

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

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

1
2
#define MAX(x,y)   ((x)>(y)?(x):(y))
#define IS_EVEN(n) ((n)%2==0)

(宏定义中的圆括号似乎过多,但本文后面将看到,这样做是有原因的。)现在如果后面的程序中有如下语句:

1
2
i = MAX(j+k, m-n);
if (IS_EVEN(i)) i++;

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

1
2
i = ((j+k)>(m-n)?(j+k):(m-n));
if (((i)%2==0)) i++;

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

下面的宏也类似函数,但更为复杂:

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

这个宏检测字符 c 是否在 'a''z' 之间。如果在的话,这个宏会用 c 的值减去 'a' 再加上 'A',从而计算出 c 所对应的大写字母。如果 c 不在这个范围,就保留原来的 c。[<ctype.h> 头文件中提供了一个类似的函数 toupper,它的可移植性更好。]

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

1
#define getchar()  getc(stdin)

空的参数列表不是必需的,但这样可以使 getchar 更像一个函数。(没错,这就是 <stdio.h> 中的 getchargetchar 经常实现为宏,也经常实现为函数。)

使用带参数的宏替代真正的函数有两个优点。

  • 程序可能会稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等,而调用宏则没有这些运行开销。[注意,C99 的内联函数为我们提供了一种不使用宏而避免这一开销的办法。]
  • 宏更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用 MAX 宏从两个数中选出较大的一个,数的类型可以是 intlongfloatdouble 等。

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

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

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

    下面是预处理后的语句:

    1
    
    n = ((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k))));
    
  • 宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们是否是正确的类型。如果不是,要么将参数转换成正确的类型,要么由编译器产生一条出错消息。预处理器不会检查宏参数的类型,也不会进行类型转换。

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

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

    1
    
    n = MAX(i++, j);
    

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

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

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

注意

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

带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。如果我们已经写烦了语句

1
printf("%d\n", i);

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

1
#define PRINT_INT(n) printf("%d\n", n)

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

1
PRINT_INT(i/j);

转换为

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

三、# 运算符

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

# 运算符将宏的一个参数转换为字面串。它仅允许出现在带参数的宏的替换列表中。(# 运算符所执行的操作可以理解为“串化”(stringization),这个词你在字典里肯定看不到。)

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

1
#define PRINT_INT(n) printf(#n " = %d\n", n)

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

1
PRINT_INT(i/j);

会变为

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

C 语言字符串字面量简介 可知,C 语言中相邻的字面串会被合并。因此上边的语句等价于

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

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

1
i/j = 5

四、## 运算符

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

1
#define MK_ID(n) i##n

MK_ID 被调用时(比如 MK_ID(1)),预处理器首先使用实际参数(这个例子中是 1)替换形式参数 n。接着,预处理器将 i1 合并为一个记号(i1)。下面的声明使用 MK_ID 创建了 3 个标识符:

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

预处理后这一声明变为

1
int i1, i2, i3;

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

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

1
2
3
4
5
#define GENERIC_MAX(type)        \
type type##_max(type x, type y)  \
{                                \
  return x > y ? x : y;          \
}

注意,宏的定义中是如何将 type_max 相连来形成新函数名的。

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

1
GENERIC_MAX(float)

预处理器会将这行代码展开如下:

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

五、宏的通用属性

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

  • 宏的替换列表可以包含对其他宏的调用。例如,我们可以用宏 PI 来定义宏 TWO_PI

    1
    2
    
    #define PI      3.14159
    #define TWO_PI  (2*PI)
    

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

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

    1
    2
    3
    4
    5
    6
    
    #define SIZE 256
    
    int BUFFER_SIZE;
    
    if (BUFFER_SIZE > SIZE)
    puts("Error: SIZE exceeded");
    

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

    1
    2
    3
    4
    
    int BUFFER_SIZE;
    
    if (BUFFER_SIZE > 256)
    puts("Error: SIZE exceeded");
    

    尽管标识符 BUFFER_SIZE 和字符串"Error: SIZE exceeded"都包含 SIZE,但是它们没有被预处理影响。

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

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

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

    1
    
    [#undef指令] #undef 标识符
    

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

    1
    
    #undef N
    

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

六、宏定义中的圆括号

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

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

1
#define TWO_PI (2*3.14159)

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

1
#define SCALE(x) ((x)*10)

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

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

1
2
#define TWO_PI 2*3.14159
  /* 需要给替换列表加圆括号 */

在预处理时,语句

1
conversion_factor = 360/TWO_PI;

变为

1
conversion_factor = 360/2*3.14159;

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

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

1
#define SCALE(x) (x*10)   /* 需要给x添加括号 */

在预处理过程中,语句

1
j = SCALE(i+1);

变为

1
j = (i+1*10);

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

1
j = i+10;

当然,我们希望的是

1
j = (i+1)*10;

注意

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

七、创建较长的宏

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

1
#define ECHO(s) (gets(s), puts(s))

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

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

如果不想在 ECHO 的定义中使用逗号运算符,我们还可以将 gets 函数和 puts 函数的调用放在花括号中形成复合语句:

1
#define ECHO(s)  { gets(s);  puts(s);  }

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

1
2
3
4
if (echo_flag)
  ECHO(str);
else
  gets(str);

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

1
2
3
4
if (echo_flag)
  { gets(str);  puts(str);  };
else
  gets(str);

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

1
2
if (echo_flag)
  { gets(str);  puts(str);  }

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

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

1
do { ... } while (0)

注意,这个 do 语句是不完整的——后面还缺一个分号。为了看到这个技巧(嗯,应该说是技术)的实际作用,将它用于 ECHO 宏中:

1
2
3
4
5
#define ECHO(s)      \
        do {         \
          gets(s);   \
          puts(s);   \
        } while (0)

当使用 ECHO 宏时,一定要加分号以使 do 语句完整:

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

八、预定义宏

C 语言有一些预定义宏,每个宏表示一个整型常量或字面串。如表 1 所示,这些宏提供了当前编译或编译器本身的信息。

表 1  预定义宏

名字 描述
__LINE__ 当前宏所在行的行号
__FILE__ 当前文件的名字
__DATE__ 编译的日期(格式"mm dd yyyy")
__TIME__ 编译的时间(格式"hh:mm:ss")
__STDC__ 如果编译器符合 C 标准(C89 或 C99),那么值为 1

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

1
2
printf("Wacky Windows (c) 2010 Wacky Software, Inc.\n");
printf("Compiled on %s at %s\n", __DATE__, __TIME__);

每次程序开始执行时,程序都会显示如下的两行内容:

1
2
Wacky Windows (c) 2010 Wacky Software, Inc.
Compiled on Dec 23 2010 at 22:18:48

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

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

1
2
3
4
#define CHECK_ZERO(divisor)  \
  if (divisor == 0) \
    printf("*** Attempt to divide by zero on line %d  "  \
           "of file %s  ***\n", __LINE__, __FILE__)

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

1
2
CHECK_ZERO(j);
k = i / j;

如果 j0,会显示出如下形式的信息:

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

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

如果编译器符合 C 标准(C89 或 C99),__STDC__ 宏存在且值为 1。通过让预处理器测试这个宏,程序可以在早于 C89 标准的编译器下编译通过(C 语言条件编译简介 会给出一个例子)。

九、C99 中新增的预定义宏

C99 中新增了几个预定义宏(见表 2)。

表 2   C99 中新增的预定义宏

名字 描述
__STDC_HOSTED__ 如果是托管式实现,则值为 1;如果是独立式实现,则值为 0
__STDC_VERSION__ 支持的 C 标准版本
__STDC_IEC_559__ 如果支持 IEC 60559 浮点算术运算,则值为 1
__STDC_IEC_559_COMPLEX__ 如果支持 IEC 60559 复数算术运算,则值为 1
__STDC_ISO_10646__ 被定义为 yyyymmL 形式的整型常量,意味着可以用 wchar_t 类型来存储 ISO 10646 标准所定义的,以及在指定年月所修订和补充的 Unicode 字符

① 有条件定义。

要了解 __STDC_HOSTED__ 的意义需要介绍些新的名词。C 的实现(implementation)包括编译器和执行 C 程序所需要的其他软件。C99 将实现分为两种:托管式(hosted)和独立式(freestanding)。托管式实现(hosted implementation)能够接受任何符合 C99 标准的程序,而独立式实现(freestanding implementation)除了几个最基本的以外,不一定要能够编译使用复数类型或标准头的程序。(特别是,独立式实现不需要支持 <stdio.h> 头。)如果编译器是托管式实现,则 __STDC_HOSTED__ 宏代表常数 1,否则值为 0。

__STDC_VERSION__ 宏为我们提供了一种查看编译器所识别出的 C 标准版本的方法。这个宏第一次出现在 C89 标准的 Amendment 1 中,该文档指明宏的值为长整型常量 199409L(代表修订的年月)。如果编译器符合 C99 标准,其值为 199901L。对于标准的每一个后续版本(以及每一次后续修订),宏的值都有所变化。

C99 编译器可能(也可能没有)另外定义以下 3 种宏。仅当编译器满足特定条件时才会定义相应的宏。

  • 如果编译器根据 IEC 60559 标准[IEEE 754 标准(C 语言浮点类型简介)的别名]执行浮点算术运算,则定义 __STDC_IEC_559__ 宏,且其值为 1。
  • 如果编译器根据 IEC 60559 标准执行复数算术运算,则定义 __STDC_IEC_559_COMPLEX__ 宏,且其值为 1。
  • __STDC_ISO__10646__ 定义为 yyyymmL 格式(如 199712L)的整型常量,前提是 wchar_t 类型的值由 ISO/IEC 10646 标准(包括指定年月的修订版本)中的码值表示。

十、空的宏参数

C99 允许宏调用中的任意或所有参数为空。当然这样的调用需要有和一般调用一样多的逗号(这样容易看出哪些参数被省略了)。

在大多数情况下,实际参数为空的效果是显而易见的。如果替换列表中出现相应的形式参数名,那么只要在替换列表中不出现实际参数即可,不需要替换。例如:

1
#define ADD(x,y) (x+y)

经过预处理之后,语句

1
i = ADD(j,k);

变成

1
i = (j+k);

而赋值语句

1
i = ADD(,k);

则变为

1
i = (+k);

当空参数是 ### 运算符的操作数时,其用法有特殊规定。如果空的实际参数被 # 运算符“串化”,则结果为 ""(空字符串):

1
2
3
#define MK_STR(x) #x
...
char empty_string[] = MK_STR();

预处理之后,上面的声明变成

1
char empty_string[] = "";

如果 ## 运算符之后的一个实际参数为空,它将被不可见的“位置标记”记号代替。把原始的记号与位置标记记号相连接,得到的还是原始的记号(位置标记记号消失了)。如果连接两个位置标记记号,得到的是一个位置标记记号。宏扩展完成后,位置标记记号从程序中消失。考虑下面的例子:

1
2
3
#define JOIN(x,y,z) x##y##z
...
int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);

预处理之后,声明变成

1
int abc, ab, ac, c;

漏掉的参数由位置标记记号代替,这些记号在与非空参数相连接之后消失。JOIN 宏的 3 个参数可以同时为空,这样得到的结果为空。

十一、参数个数可变的宏

在 C89 中,如果宏有参数,那么参数的个数是固定的。在 C99 中,这个条件被适当放宽了,允许宏具有可变长度的参数列表。这个特性对于函数来说早就有了,所以应用于宏也不足为奇。

宏具有可变参数个数的主要原因是,它可以将参数传递给具有可变参数个数的函数,如 printfscanf。下面给出几个例子:

1
2
3
#define TEST(condition, ...) ((condition)? \
  printf("Passed test: %s\n", #condition): \
  printf(__VA_ARGS__))

...记号(省略号)出现在宏参数列表的最后,前面是普通参数。__VA_ARGS__ 是一个专用的标识符,只能出现在具有可变参数个数的宏的替换列表中,代表所有与省略号相对应的参数。(至少有一个与省略号相对应的参数,但该参数可以为空。)宏 TEST 至少要有两个参数,第一个参数匹配 condition,剩下的参数匹配省略号。

下面这个例子说明了 TEST 的使用方法:

1
2
TEST(voltage <= max_voltage,
     "Voltage %d exceeds %d\n", voltage, max_voltage);

预处理器将产生如下的输出(重排格式以增强可读性):

1
2
3
((voltage <= max_voltage)?
  printf("Passed test: %s\n", "voltage <= max_voltage"):
  printf("Voltage %d exceeds %d\n", voltage, max_voltage));

如果 voltage 不大于 max_voltage,程序执行时将显示如下消息:

1
Passed test: voltage <= max_voltage

否则,将分别显示 voltagemax_voltage 的值:

1
voltage 125 exceeds 120

十二、__func__ 标识符

C99 的另一个新特性是 __func__ 标识符。__func__ 与预处理器无关,所以实际上与本文内容不相关。但是,与许多预处理特性一样,它也有助于调试,所以在这里一并讨论。

每一个函数都可以访问 __func__ 标识符,它的行为很像一个存储当前正在执行的函数的名字的字符串变量。其作用相当于在函数体的一开始包含如下声明:

1
static const char __func__[] = "function-name";

其中 function-name 是函数名。这个标识符的存在使得我们可以写出如下的调试宏:

1
2
#define FUNCTION_CALLED() printf("%s called\n", __func__);
#define FUNCTION_RETURNS() printf("%s returns\n", __func__);

对这些宏的调用可以放在函数体中,以跟踪函数的调用:

1
2
3
4
5
6
void f(void)
{
  FUNCTION_CALLED();    /* displays "f called" */
  ...
  FUNCTION_RETURNS();   /* displays "f returns" */
}

__func__ 的另一个用法:作为参数传递给函数,让函数知道调用它的函数的名字。

请参阅

(完)

comments powered by Disqus