for ( init_clause ; cond_expression ; iteration_expression )
loop_statement
- for 循环中的 cond_expression 和 interation_expression 都必须是表达式,而不能是直接的语句。
- for 循环中第一个部分 init_clause 一开始是用来放置给变量赋值的表达式;但从ANSI-C99开始,init_clause 可以被用来建立局部变量;而局部变量的生命周期覆盖且仅覆盖整个for循环——这一点非常有利用价值,也是大家容易忽略的地方。
为了说明这一点,我们不妨举几个例子。首先在C99标准之前,如果你要在 for 循环中使用一个循环变量,你只能在进入 for 之前将其定义好:
int i = 0;
...
for (i = 0; i < 100; i++) {
...
}
int i = 0, j,k;
...
for (i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, short j = 100; i < 100; i++) {
...
}
for (int i = 0, *p = NULL; i < 100; i++) {
...
}
- 另外一个值得注意的是 for 的执行顺序,它可以用下面的流程图来表示:
- 在执行用户代码之前(灰色部分),有能力进行一定的“准备工作”(Before部分);
- 在执行用户代码之后,有能力执行一定的“收尾工作”(After部分)
- 在init_clause阶段有能力定义一个“仅仅只覆盖” for 循环的,并且只对 User Code可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的地方的。
上面所提到的结构,在C#中有一个类似的语法,叫做 using(),其典型的用法如下:
using (StreamReader tReader = File.OpenText(m_InputTextFilePath))
{
while (!tReader.EndOfStream)
{
...
}
}
- 在 using 圆括号内定义的变量,其生命周期仅覆盖 using 紧随其后的花括号内部;
- 当用于代码离开 using 结构的时候,using 会自动执行一个“扫尾工作”,而这个扫尾工作是对应的类事先定义好的。在上述例子中,所谓的扫尾工作就是关闭 与 类StreamReader的实例tReader 所关联的文件——简单说就是using会自动把文件关闭,而不必用户亲自动手。
for (int i = 1; i > 0; i++) {
...
}
- 如何实现 before和after的部分?
- 现在用的变量 i 固定是 int 类型的,如何允许用户在 init_clause 定义自己的局部变量,并允许使用自己的类型?
对比前面的图例,我们知道 before 和 after 的部分实际上分别对应 for 循环的 cond_expression 和 iteration_expression;同时,这两个部分都必须是表达式——由于表达式的限制,能插入在 before 和 after 部分的内容实际上就只能是“普通表达式”或者是“函数”。
由于我们还必须至少借助 cond_expression 来实现 “只运行一次” 的功能,如何见缝插针的实现 before 的功能呢?不绕弯子,看代码:
//! 假设用户要插入的内容我们都放在叫做 before 和after的函数里
extern void before(void);
extern void after(void);
for (int i = 1; //!< init_clause
i--?(before(),1):0; //!< cond_expression
after()) //!< iteration_expression
{
...
}
接下来,我们借助一个问好表达式,尝试给 i-- 的结果做一个等效“解释”,即:
(i--) ? 1 : 0 用人话说就是,如果 (i--)值是非0的,我们就返回1,反之返回0。这么做的意义是为了进一步通过逗号表达式对 "1" 所在的部分进行扩展:
(i--) ?
(before(), 1) //!< 使用逗哈表达式进行扩展
: 0
至此,插入 before() 和 after() 的问题圆满解决。
问题二:如何允许用户定义自己的局部变量,并且拥有自己的类型
要解决这个问题,首先必须打破定势思维,即:for循环只能用整型变量。实际并非如此,对for来说真正起作用的只有 cond_expression 的返回值,而它只关心用户的表达式返回的 布尔量 是什么——换句话说,有无数种方法来产生 cond_expression,而使用普通的整形计数器,并对其进行判断只是众多方法中的一种。
打破了这一定势思维后,我们就从问题本身出发考虑:允许用户用自己的类型定义自己的变量——虽然看似我们并不能知道用户会用什么类型来定义变量,因而就无法写出通用的 cond_expression 来实现“让for执行且执行一次”的功能,然而,你们也许忘记了 init_clause 的一个特点:它还可以定义指针——换句话说,无论用户定义了什么类型,我们都可以在最后定义一个指向该类型的指针:
#define using(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *_ptr = NULL; \
_ptr++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
using(int a = 0,printf("========= On Enter =======\r\n"),
printf("========= On Leave =======\r\n"))
{
printf("\t In Body a=%d \r\n", ++a);
}
我们不妨将上述的宏进行展开,一个可能的结果是:
for (int a = 0, *_ptr = NULL;
_ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0;
printf("========= On Leave =======\r\n") )
{
printf("\t In Body a=%d \r\n", ++a);
}
int a = 0, *_ptr = NULL; 接下来,为了提高宏的鲁棒性,我们可以继续做一些改良,比如给指针一个唯一的名字:
#define using(__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 \
)
CONNECT3(__using_, __LINE__,_ptr)
更进一步,如果用户有不同的需求:比如想定义两个以上的局部变量,或是想省确 __on_enter_expr 或者是 __on_leave_expr ——我们完全可以定义多个不同版本的 using:
#define __using1(__declare) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL; \
)
#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__)
【提供不阻碍调试的代码封装】
前面的文章中,我们曾有意无意的提供过一个实现原子操作的封装:即在代码的开始阶段关闭全局中断并记录此前的中断状态;执行用户代码后,恢复关闭中断前的状态。其代码如下:
#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 controller
\param[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);
)
}
#define SAFE_ATOM_CODE() \
__using2( uint32_t CONNECT2(temp,__LINE__) = __disable_irq(), \
__set_PRIMASK(CONNECT2(temp,__LINE__)))
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);
}
}
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);
}
}
举一反三,此类方法除了用来开关中断以外,还可以用在以下的场合:
- 在OOPC中自动创建类,并使用 before 部分来执行构造函数;在 after 部分完成 类的析构。
- 在外设操作中,在 init_clause 部分定义指向外设的指针;在 before部分 Enable或者Open外设;在after部分Disable或者Close外设。
- 在RTOS中,在 before 部分尝试进入临界区;在 after 部分释放临界区
- 在文件操作中,在 init_clause 部分尝试打开文件,并获得句柄;在 after 部分自动 close 文件句柄。
- 在有MPU进行内存保护的场合,在 before 部分,重新配置MPU获取目标地址的访问权限;在 after部分再次配置MPU,关闭对目标地址范围的访问权限。
- ……
不知道你们在实际应用中有没有遇到一连串指针访问的情形——说起来就好比是:
你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机 如果我们要操作这里的“手机”,实在是不想每次都写这么一长串“恶心”的东西,为了应对这一问题,Visual Basic(其实最早是Quick Basic)引入了一个叫做 WITH 块的概念,它的用法如下:
WITH 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机
# 这里可以直接访问手机的各项属性,用 “.” 开头就行
. 手机壳颜色 = xxxxx
. 贴膜 = 玻璃膜
END WITH
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;
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;
}
#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;
}
【回归本职 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(example_lv0_t, s_tItem) {
printf("Processing item with ID = %d\r\n", _.chID);
}
这里的难点在于,如何定义一个局部的指针,并且它的作用范围仅仅只覆盖 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__)-- \
)
#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 = %d\r\n", (*_p).chID);
}
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__)-- \
)
#define foreach(...) \
CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
foreach(example_lv0_t, s_tItem, ptItem) {
printf("Processing item with ID = %d\r\n", 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 = %d\r\n", ptItem->chID);
}
【后记】
本文的目的,算是对【为宏正名】系列所介绍的知识进行一次示范——告诉大家如何正确的使用宏,配合已有的老的语法结构来“固化”一个新的模板,并以这个模板为起点,理解它的语法意义和用户,简化我们的日常开发。在这篇文章中,老的语法结构就是 for,它是由C语言原生支持的,借助宏,我们封装了一个新的语法结构 using(), 借助它的4种不同形式、理解它们各自的特点,我们又分别封装了非常实用的SAFE_ATOM_CODE(),With块和foreach语法结构——他们的存在至少证明了以下几点:
- 宏不是奇技淫巧
- 宏可以封装出其它高级语言所提供的“基础设施”
- 设计良好的宏可以提升代码的可读性,而不是破坏它
- 设计良好的宏并不会影响调试
- 宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using() 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险
本文源自微信公众号:痞子衡嵌入式,不代表用户或本站观点,如有侵权,请联系nick.zong@aspencore.com 删除!