在C语言中,可以采用命令
#define
来定义宏。该命令允许把一个名称指定为任何文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。
宏定义的一般形式
#define 宏名(参数) 字符串
一般宏名采用大写,有利于将宏与一般变量区分。
没有参数的宏
没有参数的宏就是简单的字符串替换,在预处理阶段,源代码中每个出现宏名的地方,都用字符串简单替换就行。出现在字符串字面量中的宏名称不会被展开,因为整个字符串字面量算作一个预处理器记号。
#define RANDOM (-1.0 + 2.0*(double)rand() / RAND_MAX)
int main() {
double x = RANDOM;
}
上面这段代码等价于
int main() {
double x = (-1.0 + 2.0*(double)rand() / RAND_MAX);
}
这并不是一个函数调用,只是简单的文本替换而已。
带参数的宏
可以定义具有形式参数的宏。当预处理器展开这类宏时,它先使用调用宏时指定的实际参数取代替换文本中对应的形参。带有形参的宏通常也称为类函数宏(function-like macro)
#define 宏名称( [形参列表] ) 替换文本
#define 宏名称( [形参列表 ,] ... ) 替换文本
**宏名称和参数列表的括号之间不能有空格,否则将会视其为无参数的宏,且从左括号开始替换宏。**括号内形参之间可以有空格,形参与括号之间也可以有空格。使用带有形参的宏时,实参列表中的实参数量必须与宏定义中的形参数量一样。
替换文本中出现的所有形参,都应该使用括号将其包围。这样可以确保无论实参是否是表达式,都能正确地被计算:
#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)。把实参转为字符串,#的操作数必须是宏中的形参。当形参名称出现在替换文本中,并且具有前缀 # 字符时,预处理器会把与该形参对应的实参放到一对双引号中,形成一个字符串字面量。
#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));
#define showArgs(...) puts(#__VA_ARGS__)
showArgs( one\n, "2\n", three );
// 等价于
puts("one\n, \"2\\n\", three");
记号粘贴运算符
##:记号粘贴运算符(token-pasting operator)。二元运算符,出现在宏替换文本中,该运算符会把左、右操作数结合在一起。##
前后的空白字符会被删除,如果结果文本中,仍然有宏名,那么继续进行宏替换。
如果宏的形参是 ## 运算符的操作数,并且在某次宏调用时,并没有为该形参准备对应的实参,那么预处理使用占位符(placeholder)表示该形参被空字符串取代。把一个占位符和任何记号进行记号粘贴操作的结果还是原来的记号。如果对两个占位符进行记号粘贴操作,则得到一个占位符。
在宏内使用宏
宏无法递归,自递归和互递归都不行。
宏的作用域和重新定义
你无法再次使用 #define 命令重新定义一个已经被定义为宏的标识符,除非重新定义所使用的替换文本与已经被定义的替换文本完全相同。如果该宏具有形参,重新定义的形参名称也必须与已定义形参名称的一样。
如果想改变一个宏的内容,必须首先使用下面的命令取消现在的定义:
#undef 宏名称
执行上面的命令之后,标识符“宏名称”可以再次在新的宏定义中使用。如果上面指定的标识符并非一个已定义的宏名称,那么预处理器会忽略这个 #undef 命令。
实例
宏学习主要有南大的计算机系统基础PA驱动,当在做PA1时,由于以前只会简单的如#define N 100
这种宏,看不懂PA中复杂的宏,所以学习一下宏的基础知识。
// macro concatenation
#define concat_temp(x, y) x ## y
// macro testing
// See https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
// define placeholders for some property
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
// define some selection functions based on the properties of BOOLEAN macro
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
我们通过自顶向下的方式来分析一下宏MUXDEF
。MUXDEF
是个选择函数,当macro
被定义时,返回X
,当未被定义时,返回Y
。
MUXDEF
的替换文本有两一个宏MUX_MACRO_PROPERTY
,又调用MUX_WITH_COMMA
这个宏,但是在调用MUX_WITH_COMMA
之前,先会替换concat
这个宏。宏concat
的作用很明确,其中有记号粘贴运算符**##**,将contact
中的形参X
和Y
简单的拼接在一起,在这里将会返回__P_DEF_macro
(macro将会是传给MUXDEF
的第一个形参来替代)。
最关键的地方在于#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
,可以看到,contain_comma
是concat
拼接后的结果,如果拼接后的结果是一个宏,比如__P_DEF_0
,那么CHOOSE2nd(contain_comma a, b)
将实际变成CHOOSE2nd(X, a, b)
,那么选择函数选择第二个参数返回,那么将会返回a
,最终返回MUXDEF
的X
,这展示了占位符的作用,是宏定义的一个技巧。如果拼接后不是一个宏,那么传给CHOOSE2nd
的参数将只有两个,那么将返回b
,最终返回MUXDEF
的Y
。
下面用实际的例子来说明。
-
当宏
ISDEF
没有被定义。MUXDEF(ISDEF, "ON", "OFF");
concat
将返回__P_DEF_ISDEF
,显然__P_DEF_ISDEF
没有被定义,那么上述宏被展开为"OFF"
。 -
当宏
ISDEF
被定义。#define ISDEF 1 MUXDEF(ISDEF, "ON", "OFF");
concat
将返回__P_DEF_1
,显然__P_DEF_1
被定义,那么上述宏被展开为"ON"
。
参考
C语言宏的定义和宏的使用方法(主要参考)