C语言宏定义
宏定义
macro definition
是C/C++
中的一种预处理指令
可以在编译之前替换源代码中的一些文本。
1 概念
宏定义是一种预处理指令,用于在源代码中定义一些常量、函数或代码片段的缩写形式。
通过宏定义,可以将一段代码片段或者常量值与一个标识符相关联,然后在代码中使用该标识符来代替相应的代码或值。
1.1 无参宏
通常,宏定义使用#define
关键字来创建,其基本形式是:
1 |
宏名称:标识符,用于代表宏定义的名称。
宏取代文本:宏定义的内容,可以是常量、表达式、代码片段等。
比如,定义一个常量宏:
1 |
然后,便可以在代码中使用PI
来代替3.1415926
。
又如,使用宏替换代码片段:
1 |
在代码中使用HELLO
,相当于使用printf("Hello, World!\n")
。
1.2 注意事项
1.2.1 书写规范
宏定义的标识符通常使用大写字母,以便与变量区分。
宏定义的内容通常使用括号括起来,以避免优先级问题。
宏定义的内容通常不以分号结尾。
宏定义的内容可以跨行书写,使用反斜杠
\
连接符。建议在宏定义中使用空格,因为宏定义的内容会直接替换到代码中,如果不使用空格可能会导致语法错误。
1.2.2 作用域
宏定义的作用域是从定义处开始,到文件末尾或者遇到#undef
指令为止。 如果在文件中多次定义同一个宏,后面的定义会覆盖前面的定义。如:
1 |
|
1.2.3 嵌套宏定义
宏定义中可以使用其他宏定义,这种嵌套定义称为递归宏定义。如:
1 |
|
在预处理时,AREA
会被替换为PI * R * R
,然后PI
和R
又会被替换为3.1415926
和2
,即printf("%f\n", 3.1415926 * 2 * 2);
。最终输出12.5663704
。
1.3 #define
和 #typedef
#define
和#typedef
都是预处理指令,用于定义常量或类型别名。它们的区别在于:
#define
是文本替换,在编译时将宏定义的内容替换到代码中。#typedef
是类型别名,在编译时为类型定义一个别名。
比如:
1 |
|
以上代码中,PI
是一个常量,INT
是int
类型的别名。前者使用PI
时会被替换为3.1415926
,后者使用INT
时会被替换为int
。
2 有参宏
上面的宏定义在宏名之后没有参数,这种宏称为无参宏。除此之外,还有有参宏,即在宏名之后带有参数的宏。
有参宏的基本形式是:
1 |
和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。 与无参宏不同的一点是,有参宏在调用中,不仅要进行宏展开,而且还要用实参去替换形参。比如定义以下有参宏:
1 |
|
在代码中使用MAX
时,会将a
和b
的值传递给MAX
,然后返回较大的值。
看上去用法与函数调用类似,但实际上是有很大差别,再来看一个例子:
1 |
|
如果用函数来实现,那么COUNT(x + 1)
和COUNT(++x)
的结果都是7*7 = 49
,但是实际上结果是不同的。 在第一个printf
中,COUNT(x + 1)
会被替换为x + 1 * x + 1
,即6 + 1 * 6 + 1
,结果为13
。 在第二个printf
中,COUNT(++x)
会被替换为++x * ++x
,即7 * 8
,结果为56
。
这也是上文中提到的为什么要使用括号的原因,因为宏定义的内容会直接替换到代码中,如果不使用括号可能会导致优先级问题。
3 运算符
常见的运算符有:
#
:字符串化运算符,将宏参数转换为字符串。##
:连接运算符,将两个宏参数连接为一个标识符。
3.1 字符串化 #
运算符
字符串化运算符#
用于将宏参数转换为字符串,其基本形式是:
1 |
|
在上面的例子中,STR(Hello)
会被替换为"Hello"
,然后传递给printf
函数。
3.2 连接 ##
运算符
连接运算符##
用于将两个宏参数连接为一个标识符,其基本形式是:
1 |
|
在上面的例子中,CONCAT(a, b)
会被替换为a##b
,然后传递给printf
函数。
4 常见用法
4.1 条件编译
宏定义可以用于条件编译,通过#if
、#elif
、#else
、#endif
等指令来控制代码的编译。
比如在sylar
项目中的macro.h
文件中定义了一些宏:
1 |
|
逐句解释:
#if defined __GNUC__ || defined __llvm__
:如果定义了__GNUC__
或者__llvm__
宏,则定义以下宏。SYLAR_LIKELY(x) __builtin_expect(!!(x), 1)
SYLAR_UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
:否则定义以下宏。SYLAR_LIKELY(x) (x)
SYLAR_UNLIKELY(x) (x)
#endif
结束条件编译。
__builtin_expect(!!(x), 1)
:__builtin_expect
是GCC
和Clang
的内建函数,用于告诉编译器条件的可能性,!!(x)
是将x
转换为0
或1
,1
表示条件成立。
这样,当编译器为gcc
或llvm
时,会使用__builtin_expect
函数,否则直接返回x
。
4.2 宏定义函数
1 |
这是sylar
项目中的一个宏定义函数,用于断言,如果x
为false
,则输出错误日志,并打印调用栈。
#x
:字符串化运算符,将x
转换为字符串。sylar::BacktraceToString(100, 2, " ")
:打印调用栈,100
表示最多打印100
层,2
表示从第2
层开始打印," "
表示每一层的缩进。assert(x)
:断言,如果x
为false
,则终止程序。
这样,当程序中使用SYLAR_ASSERT(x)
时,如果x
为false
,会输出错误日志,并打印调用栈。
4.3 __FILE__
和 __LINE__
__FILE__
和__LINE__
是预定义的宏,分别表示当前文件名和当前行号。
1 |
这是sylar
项目中的一个宏定义函数,用于输出错误日志,其中__FILE__
和__LINE__
表示当前文件名和当前行号。
当程序中使用SYLAR_LOG_ERROR(logger)
时,会输出错误日志,并记录当前文件名和行号。
4.4 __VA_ARGS__
__VA_ARGS__
是预定义的宏,表示可变参数,用于宏定义中的可变参数列表。
1 |
这是sylar
项目中的一个宏定义函数,用于输出格式化日志,其中##__VA_ARGS__
表示可变参数列表。
如果程序中使用SYLAR_LOG_FMT(level, logger, fmt, ...)
时,会输出格式化日志,并记录当前文件名和行号,以及可变参数列表,比如:
1 | SYLAR_LOG_FMT(sylar::LogLevel::ERROR, SYLAR_LOG_ROOT(), "test %s %d", "hello", 123); |
输出结果为: 1
2024-04-28 18:58:29 ERROR 140735918953472 test hello 123