脚本没有类似C语言的预编译,【为宏正名】本应写入教科书的“世界设定”

#if USART_COUNT > 0extern int usart0_init(void);# define USART0_idx 0#endif

#if USART_COUNT > 1extern int usart1_init(void);# define USART1_idx 1#endif

#if USART_COUNT > 2extern int usart2_init(void);# define USART2_idx 2#endif

#if USART_COUNT > 3extern int usart3_init(void);# define USART3_idx 3#endif

那么是不是说,宏就比枚举好呢?当然不是,准确的说法应该是: 在谁的地盘谁的优点就突出。我们说枚举仅在编译阶段有效、它具有明确的语法意义(具体语法意义请参考相应的C语言教材)。相对宏来说,怎么理解枚举的好处呢?

枚举可以被当作类型来使用,并定义枚举变量 ——宏做不到;

当使用枚举作为函数的形参或者是switch检测的目标时 ,有些比较“智能”的C编译器会在编译阶段把枚举作为参考进行“强类型”检测 ——比如检查函数传递过程中你给的值是否是枚举中实际存在的;又比如在switch中是否所有的枚举条目都有对应的case(在省缺default的情况下)。

除IAR以外, 保存枚举所需的整型在一个编译环境中是相对来说较为确定的 (不是short就是int)——在这种情况下,枚举的常量值就具有了类型信息,这是用宏表示常量时所不具备的。

少数IDE只能对枚举进行语法提示而无法对宏进行语法提示。

【宏的本质和替换规则】

很多人都知道宏的本质是文字替换,也就是说, 预编译过程中宏会被替换成对应的字符串;然而在这一过程中所遵守的关键规则,很多人就不清楚了。

首先,针对一个没有被定义过的宏:

在#ifdef、#ifndef 以及 defined 表达式中,它可以正确的返回boolean量——确切的表示它没有被定义过;

在#if 中被直接使用(没有配合defined),则很多编译器会报告warning,指出这是一个不存在的宏,同时默认它的值是boolean量的false——而并不保证是"0";

在除以上情形外的其它地方使用,比如在代码中使用,则它会被作为代码的一部分原样保留到编译阶段——而不会进行任何操作; 通常这会在链接阶段触发“undefined symbol”错误——这是很自然的,因为你以为你在用宏(只不过因为你忘记定义了,或者没有正确include所需的头文件),编译器却以为你在说函数或者变量——当然找不到了。

举个例子,宏 __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

其次,定义宏的时候,如果只给了名字却没有提供内容:

在#ifdef、#ifndef 以及 defined 表达式中,它可以正确的返回boolean量——确切的表示它被定义了;

在#if 中被直接使用(没有配合defined) ,编译器会把它看作“空”;在一些数值表达式中,它会被默认当作“0”,没有任何警告信息会被产生

在除以上情形外的其它地方使用,比如在代码中使用, 编译器会把它看作“空字符串”(注意,这里不包含引号)——它不会存活到编译阶段;

最后,我们来说一个容易被人忽视的结论:

第一条:任何使用到胶水运算“##”对形参进行粘合的参数宏,一定需要额外的再套一层

第二条:其余情况下,如果要用到胶水运算,一定要在内部借助参数宏来完成粘合过程

为了理解这一“结论”,我们不妨举一个例子:在前面的代码中,我们定义过一个用于自动关闭中断并在完成指定操作后自动恢复原来状态的宏:

#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 #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的值是什么。

实际上,宏的变量特性是不存在的,更确切地说法是,宏是一种“引用”。那么什么是引用呢?《六祖坛经》中有一个非常著名的公案,用于解释慧能关于“不立文字”的主张,他说,通过“文字”来了解真理,就好比用手指向月亮——正如手指可以指出明月的所在,文字也的确可以用来描述真理,但毕竟手指不是明月,文字也不是真理本身,因此如果有办法直击真理,又如何需要执着于文字(经文)本身呢?我们虽然不一定要修禅,但这里手指与明月的关系恰好可以非常生动的解释“引用”这一概念。

69287ba4cb3cef1ee0d1738ad3260274.png

我们说宏的本质是一个引用,那么如何理解这种说法呢?我们来看一个例子:

#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);

打完收工。

免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。返回搜狐,查看更多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值