advanced marco in c

介绍

宏定义是在c/c++里特有的方式, 像变量一样, 又像模板编程一样, 但最常见的用法还是做头文件的唯一性保证

在每一个头文件都套用这种格式,就可以避免多次引入头文件而导致的重复定义报错

1
2
3
4
5
6
#ifdef FILE_NAME
#def FILE_NAME

// 代码

#endif FILE_NAME

原理

宏定义与变量、模板的最大区别在与处理的时期, 宏定义在预编译时处理, 而变量和模板函数则是在编译期处理。
查看预编译后的代码可以使用命令gcc -E 或者 cpp, 实际上是前者是调用了后者

1
2
NAME
cpp - The C Preprocessor

用法

除了#ifdef 的用法,宏定义可以分两种类型,变量型和函数型

变量型

这个最简单,就像使用变量一样,先define 然后再使用

1
2
3
4
5
6
# marco.c
#define BUFFER_SIZE 1024

int main(){
foo = (char *) malloc (BUFFER_SIZE);
}

执行 gcc -E marco.c 得到

1
foo = (char *) malloc (1024);

多行使用 ‘' 来连接

1
2
3
4
#include <stdio.h>
#define GREETING_STR \
"hello \
world"
  • 注意, 宏定义的定义不分前后, 也不像变量那样先定义再使用, 宏定义可以先使用后定义

以下两种方式的效果相同

1
2
3
4
5
6
7
8
define GREETING_NAME "wayou"

define GREETING "hello," GREETING_NAME

int main() {
printf(GREETING);
return 0;
}
1
2
3
4
5
6
7
8
9
10
+define GREETING "hello," GREETING_NAME

define GREETING_NAME "wayou"

-define GREETING "hello," GREETING_NAME

int main() {
printf(GREETING);
return 0;
}

函数型

函数类型的宏,可以像正常函数一样指定入参,入参需为逗号分隔合法的 C 字面量。
宏的参数必须要用括号包起来,否则当参数为表达式时,会出错

1
2
3
4
#define min(X, Y)  ((X) < (Y) ? (X) : (Y))
x = min(a, b); → x = ((a) < (b) ? (a) : (b));
y = min(1, 2); → y = ((1) < (2) ? (1) : (2));
z = min(a + 28, *p); → z = ((a + 28) < (*p) ? (a + 28) : (*p));

宏定义字符串化

当宏定义的参数被引号包起来时, 不会进行替换,如下

1
2
#define foo(x) x, "x"
foo(bar) → bar, "x"

加入需要将参数替换到字符串里, 可以使用’#’

1
2
3
4
5
6
7
#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);

而当 这里的x 也是宏定义时, 只有if里的x会替换, 字符串里的x则不会替换

1
2
#define X ( 1 - 1 )
WARN_IF ( X == 0);

会被替换为

1
2
3
do { if (( 1 - 1 ) == 0) fprintf (
stderr
, "Warning: " "X == 0" "\n"); } while (0);

拼接

通过 ## 可将两个宏展开成一个,即将两者进行了拼接,宏拼接一般用在需要拼接的宏是来自宏参数的情况,
其他情况,大可直接将两个宏写在一起即可

当有以下情况时非常有用

1
2
3
4
5
6
7
8
9
10
11
12
struct command
{
char *name;
void (*function) (void);
};

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

};

可以使用如下:

1
2
3
4
5
6
7
8
#define COMMAND(NAME)  { #NAME, NAME ## _command }

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

};

不定参数和混合参数

宏定义也可以使用不定参数

1
#define eprintf(args…) fprintf (stderr, args)

也可以使用混合参数

1
#define eprintf(format, args...) fprintf (stderr, format, args)

这个可以常在格式化打印时用到, 例如 spdlog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define SPDLOG_LOGGER_CALL(logger, level, ...)                                                                                             \
if (logger->should_log(level)) \
logger->log(spdlog::source_loc{SPDLOG_FILE_BASENAME(__FILE__), __LINE__, SPDLOG_FUNCTION}, level, __VA_ARGS__)

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_TRACE
#define SPDLOG_LOGGER_TRACE(logger, ...) SPDLOG_LOGGER_CALL(logger, spdlog::level::trace, __VA_ARGS__)
#define SPDLOG_TRACE(...) SPDLOG_LOGGER_TRACE(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_TRACE(logger, ...) (void)0
#define SPDLOG_TRACE(...) (void)0
#endif

#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG
#define SPDLOG_LOGGER_DEBUG(logger, ...) SPDLOG_LOGGER_CALL(logger, spdlog::level::debug, __VA_ARGS__)
#define SPDLOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_DEBUG(logger, ...) (void)0
#define SPDLOG_DEBUG(...) (void)0
#endif

重复和覆盖

这些是相似的:

1
2
3
#define FOUR (2 + 2)
#define FOUR (2 + 2)
#define FOUR (2 /* two */ + 2)

以下都是不同的宏

1
2
3
4
#define FOUR (2 + 2)
#define FOUR ( 2+2 ) // 空白位置不一样
#define FOUR (2 * 2) // 宏的内容不一样
#define FOUR(score,and,seven,years,ago) (2 + 2) // 入参不一样

对于使用了 #undef 注销过的宏,再次定义同名的宏时,要求新定义的宏不与老的相似。

而如果说一个已经存在的宏,并没有注销,重复定义时,如果相似,则新的定义会忽略,如果不相似,编译器会报警告同时使用新定义的宏。这允许在多个文件中定义同一个宏。

最后

可以查看更多内置宏定义