C语言宏定义

宏定义macro definitionC/C++中的一种预处理指令
可以在编译之前替换源代码中的一些文本。

1 概念

宏定义是一种预处理指令,用于在源代码中定义一些常量函数代码片段的缩写形式。

通过宏定义,可以将一段代码片段或者常量值与一个标识符相关联,然后在代码中使用该标识符来代替相应的代码或值。

1.1 无参宏

通常,宏定义使用#define关键字来创建,其基本形式是:

1
#define 宏名称 宏取代文本
  • 宏名称:标识符,用于代表宏定义的名称。

  • 宏取代文本:宏定义的内容,可以是常量、表达式、代码片段等。

比如,定义一个常量宏:

1
#define PI 3.1415926

然后,便可以在代码中使用PI来代替3.1415926

又如,使用宏替换代码片段:

1
#define HELLO printf("Hello, World!\n")

在代码中使用HELLO,相当于使用printf("Hello, World!\n")

1.2 注意事项

1.2.1 书写规范

  • 宏定义的标识符通常使用大写字母,以便与变量区分。

  • 宏定义的内容通常使用括号括起来,以避免优先级问题。

  • 宏定义的内容通常不以分号结尾。

  • 宏定义的内容可以跨行书写,使用反斜杠\连接符。

  • 建议在宏定义中使用空格,因为宏定义的内容会直接替换到代码中,如果不使用空格可能会导致语法错误。

1.2.2 作用域

宏定义的作用域是从定义处开始,到文件末尾或者遇到#undef指令为止。 如果在文件中多次定义同一个宏,后面的定义会覆盖前面的定义。如:

1
2
3
4
5
#define PI 3.1415926
#define PI 3.14
printf("%f\n", PI); // 3.14
#undef PI
printf("%f\n", PI); // error, PI未定义

1.2.3 嵌套宏定义

宏定义中可以使用其他宏定义,这种嵌套定义称为递归宏定义。如:

1
2
3
4
#define PI 3.1415926
#define R 2
#define AREA PI * R * R
printf("%f\n", AREA); // 12.5663704

在预处理时,AREA会被替换为PI * R * R,然后PIR又会被替换为3.14159262,即printf("%f\n", 3.1415926 * 2 * 2);。最终输出12.5663704

1.3 #define#typedef

#define#typedef都是预处理指令,用于定义常量或类型别名。它们的区别在于:

  • #define文本替换,在编译时将宏定义的内容替换到代码中。

  • #typedef类型别名,在编译时为类型定义一个别名。

比如:

1
2
#define PI 3.1415926
typedef int INT;

以上代码中,PI是一个常量,INTint类型的别名。前者使用PI时会被替换为3.1415926,后者使用INT时会被替换为int

2 有参宏

上面的宏定义在宏名之后没有参数,这种宏称为无参宏。除此之外,还有有参宏,即在宏名之后带有参数的宏。

有参宏的基本形式是:

1
#define 宏名称(参数列表) 宏取代文本

和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。 与无参宏不同的一点是,有参宏在调用中,不仅要进行宏展开,而且还要用实参去替换形参。比如定义以下有参宏:

1
2
3
4
5
6
#define MAX(a, b)  ((a) > (b) ? (a) : (b))

int a = 3, b = 5;
int max = MAX(a, b);
printf("%d\n", max); // 替换为((3) > (5) ? (3) : (5))
// 输出5

在代码中使用MAX时,会将ab的值传递给MAX,然后返回较大的值。

看上去用法与函数调用类似,但实际上是有很大差别,再来看一个例子:

1
2
3
4
#define COUNT(M) M * M               //定义有参宏
int x = 6;
printf("COUNT = %d\n", COUNT(x + 1));// 输出结果: COUNT = 13
printf("COUNT = %d\n", COUNT(++x)); // 输出结果: COUNT = 56

如果用函数来实现,那么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
2
#define STR(x) #x
printf("%s\n", STR(Hello)); // 输出Hello

在上面的例子中,STR(Hello)会被替换为"Hello",然后传递给printf函数。

3.2 连接 ## 运算符

连接运算符##用于将两个宏参数连接为一个标识符,其基本形式是:

1
2
3
4
#define CONCAT(a, b) a##b
int a = 10, b = 20;
int ab = CONCAT(a, b);
printf("%d\n", ab); // 输出结果:1020

在上面的例子中,CONCAT(a, b)会被替换为a##b,然后传递给printf函数。

4 常见用法

4.1 条件编译

宏定义可以用于条件编译,通过#if#elif#else#endif等指令来控制代码的编译。

比如在sylar项目中的macro.h文件中定义了一些宏:

1
2
3
4
5
6
7
8
9
10
11
12
#if defined __GNUC__ || defined __llvm__
/// LIKCLY 宏的封装, 告诉编译器优化,条件大概率成立
# define SYLAR_LIKELY(x) __builtin_expect(!!(x), 1)
/// LIKCLY 宏的封装, 告诉编译器优化,条件大概率不成立
# define SYLAR_UNLIKELY(x) __builtin_expect(!!(x), 0)

#else

# define SYLAR_LIKELY(x) (x)
# define SYLAR_UNLIKELY(x) (x)

#endif

逐句解释:

  • #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_expectGCCClang的内建函数,用于告诉编译器条件的可能性,!!(x)是将x转换为011表示条件成立。

这样,当编译器为gccllvm时,会使用__builtin_expect函数,否则直接返回x

4.2 宏定义函数

1
2
3
4
5
6
7
#define SYLAR_ASSERT(x) \
if(!(x)) { \
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ASSERTION: " #x \
<< "\nbacktrace:\n" \
<< sylar::BacktraceToString(100, 2, " "); \
assert(x); \
}

这是sylar项目中的一个宏定义函数,用于断言,如果xfalse,则输出错误日志,并打印调用栈。

  • #x:字符串化运算符,将x转换为字符串。

  • sylar::BacktraceToString(100, 2, " "):打印调用栈,100表示最多打印100层,2表示从第2层开始打印," "表示每一层的缩进。

  • assert(x):断言,如果xfalse,则终止程序。

这样,当程序中使用SYLAR_ASSERT(x)时,如果xfalse,会输出错误日志,并打印调用栈。

4.3 __FILE____LINE__

__FILE____LINE__是预定义的宏,分别表示当前文件名和当前行号。

1
2
#define SYLAR_LOG_ERROR(logger) \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, sylar::LogLevel::ERROR, __FILE__, __LINE__, 0, sylar::GetThreadId(), sylar::GetFiberId(), time(0), sylar::Thread::GetName(), sylar::Fiber::GetName(), 0, 0, 0, 0)))

这是sylar项目中的一个宏定义函数,用于输出错误日志,其中__FILE____LINE__表示当前文件名和当前行号。

当程序中使用SYLAR_LOG_ERROR(logger)时,会输出错误日志,并记录当前文件名和行号。

4.4 __VA_ARGS__

__VA_ARGS__是预定义的宏,表示可变参数,用于宏定义中的可变参数列表。

1
2
3
4
#define SYLAR_LOG_FMT(level, logger, fmt, ...) \
if(logger->getLevel() <= level) { \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, __FILE__, __LINE__, 0, sylar::GetThreadId(), sylar::GetFiberId(), time(0), sylar::Thread::GetName(), sylar::Fiber::GetName(), 0, 0, 0, 0, fmt, ##__VA_ARGS__))); \
}

这是sylar项目中的一个宏定义函数,用于输出格式化日志,其中##__VA_ARGS__表示可变参数列表。

如果程序中使用SYLAR_LOG_FMT(level, logger, fmt, ...)时,会输出格式化日志,并记录当前文件名和行号,以及可变参数列表,比如:

1
2
SYLAR_LOG_FMT(sylar::LogLevel::ERROR, SYLAR_LOG_ROOT(), "test %s %d", "hello", 123);
// ##__VA_ARGS__ 被替换为 "hello", 123 并传递给 fmt,即 "test %s %d" ==》 "test hello 123"

输出结果为:

1
2024-04-28 18:58:29 ERROR 140735918953472 test hello 123