惊呆了!C语言中for还可以这样用?
【说在前面的话】
【被低估的价值】
 
for ( init_clause ; cond_expression ; iteration_expression )loop_statement
for 循环中的 cond_expression 和 interation_expression 都必须是表达式,而不能是直接的语句。
for 循环中第一个部分 init_clause 一开始是用来放置给变量赋值的表达式;但从ANSI-C99开始,init_clause 可以被用来建立局部变量;而局部变量的生命周期覆盖且仅覆盖整个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 的执行顺序,它可以用下面的流程图来表示:
图中灰色的部分为原本实际的执行流程,而纯黑色的线条以及最下方的虚线箭头则为等效的运行流程。与do {} while(0) 相比,在我们眼中 for 循环的几个关键部分就有了新的意义:
在执行用户代码之前(灰色部分),有能力进行一定的“准备工作”(Before部分);
在执行用户代码之后,有能力执行一定的“收尾工作”(After部分)
在init_clause阶段有能力定义一个“仅仅只覆盖” for 循环的,并且只对 User Code可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的地方的。
【构造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 部分
//! 假设用户要插入的内容我们都放在叫做 before 和after的函数里extern void before(void);extern void after(void);for (int i = 1; //!< init_clausei--?(before(),1):0; //!< cond_expressionafter()) //!< iteration_expression{...}
(i--) ? 1 : 0 
(i--) ?(before(), 1) //!< 使用逗哈表达式进行扩展: 0
由于逗号表达式只管 最右边的结果,忽略所有左边的返回值,因此,哪怕before()函数没有实际返回值对C编译器来说都是无所谓的。同理,由于我们在cond_expression部分已经完成了所有功能,因此 iteration_expression 就任由我们宰割了——编译器原本就对此处表达式所产生的数值并不感兴——我们直接放下 after() 函数即可。
#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);}
从 init_clause 的展开结果来看,完全符合要求:
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" 粘连在一起,形成一个唯一的局部变量名:
CONNECT3(__using_, __LINE__,_ptr) 
如果你对 CONNECT() 宏的来龙去脉感兴趣,可以。
#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__)
至此,我们完成了对 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 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);)}
唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:
#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);}}
由于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 文件句柄。
……
【构造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】
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 = %d\r\n", _.chID);}
跟With块一样,这里我们仍然“致敬” perl——使用 "_" 表示当前循环下的元素。在这个例子中,为了使用 foreach,我们需要提供至少两个信息:目标数组元素的类型(example_lv0_t)和目标数组(s_tItem)。
#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 = %d\r\n", (*_p).chID);}
其执行结果为:
#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 = %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);}
代码已经做了适当的展开和缩进,这里就不作进一步的分析了。
【后记】
宏不是奇技淫巧
宏可以封装出其它高级语言所提供的“基础设施”
设计良好的宏可以提升代码的可读性,而不是破坏它
设计良好的宏并不会影响调试
宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using() 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险
如果你喜欢我的思维、
如果你觉得我的文章对你有所启发或是帮助,
还请“点赞、收藏、转发” 三连!
欢迎订阅 裸机思维
