c语言with循环,你非常熟悉的for,居然这么多妙用!

#define __using2(__declare, __on_leave_expr)for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;CONNECT3(__using_, __LINE__,_ptr)++ == NULL;__on_leave_expr)

#define __using3(__declare, __on_enter_expr, __on_leave_expr)for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?((__on_enter_expr),1) : 0;__on_leave_expr)

#define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr)for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL;CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?((__on_enter_expr),1) : 0;__on_leave_expr)

借助宏的重载技术,我们可以根据用户输入的参数数量自动选择正确的版本:

#define using(...)CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)

至此,我们完成了对 for 的改造,并提出了__using1, __using2, __using3 和 __using4 四个版本变体。那么问题来了,他们分别有什么用处呢?

【提供不阻碍调试的代码封装】

前面的文章中,我们曾有意无意的提供过一个实现原子操作的封装:即在代码的开始阶段关闭全局中断并记录此前的中断状态;执行用户代码后,恢复关闭中断前的状态。其代码如下:

#define SAFE_ATOM_CODE(...){uint32_t CONNECT2(temp, __LINE__) = __disable_irq;__VA_ARGS____set_PRIMASK((CONNECT2(temp, __LINE__)));}

因此可以很容易的通过如下的代码来保护关键的寄存器操作:

/**fn void wr_dat (uint16_t dat)brief Write data to the LCD controllerparam[in] dat Data to write*/static __inline void wr_dat (uint_fast16_t dat){SAFE_ATOM_CODE (LCD_CS(0);GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */LCD_CS(1);)}

唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:

#define SAFE_ATOM_CODE__using2( uint32_t CONNECT2(temp,__LINE__) = __disable_irq,__set_PRIMASK(CONNECT2(temp,__LINE__)))

27ed7e901d81acd18eca86b1adbdaf02.png

修改上述的代码为:

static __inline void wr_dat (uint_fast16_t dat){SAFE_ATOM_CODE {LCD_CS(0);GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */LCD_CS(1);}}

由于using的本质是 for 循环,因为我们可以通过花括号的形式来包裹用户代码,因此,可以很方便的在用户代码中添加断点,单步执行。至于原子保护的功能,我们不妨将上述代码进行宏展开:

static __inline void wr_dat (uint_fast16_t dat){for (uint32_t temp154 = __disable_irq, *__using_154_ptr = NULL;__using_154_ptr++ == NULL ? ((temp154 = temp154),1) : 0;__set_PRIMASK(temp154) ){LCD_CS(0);GLCD_PORT->DAT = (dat >> 8);GLCD_PORT->DAT = (dat & 0xFF);LCD_CS(1);}}

通过观察,容易发现,这里巧妙使用 init_clause 给 temp154 变量进行赋值——在关闭中断的同时保存了此前的状态;并在原本 after 的位置放置了 恢复中断的语句 __set_PRIMASK(temp154)。

举一反三,此类方法除了用来开关中断以外,还可以用在以下的场合:

在OOPC中自动创建类,并使用 before 部分来执行构造函数;在 after 部分完成 类的析构。

在外设操作中,在 init_clause 部分定义指向外设的指针;在 before部分 Enable或者Open外设;在after部分Disable或者Close外设。

在RTOS中,在 before 部分尝试进入临界区;在 after 部分释放临界区

在文件操作中,在 init_clause 部分尝试打开文件,并获得句柄;在 after 部分自动 close 文件句柄。

在有MPU进行内存保护的场合,在 before 部分,重新配置MPU获取目标地址的访问权限;在 after部分再次配置MPU,关闭对目标地址范围的访问权限。

【构造with块】

不知道你们在实际应用中有没有遇到一连串指针访问的情形——说起来就好比是:

你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机

如果我们要操作这里的“手机”,实在是不想每次都写这么一长串“恶心”的东西,为了应对这一问题,Visual Basic(其实最早是Quick Basic)引入了一个叫做 WITH 块的概念,它的用法如下:

WITH 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机# 这里可以直接访问手机的各项属性,用 “.” 开头就行. 手机壳颜色 = xxxxx. 贴膜 = 玻璃膜END WITH

不光是Visual Basic,我们使用C语言进行大规模的应用开发时,或多或少也会遇到同样的情况,比如,配置 STM32 外设时,填写外设配置结构体的时候,每一行都要重新写一遍结构体变量的名字,也是在是很繁琐:

static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef;s_UARTHandle.Instance = USART2;s_UARTHandle.Init.BaudRate = 115200;s_UARTHandle.Init.WordLength = UART_WORDLENGTH_8B;s_UARTHandle.Init.StopBits = UART_STOPBITS_1;s_UARTHandle.Init.Parity = UART_PARITY_NONE;s_UARTHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;s_UARTHandle.Init.Mode = UART_MODE_TX_RX;

入股有了with块的帮助,上述代码可能就会变得更加清爽,比如:

static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef;with(s_UARTHandle) {.Instance = USART2;.Init.BaudRate = 115200;.Init.WordLength = UART_WORDLENGTH_8B;.Init.StopBits = UART_STOPBITS_1;.Init.Parity = UART_PARITY_NONE;.Init.HwFlowCtl = UART_HWCONTROL_NONE;.Init.Mode = UART_MODE_TX_RX;}

遗憾的是,如果要完全实现上述的结构,在C语言中是不可能的,但借助我们的 using 结构,我们可以做到一定程度的模拟:

#define with(__type, __addr) using(__type *_p=(__addr))#define _ (*_p)

在这里,我们要至少提供目标对象的类型,以及目标对象的地址:

static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef;with(UART_HandleTypeDef &s_UARTHandle) {_.Instance = USART2;_.Init.BaudRate = 115200;_.Init.WordLength = UART_WORDLENGTH_8B;_.Init.StopBits = UART_STOPBITS_1;_.Init.Parity = UART_PARITY_NONE;_.Init.HwFlowCtl = UART_HWCONTROL_NONE;_.Init.Mode = UART_MODE_TX_RX;}

注意到,这里“_”实际上被用来替代 s_UARTHandle——虽然感觉有点不够完美,但考虑到脚本语言 perl 有长期使用 "_" 表示本地对象的传统,这样一看,似乎"_" 就是一个对 "perl" 的完美致敬了。

【回归本职 foreach】

很多高级语言都有专门的 foreach 语句,用来实现对数组(或是链表)中的元素进行逐一访问。原生态C语言并没有这种奢侈,即便如此,Linux也定义了一个“野生”的 foreach 来实现类似的功能。为了演示如何使用 using 结构来构造 foreach,我们不妨来看一个例子:

typedef struct example_lv0_t {uint32_t wA;uint16_t hwB;uint8_t chC;uint8_t chID;} example_lv0_t;example_lv0_t s_tItem[8] = {{.chID = 0},{.chID = 1},{.chID = 2},{.chID = 3},{.chID = 4},{.chID = 5},{.chID = 6},{.chID = 7},};

我们希望实现一个函数,能通过 foreach 自动的访问数组 s_tItem 的所有成员,比如:

foreach(example_lv0_t, s_tItem) {printf("Processing item with ID = %drn", _.chID);}

跟With块一样,这里我们仍然“致敬” perl——使用 "_" 表示当前循环下的元素。在这个例子中,为了使用 foreach,我们需要提供至少两个信息:目标数组元素的类型(example_lv0_t)和目标数组(s_tItem)。

这里的难点在于,如何定义一个局部的指针,并且它的作用范围仅仅只覆盖 foreach 的循环体。此时,坐在角落里的 __with1 按耐不住了,高高的举起了双手——是的,它仅有的功能就是允许用户定义一个局部变量,并覆盖由第三方所编写的、由 {} 包裹的区域:

#define dimof(__array) (sizeof(__array)/sizeof(__array[0]))#define foreach(__type, __array)__using1(__type *_p = __array)for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);CONNECT2(count,__LINE__) > 0;_p++, CONNECT2(count,__LINE__)--)

上述的宏并不复杂,大家完全可以自己看懂,唯一需要强调的是,using 的本质是一个for,因此__using1 下方的for 实际上是位于由 __using1 所提供的循环体内的,也就是说,这里的局部变量_p其作用域也覆盖 下面的for 循环,这就是为什么我们可以借助:

#define _ (*_p)

的巧妙代换,通过 “_” 来完成对指针“_p”的使用。为了方便大家理解,我们不妨将前面的例子代码进行宏展开:

for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL;__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;)for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));count177 > 0;_p = _p+1, count177-- ){printf("Processing item with ID = %drn", (*_p).chID);}

其执行结果为:

dda1e4b2fc88093efe1814f55b8b6cb2.png

foreach目前的用法看起来“岁月静好”,似乎没有什么问题,可惜的是,一旦进行实际的代码编写,我们会发现,假如我们要在 foreach 结构中再用一个foreach,或是在foreach中使用 with 块,就会出现 “_” 被覆盖的问题——也就是在里层的 foreach或是 with 无法通过 “_” 来访问外层"_" 所代表的对象。为了应对这一问题,我们可以对 foreach 进行一个小小的改造——允许用户再指定一个专门的局部变量,用于替代"_" 表示当前循环下的对象:

#define foreach2(__type, __array)using(__type *_p = __array)for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);CONNECT2(count,__LINE__) > 0;_p++, CONNECT2(count,__LINE__)--)#define foreach3(__type, __array, __item)using(__type *_p = __array, *__item = _p, _p = _p, )for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);CONNECT2(count,__LINE__) > 0;_p++, __item = _p, CONNECT2(count,__LINE__)--)

这里的 foreach3 提供了3个参数,其中最后一个参数就是用来由用户“额外”指定新的指针的;与之相对,老版本的foreach我们称之为 foreach2,因为它只需要两个参数,只能使用"_"作为对象的指代。进一步的,我们可以使用宏的重载来简化用户的使用:

#define foreach(...)CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)

经过这样的改造,我们可以用下面的方法来为我们的循环指定一个叫做"ptItem"的指针:

foreach(example_lv0_t, s_tItem, ptItem) {printf("Processing item with ID = %drn", ptItem->chID);}

展开后的形式如下:

for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL;__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;)for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));count177 > 0;_p = _p+1, ptItem = _p, count177-- ){printf("Processing item with ID = %drn", ptItem->chID);}

代码已经做了适当的展开和缩进,这里就不作进一步的分析了。

【后记】

本文的目的,算是对【为宏正名】系列所介绍的知识进行一次示范——告诉大家如何正确的使用宏,配合已有的老的语法结构来“固化”一个新的模板,并以这个模板为起点,理解它的语法意义和用户,简化我们的日常开发。在这篇文章中,老的语法结构就是 for,它是由C语言原生支持的,借助宏,我们封装了一个新的语法结构 using, 借助它的4种不同形式、理解它们各自的特点,我们又分别封装了非常实用的SAFE_ATOM_CODE,With块和foreach语法结构——他们的存在至少证明了以下几点:

宏不是奇技淫巧

宏可以封装出其它高级语言所提供的“基础设施”

设计良好的宏可以提升代码的可读性,而不是破坏它

设计良好的宏并不会影响调试

宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值