【说在前面的话】
市面上大部分C程序员对宏存在巨大的误解甚至是恐惧,并因此极力避免宏的适度使用,甚至将宏在封装中发挥正确作用的行为视作是对C语言的“背叛”——震惊之余,对于为什么大家会有这种想法的原因,我曾经一度是非常“傲慢的”,这种傲慢与某些人宣称“穷人都是因为懒所以才穷”时所表现出的那种态度并无任何本质不同——然而我错了,在闲暇之余认真看了不少经典的C语言教材后我才意识到:
不是读者普遍懒或者轻视教材中有关宏的内容,而是那些对宏来说如同“加法交换律、结合律”一样的基本规则和知识并没有认真且完整的出现在教科书中!
这是何等的“呵呵”。这下全都清楚了:
真是哭笑不得。这些规则是如此简单,介绍一下根本无需多么复杂的篇幅。接下来,让我们简单的学习一下这些本应该写入教科书中的基本内容。注意,这与你们在其它公众号里学到的关于某些宏的基本使用方法是两回事。
【宏不属于C语言】
说“宏不属于C语言”是一种夸张的说法,但却非常反映问题的本质和基本事实:
知道这一知识有什么用呢?首先,你会明白,宏本身是与C语言的其它语法毫无关联的。宏有自己的语法,且非常简单。在进行宏展开的时候,编译器并不会去进行任何宏以外的C语言语法检查、甚至根本不知道C语言语法。实际上,有大量C语言老鸟特别喜欢在其它C语言以外的文本文件里使用“宏”(其实还有条件编译之类的),最典型的例子就是在Arm Compiler 6的scatter-script中用宏来定义一些地址常数:
#! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c
#define ADDRESS 0x20000000
#include "include_file_1.h"
LR1 ADDRESS
{
…
}
这里,第一行的命令行:
#! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c
就是告诉linker,在处理scatter-script之前要执行“#!” 后面的命令行,这里的"-E"就是告诉armclang:“我们只进行预编译”——也就是"#include"以及宏替换之类的工作——所以宏“ADDRESS” 会被替换会 0x20000000,而"include_file_1.h" 中的内容也会被加入到当前的scatter-script文件中来。
需要强调下,在这个例子中,放在第一行“#!”后面的命令行之所以为会被linker自动执行,是因为linker就是这么使用 “.sct” 文件的。对于其它想使用C语言宏对任意文本文件进行预处理的场合,需要自己动手编写命令行和脚本。比如,如果你想在 perl 里使用 C语言的预编译,那么就需要你在执行目标 .pl 文件前,先用C语言编译器对其进行一次预编译。
总的来说,“宏不属于C语言”并非空穴来风,事实上,只要你有兴趣去写脚本,包括宏在内的所有预编译语法可以在一切文本文件中使用。
知道这一知识的另外一个作用就是回答每一个C语言初学者都绕不开的经典问题:“宏和枚举有啥区别”?有啥区别?这区别老大了:
其实,从宏和枚举服务的阶段看来,他们是老死不相往来的。那么具体在使用时,这里的区别表现在什么地方呢?我们来看一个例子:
#define USART_COUNT 4
#if USART_COUNT > 0
extern uint8_t s_chUSARTBuffer[USART_COUNT];
#endif
这里例子意图很简单,根据宏USART_COUNT的值来条件编译。如果我们把USART_COUNT换成枚举就不行了:
typedef enum {
/* list all the available USART here */
USART0_idx = 0,
USART1_idx,
USART2_idx,
USART3_idx,
/* number of USARTs*/
USART_COUNT,
}usart_idx_t;
#if USART_COUNT > 0
extern uint8_t s_chUSARTBuffer[USART_COUNT];
#endif
在这个例子里,USART_COUNT的值会随着前面列举的UARTx_idx的增加而自动增加——作为一个技巧——精确的表示当前实际有效的USART数量,从意义上说严格贴合了 USART_COUNT 这个名称的意义。这个代码看似没有问题,但实际上根据前面的知识我们知道:条件编译是在“预编译阶段”进行的、枚举是在“编译阶段”才有意义。换句话说,当下面代码判断枚举USART_COUNT的时候,预编译阶段根本不认识它是谁(预编译阶段没有任何C语言的语法知识)——这时候USART_COUNT作为枚举还没出生呢!
#if USART_COUNT > 0
extern uint8_t s_chUSARTBuffer[USART_COUNT];
#endif
同样道理,如果你想借助下面的宏来生成代码,得到的结果会出人意料:
typedef enum {
/* list all the available USART here */
USART0_idx = 0,
USART1_idx,
USART2_idx,
USART3_idx,
/* number of USARTs*/
USART_COUNT,
}usart_idx_t;
extern int usart0_init(void);
extern int usart1_init(void);
extern int usart2_init(void);
extern int usart3_init(void);
#define USART_INIT(__USART_INDEX) \
usart##__USART_INDEX##_init()
应用中,我们期望配合UARTn_idx与宏USART_INIT一起使用:
...
USART_INIT(USART1_idx);
...
借助宏的胶水运算“##”,我们期望的结果是:
...
usart1_init();
...
由于同样的原因——在进行宏展开的时候,枚举还没有“出生”——实际展开的效果是这样的:
...
usartUSART1_idx_init();
...
由于函数 usartUSART1_idx_init() 并不存在,所以在链接阶段linker会报告类似“undefined symbol usartUSART1_idx_init()”——简单说就是找不到函数。要解决这一问题也很简单,直接把枚举用宏来定义就可以了:
#define USART_COUNT 4
#if USART_COUNT > 0
extern int usart0_init(void);
# define USART0_idx 0
#endif
#if USART_COUNT > 1
extern int usart1_init(void);
# define USART1_idx 1
#endif
#if USART_COUNT > 2
extern int usart2_init(void);
# define USART2_idx 2
#endif
#if USART_COUNT > 3
extern int usart3_init(void);
# define USART3_idx 3
#endif
那么是不是说,宏就比枚举好呢?当然不是,准确的说法应该是:在谁的地盘谁的优点就突出。我们说枚举仅在编译阶段有效、它具有明确的语法意义(具体语法意义请参考相应的C语言教材)。相对宏来说,怎么理解枚举的好处呢?
【宏的本质和替换规则】
很多人都知道宏的本质是文字替换,也就是说,预编译过程中宏会被替换成对应的字符串;然而在这一过程中所遵守的关键规则,很多人就不清楚了。
首先,针对一个没有被定义过的宏:
举个例子,宏 __STDC_VERSION__ 可以被用来检查当前ANSI-C的标准:
#if __STD_VERSION__ >= 199901L
/* support C99 */
# define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
#else
/* doesn't support C99, assume C89/90 */
# define SAFE_ATOM_CODE(__CODE) \
{ \
uint32_t wTemp = __disable_irq(); \
__CODE; \
__set_PRIMASK(wTemp); \
}
#endif
上述写法在支持C99的编译器中是不会有问题的,因为 __STDC_VERSION__ 一定会由编译器预先定义过;而同样的代码放到仅支持C89/90的环境中就有可能会出问题,因为 __STDC_VERSION__ 并不保证一定会被事先定义好(C89/90并没有规定要提供这个宏),因此 __STDC_VERSION__ 就有可能成为一个未定义的宏,从而触发编译器的warning。为了修正这一问题,我们需要对上述内容进行适当的修改:
#if defined(__STD_VERSION__) && __STD_VERSION__ >= 199901L
/* support C99 */
...
#else
/* doesn't support C99, assume C89/90 */
...
#endif
其次,定义宏的时候,如果只给了名字却没有提供内容:
最后,我们来说一个容易被人忽视的结论:
为了理解这一“结论”,我们不妨举一个例子:在前面的代码中,我们定义过一个用于自动关闭中断并在完成指定操作后自动恢复原来状态的宏:
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
由于这里定义了一个变量wTemp,而如果用户插入的代码中也使用了同名的变量,就会产生很多问题:轻则编译错误(重复定义);重则出现局部变量wTemp强行取代了用户自定义的静态变量的情况,从而直接导致系统运行出现随机性的故障(比如随机性的中断被关闭后不再恢复,或是原本应该被关闭的全局中断处于打开状态等等)。为了避免这一问题,我们往往会想自动给这个变量一个不会重复的名字,比如借助 __LINE__ 宏给这一变量加入一个后缀:
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp##__LINE__ = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
一个使用例子:
...
SAFE_ATOM_CODE(
/* do something here */
...
)
...
假设这里 SAFE_ATOM_CODE 所在行的行号是 123,那么我们期待的代码展开是这个样子的(我重新缩进过了):
...
{
uint32_t wTemp123 = __disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
然而,实际展开后的内容是这样的:
...
{
uint32_t wTemp__LINE__ = __disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
这里,__LINE__似乎并没有被正确替换为123,而是以原样的形式与wTemp粘贴到了一起——这就是很多人经常抱怨的 __LINE__ 宏不稳定的问题。实际上,这是因为上述宏的构建没有遵守前面所列举的两条结论导致的。
从内容上看,SAFE_ATOM_CODE() 要粘合的对象并不是形参,根据结论第二条,需要借助另外一个参数宏来帮忙完成这一过程。为此,我们需要引入一个专门的宏:
#define CONNECT2(__A, __B) __A##__B
注意到,这个参数宏要对形参进行胶水运算,根据结论第一条,需要在宏的外面再套一层,因此,修改代码得到:
#define __CONNECT2(__A, __B) __A##__B
#define CONNECT2(__A, __B) __CONNECT2(__A, __B)
#define __CONNECT3(__A, __B, __C) __A##__B##__C
#define CONNECT2(__A, __B, __C) __CONNECT3(__A, __B, __C)
修改前面的定义得到:
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t CONNECT2(wTemp,__LINE__) = \
__disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
有兴趣的朋友可以通过 "-E" 可以观察到 __LINE__ 被正确的展开了。
【宏是引用而非变量】
具体实践中,很多人在使用宏过程中会产生“宏是一种变量”的错觉,这是因为无论一个宏此前是否定义过,我们都可以借助 #undef 操作,强制注销它,从而有能力重新给这一宏赋予一个新的值,例如:
#include <stdbool.h>
#undef false
#undef true
#define false 0
#define true (!false)
上述例子里,在stdbool.h中,true通常被定义为1,这会导致很多人在编写期望值是true的逻辑表达式时,一不小心落入圈套——因为true的真实含义是“非0”,这就包含了除了1以外的一切非0的整数,当用户写下:
if (true == xxxxx) {...}
表达式时,实际获得的是:
if (1 == xxxxx) {...}
这显然是过于狭隘的——会出现实际为true却判定为false(走else分支)的情况,为了避免这种情况,实践中,我们应该避免在逻辑表达式中使用true——无论true的值是什么。
实际上,宏的变量特性是不存在的,更确切地说法是,宏是一种“引用”。那么什么是引用呢?《六祖坛经》中有一个非常著名的公案,用于解释慧能关于“不立文字”的主张,他说,通过“文字”来了解真理,就好比用手指向月亮——正如手指可以指出明月的所在,文字也的确可以用来描述真理,但毕竟手指不是明月,文字也不是真理本身,因此如果有办法直击真理,又如何需要执着于文字(经文)本身呢?我们虽然不一定要修禅,但这里手指与明月的关系恰好可以非常生动的解释“引用”这一概念。
我们说宏的本质是一个引用,那么如何理解这种说法呢?我们来看一个例子:
#define EXAMPLE_A 123
#define EXAMPLE EXAMPLE_A
#undef EXAMPLE_A
对于下面的代码:
CONNECT2(uint32_t wVariable, EXAMPLE);
如果宏是一个变量,那么展开的结果应该是:
uint32_t wVariable123;
然而,我们实际获得的是:
uint32_t wVariableEXAMPLE_A;
如何理解这一结果呢?
如果宏是一个引用,那么当EXAMPLE_A与123之间的关系被销毁时,原本EXAMPLE > EXAMPLE_A > 123 的引用关系就只剩下 EXAMPLE > EXAMPLE_A。又由于EXAMPLE_A已经不复存在,因此EXAMPLE_A在展开时就被当作是最终的字符串,与"uint32_t wVariable"连接到了一起。
这一知识对我们有什么帮助呢?帮助实在太大了!甚至可以把预编译器直接变成一个脚本解释器。受到篇幅的限制,我们无法详细展开,就展示一个最常见的用法吧:
还记得前面定义的USART_INIT()宏么?
#define USART_INIT(__USART_INDEX) \
usart##__USART_INDEX##_init()
使用的时候,我们需要确保填写在括号中的任何内容都必须直接对应一个在效范围内的整数(比如0~3),比如:
USART_INIT(USART1_idx);
由于USART1_idx直接对应于字符串 “1”,因此,实际会被展开为:
usart1_init();
很多时候,我们可能会希望代码有更多的灵活性,因此,我们会再额外定义一个宏来将某些代码与具体的USART祛除不必要的耦合:
#include "app_cfg.h"
#ifndef DEBUG_USART
# define DEBUG_USART USART0_idx
#endif
USART_INIT(DEBUG_USART);
这样,虽然代码默认使用USART0作为 DEBUG_USART,但用户完全可以通过配置文件 "app_cfg.h" 来修改这一配置。到目前为止,一切都好。但此时,app_cfg.h 中的内容已经和模块内的代码有了一定的“隔阂”——用户不一定知道 DEBUG_USART 必须是一个有效的数字字符串,而不能是一个表达式,哪怕这个表达式会“自动”计算出最终需要使用的值。比如,在 app_cfg.h 中,可能会出现以下的内容:
/* app_cfg.h */
#define USART_MASTER_CNT 1
#define USART_SLAVE_CNT 2
#define DEBUG_USART (USART_MASTER_CNT + USART_SLAVE_CNT)
这里,出于某种不可抗拒原因,用户希望永远使用最后一个USART作为 DEBUG_USART,并通过一个表达式计算出了这个USART的编号。遗憾的是,当用户自信满满的写下这一“智能算法”后,我们得到的实际上是:
usart(1+2)_init();
对编译器来说,这显然不是一个有效的C语法,因此报错是在所难免。那么如何解决这一问题呢?借助宏的引用特性,我们可以获得如下的内容:
#include "app_cfg.h"
#ifndef DEBUG_USART
# define DEBUG_USART USART0_idx
#else
# if DEBUG_USART == 0
# undef DEBUG_USART
# define DEBUG_USART 0
# elif DEBUG_USART == 1
# undef DEBUG_USART
# define DEBUG_USART 1
# elif DEBUG_USART == 2
# undef DEBUG_USART
# define DEBUG_USART 2
# elif DEBUG_USART == 3
# undef DEBUG_USART
# define DEBUG_USART 3
# else
# error "out of range for DEBUG_USART"
#endif
进一步思考,假设一个宏的取值范围是 0~255,而我们想把这一宏的值切实的转化为对应的十进制数字字符串,按照上面的方法,那我们岂不是要累死?且慢,我们还有别的办法,假设输入数值的宏叫 MFUNC_IN_U8_DEC_VALUE 首先分别获得3位十进制的每一位上的数字内容:
#undef __MFUNC_OUT_DEC_DIGIT_TEMP0
#undef __MFUNC_OUT_DEC_DIGIT_TEMP1
#undef __MFUNC_OUT_DEC_DIGIT_TEMP2
#undef __MFUNC_OUT_DEC_STR_TEMP
/* 获取个位 */
#if (MFUNC_IN_U8_DEC_VALUE % 10) == 0
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 0
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 1
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 1
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 2
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 2
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 3
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 3
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 4
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 4
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 5
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 5
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 6
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 6
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 7
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 7
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 8
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 8
#elif (MFUNC_IN_U8_DEC_VALUE % 10) == 9
# define __MFUNC_OUT_DEC_DIGIT_TEMP0 9
#endif
/* 获取十位数字 */
#if ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 0
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 0
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 1
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 1
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 2
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 2
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 3
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 3
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 4
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 4
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 5
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 5
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 6
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 6
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 7
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 7
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 8
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 8
#elif ((MFUNC_IN_U8_DEC_VALUE/10) % 10) == 9
# define __MFUNC_OUT_DEC_DIGIT_TEMP1 9
#endif
/* 获取百位数字 */
#if ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 0
# define __MFUNC_OUT_DEC_DIGIT_TEMP2 0
#elif ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 1
# define __MFUNC_OUT_DEC_DIGIT_TEMP2 1
#elif ((MFUNC_IN_U8_DEC_VALUE/100) % 10) == 2
# define __MFUNC_OUT_DEC_DIGIT_TEMP2 2
#endif
接下来,我们将代表“个、十、百”的三个宏拼接起来:
#if __MFUNC_OUT_DEC_DIGIT_TEMP2 == 0
# if __MFUNC_OUT_DEC_DIGIT_TEMP1 == 0
# define MFUNC_OUT_DEC_STR __MFUNC_OUT_DEC_DIGIT_TEMP0
# else
# define MFUNC_OUT_DEC_STR CONNECT2( __MFUNC_OUT_DEC_DIGIT_TEMP1,\
__MFUNC_OUT_DEC_DIGIT_TEMP0)
# endif
#else
# define MFUNC_OUT_DEC_STR CONNECT3( __MFUNC_OUT_DEC_DIGIT_TEMP2,\
__MFUNC_OUT_DEC_DIGIT_TEMP1,\
__MFUNC_OUT_DEC_DIGIT_TEMP0)
#endif
#undef MFUNC_IN_U8_DEC_VALUE
此时,保存在 MFUNC_OUT_U8_DEC_VALUE 中的值就是我们所需的十进制数字了。为了方便使用,我们将上述内容放置到一个专门的头文件中,就叫做mf_u8_dec2str.h (https://github.com/vsfteam/vsf/blob/master/source/vsf/utilities/preprocessor/mf_u8_dec2str.h),修改前面的例子:
#include "app_cfg.h"
#ifndef DEBUG_USART
# define DEBUG_USART USART0_idx
#endif
/* 建立脚本输入值与 DEBUG_USART 之间的引用关系*/
#undef MFUNC_IN_U8_DEC_VALUE
#define MFUNC_IN_U8_DEC_VALUE DEBUG_USART
/* "调用"转换脚本 */
#include "mf_u8_dec2str.h"
/* 建立 DEBUG_USART 与脚本输出值之间的引用 */
#undef DEBUG_USART
#define DEBUG_USART MFUNC_OUT_U8_DEC_VALUE
USART_INIT(DEBUG_USART);