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