GVKun编程网logo

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

14

对于想了解C语言宏的定义和宏的使用方法的读者,本文将提供新的信息,我们将详细介绍#define,并且为您提供关于C语言宏的特殊用法和几个坑(转)、C++,vs中pow和宏的使用、C++中内联和宏的区别

对于想了解C语言宏的定义和宏的使用方法的读者,本文将提供新的信息,我们将详细介绍#define,并且为您提供关于C 语言宏的特殊用法和几个坑 (转)、C++,vs中pow和宏的使用、C++中内联和宏的区别、C/C++宏的使用总结的有价值信息。

本文目录一览:

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

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

在 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,(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",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,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 命令,那么它的作用域在该翻译单元结束时终止。

C 语言宏的特殊用法和几个坑 (转)

C 语言宏的特殊用法和几个坑 (转)

总结一下 C 语言中宏的一些特殊用法和几个容易踩的坑。由于本文主要参考 GCC 文档,某些细节(如宏参数中的空格是否处理之类)在别的编译器可能有细微差别,请参考相应文档。

宏基础

宏仅仅是在 C 预处理阶段的一种文本替换工具,编译完之后对二进制代码不可见。基本用法如下:

1. 标示符别名

#define BUFFER_SIZE 1024

预处理阶段,foo = (char *) malloc (BUFFER_SIZE); 会被替换成 foo = (char *) malloc (1024);

宏体换行需要在行末加反斜杠 \

#define NUMBERS 1, \
                2, \
                3

预处理阶段 int x[] = { NUMBERS }; 会被扩展成 int x[] = { 1, 2, 3 };

2. 宏函数

宏名之后带括号的宏被认为是宏函数。用法和普通函数一样,只不过在预处理阶段,宏函数会被展开。优点是没有普通函数保存寄存器和参数传递的开销,展开后的代码有利于 CPU cache 的利用和指令预测,速度快。缺点是可执行代码体积大。

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

y = min(1, 2); 会被扩展成 y = ((1) < (2) ? (1) : (2));


宏特殊用法

1. 字符串化 (Stringification)

在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:

#define WARN_IF(EXP) \
     do { if (EXP) \
             fprintf (stderr, "Warning: " #EXP "\n"); } \
     while (0)

WARN_IF (x == 0); 会被扩展成:

do { if (x == 0)
    fprintf (stderr, "Warning: " "x == 0" "\n"); }
while (0);

这种用法可以用在 assert 中,如果断言失败,可以将失败的语句输出到反馈信息中

2. 连接 (Concatenation)

在宏体中,如果宏体所在标示符中有##,那么在宏体扩展的时候,宏参数会被直接替换到标示符中。如:

#define COMMAND(NAME)  { #NAME, NAME ## _command }

struct command
{
    char *name;
    void (*function) (void);
};

在宏扩展的时候

struct command commands[] =
{
    COMMAND (quit),
    COMMAND (help),
    ...
};

会被扩展成:

struct command commands[] =
{
    { "quit", quit_command },
    { "help", help_command },
    ...
};

这样就节省了大量时间,提高效率。


几个坑

1. 语法问题

由于是纯文本替换,C 预处理器不对宏体做任何语法检查,像缺个括号、少个分号神马的预处理器是不管的。这里要格外小心,由此可能引出各种奇葩的问题,一下还很难找到根源。

2. 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

#define MULTIPLY(x, y) x * y

MULTIPLY(1, 2) 没问题,会正常展开成 1 * 2。有问题的是这种表达式 MULTIPLY(1+2, 3),展开后成了 1+2 * 3,显然优先级错了。

在宏体中,给引用的参数加个括号就能避免这问题。

#define MULTIPLY(x, y) (x) * (y)

MULTIPLY(1+2, 3) 就会被展开成 (1+2) * (3),优先级正常了。

其实这个问题和下面要说到的某些问题都属于由于纯文本替换而导致的语义破坏问题,要格外小心。

3. 分号吞噬问题

有如下宏定义:

#define SKIP_SPACES(p, limit)  \
     { char *lim = (limit);         \
       while (p < lim) {            \
         if (*p++ != '' '') {         \
           p--; break; }}}

假设有如下一段代码:

if (*p != 0)
   SKIP_SPACES (p, lim);
else ...

一编译,GCC 报 error: ‘else’ without a previous ‘if’。原来这个看似是一个函数的宏被展开后是一段大括号括起来的代码块,加上分号之后这个 if 逻辑块就结束了,所以编译器发现这个 else 没有对应的 if。

这个问题一般用 do ... while(0) 的形式来解决:

#define SKIP_SPACES(p, limit)     \
     do { char *lim = (limit);         \
          while (p < lim) {            \
            if (*p++ != '' '') {         \
              p--; break; }}}          \
     while (0)

展开后就成了

if (*p != 0)
    do ... while(0);
else ...

这样就消除了分号吞噬问题。

这个技巧在 Linux 内核源码里很常见,比如这个置位宏#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)(位于 arch/mips/include/asm/mach-pnx833x/gpio.h)

4. 宏参数重复调用

有如下宏定义:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

当有如下调用时 next = min (x + y, foo (z));,宏体被展开成 next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));,可以看到,foo(z) 被重复调用了两次,做了重复计算。更严重的是,如果 foo 是不可重入的 (foo 内修改了全局或静态变量),程序会产生逻辑错误。

所以,尽量不要在宏参数中传入函数调用。

5. 对自身的递归引用

有如下宏定义:

#define foo (4 + foo)

按前面的理解,(4 + foo) 会展开成 (4 + (4 + foo)),然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo 只会展开成 (4 + foo),而展开之后 foo 的含义就要根据上下文来确定了。

对于以下的交叉引用,宏体也只会展开一次。

#define x (4 + y)
#define y (2 * x)

x 展开成 (4 + y) -> (4 + (2 * x))y 展开成 (2 * x) -> (2 * (4 + y))

注意,这是极不推荐的写法,程序可读性极差。

6. 宏参数预处理

宏参数中若包含另外的宏,那么宏参数在被代入到宏体之前会做一次完全的展开,除非宏体中含有###

有如下宏定义:

#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZE
  • AFTERX(BUFSIZE) 会被展开成 X_BUFSIZE。因为宏体中含有##,宏参数直接代入宏体。
  • XAFTERX(BUFSIZE) 会被展开成 X_1024。因为 XAFTERX(x) 的宏体是 AFTERX(x),并没有###,所以 BUFSIZE 在代入前会被完全展开成 1024,然后才代入宏体,变成 X_1024

-EOF-

C++,vs中pow和宏的使用

C++,vs中pow和宏的使用

#include <iostream>
  • using namespace std;
  • //#define Mul ++x*y++ //错的,必须要是Mul(x,y)
  • #define Mul(x,y) ++x*y++
  • int main(){
  • //cout<<pow(10,2);错误
  • cout<<pow((float)10,2)<<endl;
  • cout<<pow((double)10,2)<<endl;
  • int a=2,b=4,c=8;
  • int d = Mul(a+b,b+c); //++2+4*4+8++ = 3+16+8 = 27
  • cout <<d<<endl;//27
  • return 0;
  • }




  • C++中内联和宏的区别

    C++中内联和宏的区别

    1. 内联

    内联函数是由 inline 关键字定义的普通函数。内联函数是由编译器扩展的短函数。并且它的参数只被评估一次。内联函数是在类中不使用 inline 关键字的情况下自动成为内联函数的短长度函数。

    内联函数的语法:

    inline return_type function_name ( parameters )
    { 
     // inline function code
    }
    

    内联函数示例:

    #include <iostream>
    using namespace std;
    
    // Inline function
    inline int Maximum(int a, int b)
    {
        return (a > b) ? a : b;
    }
    
    // Main function for the program
    int main()
    {
        cout << Max (100, 1000): << Maximum(100, 1000) << endl;
        cout << Max (20, 0):  << Maximum(20, 0) << endl;
    
        return 0;
    }
    

    运行结果:

    Max (100, 1000): 1000
    Max (20, 0): 20
    

    2. 宏

    它也称为预处理器指令。宏由#define 关键字定义。在程序编译之前,只要预处理器检测到宏,预处理器就会检查程序,然后预处理器用宏定义替换宏。

    宏的语法:

    #define MACRO_NAME Macro_deFinition
    

    宏示例:

    #include <iostream>
    using namespace std;
    
    // macro with parameter
    #define MAXIMUM(a, b) (a > b) ? a : b
    
    // Main function for the program
    int main()
    {
        cout << Max (100, 1000):;
        int k = MAXIMUM(100, 1000);
        cout << k << endl;
    
        cout << Max (20, 0):;
        int k1 = MAXIMUM(20, 0);
        cout << k1;
    
        return 0;
    }
    

    运行结果:

    Max (100, 1000):1000
    Max (20, 0):20
    

    C++ 中内联和宏的区别:

    编号 内联
    1 内联函数由 inline 关键字定义。 宏是由#define 关键字定义的。
    2 通过内联函数,可以访问类的数据成员。 宏不能访问类的数据成员。
    3 内联函数的情况下,可以方便的调试程序。 在宏的情况下,程序不容易调试。
    4 在内联的情况下,参数只计算一次。 在宏的情况下,每次在程序中使用宏时都会评估参数。
    5 在 C++ 中,内联可以定义在类内或类外。 宏始终是在程序开始时定义的。
    6 在 C++ 中,在类内部,短长度函数自动成为内联函数。 宏是专门定义的。
    7 内联不像宏那样广泛使用。 宏被广泛使用。
    8 内联不用于竞争性编程。 宏在竞争性编程中非常有用。
    9 内联函数以大括号结尾。虽然宏没有由任何符号终止,但它由新行终止。

    C/C++宏的使用总结

    C/C++宏的使用总结

    C/C++宏的使用总结

    我们常用的宏替换主要有这么几个类型。
     

    1、宏常量


    在ACM等算法竞赛中,经常会把数组的最大下标通过宏定义的方法给出,以方便调试,例如:
    #define MAX 1000

    int array[MAX][MAX]
    ......

    for(int i = 0; i < MAX; i++)
    ......

    将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著作《Effective C++》中,这种做法却并不提倡,书中更加推荐以const常量来代替宏常量。因为在进行词法分析时,宏的引用已经被其实际内容替换,因此宏名不会出现在符号表中。所以一旦出错,看到的将是一个无意义的数字,比如上文中的1000,而不是一个有意义的名称,如上文中的MAX。而const在符号表中会有自己的位置,因此出错时可以看到更加有意义的错误提示。

    2、用于条件编译标识的宏


    #define常与#ifdef/#ifndef/defined指令配合使用,用于条件编译。
    #ifndef _HEADER_INC_
    #define _HEADER_INC_
    ……
    ……
    #endif
        这种宏标记在头文件中十分常见,用于防止头文件被反复包含。应该养成习惯在每个头文件中都添加这种标记。
        还有一种用于条件编译的用法
    #ifdef DEBUG
    printf("{“}Debug information\n");
    #endif
    通过DEBUG宏,我们可以在代码调试的过程中输出辅助调试的信息。当DEBUG宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果。
     
    在条件编译时建议使用#if defined和#if !defined来代替使用#ifdef/#ifndef,因为前者更方便处理多分支的情况与较复杂条件表达式的情况。#ifdef/#ifndef只能处理两个分支:#ifdef/#ifndef,#else,#endfi;#if defined和#if !defined可以处理多分支的情况:#if defined/#if !defined, #elif defined, #else, #endif。#ifdef只能判断是否定义,但是#if defined可以判断复杂的表达式的值是否为真。
    #if defined(OS_HPUX)&&(defined(HPUX_11_11)|| defined(HPUX_11_23) 
    // for HP-UX 11.11 and 11.23 
    #elif defined(OS_HPUX) && defined(HPUX_11_31 
    // for HP-UX 11.31 
    #elif defined(OS_AIX) 
    // for AIX 
    #else 
    … 
    #endif

    条件编译时,如果一个文件中太多条件编译的代码,有些编辑器的智能感知可能都不能很好地解析,还是保持代码越简单越好。

    对于函数级别的条件编译主要有两种实现方式:
    (1) 同一个函数声明,同一个函数定义,函数体内使用条件编译代码。这种方式有个问题,如果条件编译代码太多,会导致这个函数体很长,不利于阅读与维护;有一个优点是,有利于编辑器的智能感知,因为这样解析函数名比较方便,但随着编辑器功能的完善,这方面的差别就不明显了。
    (2) 根据编译条件,将编译条件相同的代码放到单独的文件中,这些文件在顶层文件中使用条件编译指令来引用。这种方式最大的优点就是不同平台的程序由不同的源文件来实现,很便于多人分工合作,对于某一部分代码由一个人实现并测试完成后直接把源文件复制过来就可以了,进行低层次的单元测试非常方便;它的缺点就是增加了目录中的文件数量。

    3、宏函数

    宏函数的语法有以下特点:

        (1)、如果需要换行,则行末要加反斜杠“\”表示换行。宏函数体的最后一行不加反斜杠。
        (2)、假设有参数ARGU,值为argu,则所有的ARGU被直接替换为argu,#ARGU被认为是字符串,会被替换成"argu"(带引号)。
        (3)、由于宏函数是直接替换,所有一般情况下对宏函数的调用时行末不需要加分号。
     

    宏函数的作用:

        (1)、避免函数调用,提高程序效率
    常用的就是最大值与最小值的判断函数,由于函数内容并不多,如果定义为函数在调用比较频繁的场合会明显降低程序的效率,其实宏是用空间效率换取了时间效率。如取两个值的最大值: 
    #define MAX(a,b) ((a)<(b) ? (b) : (a))
    定义为函数: 
    inline int Max(int a, int b)
    {
     return a<b ? b : a;
    }
    定义为模板: 
    template <typename T> 
    inline T TMax(T a, T b)
    {
     return a < b ? b : a ;
    }
    使用宏函数的优点有两个:
    (1)适用于任何实现了operator<的类型,包括自定义类型;
    (2)效率最高。虽然使用inline提示符也将函数或模板定义为内联的,但这只是一种提示而已,到底编译器有没有优化还依赖于编译器的实现,而使用宏函数则是完全由代码本身控制。 
    需要注意的是,由于宏的本质是直接的文本替换,所以在宏函数的“函数体”内都要把参数使用括号括起来,防止参数是表达式时造成语法错误或结果错误,如:
    #define MIN( a, b) b < a ? b : a 
    #define SUM( a, b) a + b 
    cout<<MIN(3,5)<<endl; // 语法错误:cout<<b < a ? b : a<<endl; 
    int c = SUM(a,b)*2;  // c的期望值:16,实际值:13

        (2)、引用编译期数据
    上述的这些作用虽然使用宏函数可以取得更好的性能,但如果从功能上讲完全可以不使用宏函数,而使用模板函数或普通函数实现,但还有些时候只能通过宏实现。例如,程序中在执行某些操作时可能会失败,此时要打印出失败的代码位置,只能使用宏实现。 
    #define SHOW_CODE_LOCATION() cout<<__FILE__<<'':''<<__LINE__<<''\n'' 
    if( 0 != rename("oldFileName", "newFileName") )

     cout<<"failed to move file"<<endl; 
     SHOW_CODE_LOCATION(); 
    }
    虽然宏是简单的替换,所以在调用宏函数SHOW_CODE_LOCATION时,分号可以直接写到定义里,也可以写到调用处,但最好还是写到调用处,看起来更像是调用了函数,否则看着代码不伦不类,如:
    #define SHOW_CODE_LOCATION() cout<<__FILE__<<'':''<<__LINE__<<''\n'' 
    if( 0 != rename("oldFileName", "newFileName") )

     cout<<"failed to move file"<<endl; 
     SHOW_CODE_LOCATION()
    }

        (3)、do-while的妙用
    do-while循环控制语句的特点就是循环体内的语句至少会被执行一次,如果while(…)内的条件始终为0时,循环体内的语句就会被执行且只被执行一次,这样的执行效果与直接使用循环体内的代码相同,但这们会得到更多的益处。 
    #define SWAP_INT(a, b) do
    {\
     int tmp = a; \
     a = b; \
     b = tmp; \
    }while(0)

    int main( void ) 

     int x = 3, y = 4;
     if( x > y )
     {
      SWAP_INT(x, y);
     }
     return 0;
    }
    通过do-while代码块的宏定义我们不仅可以把SWAP_INT像函数一样用,而且还有优点: 
    (1)、在宏定义中可以使用局部变量; 
    (2)、在宏定义中可以包含多个语句,但可以当作一条语句使用,如代码中的if分支语句,如果没有do-while把多条语句组织成一个代码块,则程序的运行结果就不正确,甚至不能编译。 
    其实我们定义的SWAP_INT(a, b)相当于定义了引用参数或指针参数的函数,因为它可以改变实参的值。在C++0X中有了decltype关键词,这种优势就更显示了,因为在宏中使用了局部变量必须确定变量的类型,所以这个宏只能用于交换int型的变量值,如果换作其它类型则还必须定义新的宏,如SWAP_FLOAT、SWAP_CHAR等,而通过decltype,我们就可以定义一个万能的宏。 
    #include <iostream> 
    using namespace std; 
    #define SWAP(a, b) do
    { \
     decltype(a) tmp = a; \
     a = b; \
     b = tmp; \
    }while(0)

    int main( void ) 

     int a = 1, b = 2; 
     float f1 = 1.1f, f2 = 2.2f; 
     SWAP(a, b); 
     SWAP(f1,f2); 
     return 0; 
    }
    通过宏实现的SWAP“函数”要比使用指针参数效率还要高,因为它连指针参数都不用传递而是使用直接代码,对于一些效率要求比较明显的场合,宏还是首选。

    4、取消宏定义


    #undef指令用于取消前面用#define定义的宏,取消后就可以重新定义宏。该指令用的并不多,因为过多的#undef会使代码维护起来非常困难,一般也只用于配置文件中,用来清除一些#define的开关,保证宏定义的唯一性。
    // config.h 
    #undef HAS_OPEN_SSL 
    #undef HAS_ZLIB 
    #if defined(HAS_OPEN_SSL) 
    … 
    #endif 
    #if defined(HAS_ZLIB) 
    … 
    #endif
    将对该头文件的引用放到所有代码文件的第一行,就可以保证HAS_OPEN_SSL没有被定义,即使是在编译选项里定义过一宏,也会被#undef指令取消,这样使得config.h就是唯一一处放置条件编译开关的地方,更有利于维护。

    5、注意事项


    1)、普通宏定义
    (1)宏名一般用大写
    (2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。
    (3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
    (4)宏定义末尾不加分号;
    (5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
    (6)可以用#undef命令终止宏定义的作用域
    (7)宏定义可以嵌套
    (8)字符串""中永远不包含宏
    (9)宏定义不分配内存,变量定义分配内存。
    2)、带参宏定义
    (1)实参如果是表达式容易出问题
    (2)宏名和参数的括号间不能有空格
    (3)宏替换只作替换,不做计算,不做表达式求解
    (4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
    (5)宏的哑实结合不存在类型,也没有类型转换。
    (6)函数只有一个返回值,利用宏则可以设法得到多个值
    (7)宏展开使源程序变长,函数调用不会
    (8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)

    6、关于#和##


    在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。比如下面代码中的宏:
    #define WARN_IF(EXP)    \
        do{ if (EXP)    \
                fprintf(stderr, "Warning: " #EXP "\n"); }   \
        while(0)
    那么实际使用中会出现下面所示的替换过程:
    WARN_IF (divider == 0);

     被替换为

    do {
        if (divider == 0)
      fprintf(stderr, "Warning" "divider == 0" "\n");
    } while(0);
    这样每次divider(除数)为0的时候便会在标准错误流上输出一个提示信息。
    而##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。比如你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:
    struct command
    {
     char * name;
     void (*function) (void);
    };

    #define COMMAND(NAME) { NAME, NAME ## _command }

    // 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:

    struct command commands[] = {
     COMMAND(quit),
     COMMAND(help),
     ...
    }
    COMMAND宏在这里充当一个代码生成器的作用,这样可以在一定程度上减少代码密度,间接地也可以减少不留心所造成的错误。我们还可以n个##符号连接 n+1个Token,这个特性也是#符号所不具备的。比如:
    #define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d

    typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);
    // 这里这个语句将展开为:
    //  typedef struct _record_type name_company_position_salary;

    7、关于...的使用

    在C宏中称为Variadic Macro,也就是变参宏。比如:
    #define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)

     // 或者

    #define myprintf(templt,args...) fprintf(stderr,templt,args)
    第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要求我们必须写成:
    myprintf(templt,);
    的形式。这时的替换过程为:
    myprintf("Error!\n",);

     替换为:
     
    fprintf(stderr,"Error!\n",);
    这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:
    myprintf(templt);
    而它将会被通过替换变成:
    fprintf(stderr,"Error!\n",);
    很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU CPP都支持下面的宏定义方式:
    #define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
    这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
    myprintf(templt);

     被转化为:

    fprintf(stderr,templt);
    这样如果templt合法,将不会产生编译错误。 这里列出了一些宏使用中容易出错的地方,以及合适的使用方式。

    今天关于C语言宏的定义和宏的使用方法#define的介绍到此结束,谢谢您的阅读,有关C 语言宏的特殊用法和几个坑 (转)、C++,vs中pow和宏的使用、C++中内联和宏的区别、C/C++宏的使用总结等更多相关知识的信息可以在本站进行查询。

    本文标签:

    上一篇GNU编译器套件(gnu编译器下载)

    下一篇C语言#include的用法(c语言include的用法详解)