你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言中的 for 循环结构

[复制链接]
gaosmile 发布时间:2020-9-5 12:17
【被低估的价值】

想必大家对C语言中的 for 循环结构并不陌生。根据C/C++语法网站cppreference.com 的介绍,for 的语法结构如下:

  1. % J3 S, ~! G/ T* K& E
  2. for ( init_clause ; cond_expression ; iteration_expression )
    : H9 R6 f) n+ B. E6 g
  3.     loop_statement
复制代码
这里,我并不想假设大家对 for 结构一无所知,并介绍一堆教科书上已有的内容。然而,在 for 的语法结构中有几个大家容易忽视的地方,而它们恰恰是本文后续各种“展开”的基础:
( g+ \8 Y0 l. W8 r. [

* o& ]) A* c/ w5 w* t! m0 W, C2 u
  • for 循环中的 cond_expression 和 interation_expression 都必须是表达式,而不能是直接的语句。
    $ V: b4 p% V3 X/ _+ Z
  • for 循环中第一个部分 init_clause 一开始是用来放置给变量赋值的表达式;但从ANSI-C99开始,init_clause 可以被用来建立局部变量;而局部变量的生命周期覆盖且仅覆盖整个for循环——这一点非常有利用价值,也是大家容易忽略的地方。
    ; L9 |$ m  A( Y; a% ~0 S
    " ]7 b' J9 b0 a  J
3 K- ]3 d6 ~" a

为了说明这一点,我们不妨举几个例子。首先在C99标准之前,如果你要在 for 循环中使用一个循环变量,你只能在进入 for 之前将其定义好:; I" i7 I7 x( U8 d! R4 m. x* F

  1. 4 s3 l2 y, h+ Z0 e4 g0 B
  2. int i = 0;/ W$ {$ L1 S$ J$ S
  3. ...
    . D: `: A* k8 k! R: l# ]
  4. for (i = 0; i < 100; i++) {
    6 ^  x4 u1 t9 b# k; T* Q4 x8 B( o
  5.     ...
    5 o, w& r9 G- s
  6. }
复制代码
如你所见,虽然我们可以在 init_clause 的位置对变量赋值,但它并不是必须的——多少一点鸡肋是不是?也许更鸡肋的是,你可以在 init_clause 这里完成更多的赋值操作,比如:
7 k! H" @9 V# s5 _. b
  1. ) H( ?3 _$ }) [
  2. int i = 0, j,k;
    ! C, g" q% y  y) F% K7 e: j( x" E
  3. ...
    ) k. [% s' B2 w- n+ X; Q$ x" L
  4. for (i = 0, j = 100, k = 1; i < 100; i++) {
    " x3 X3 \- y0 A  `5 x' A
  5.     ...
      m* m$ j! c8 A6 \5 X3 o( V
  6. }
复制代码
实际上,明眼人都可以看出,init_clause 中所作的事情完全可以放置到 for 循环之前去完成,还可以避免“使用逗号进行分隔” 这样让人不那么习惯的使用方式。也许是意识到这一点,C99允许在 init_clause 里定义局部变量,而正是这一点,完全改变了 for 的命运(关于这一点,我们将在随后的内容中详细介绍)。现在,上述代码可以等效的改写为:

  • , z% X* O3 t& X0 [$ q) Q. P7 H
for (int i = 0, j = 100, k = 1; i < 100; i++) {    ...}需要强调的是,这里仍然有一个小小的限制,即:init_clause 里虽然可以定义局部变量,但这些变量只能是同一类型的,或者是指向这一类型的指针。因此下面的写法是非法的:: D: h" Z% }) v) `) a
  1. 2 t0 |+ r7 |" Z0 e7 L
  2. for (int i = 0, j = 100, k = 1; i < 100; i++) {
    ) }/ A* _3 p  N8 ]! G1 {3 a' H2 L& x  p
  3.     ...3 S* `7 ^4 m, m% c. l3 [* q
  4. }
复制代码
而这样的写法是合法的:
5 z+ `6 \, g7 U: e
  1.   w; J4 A' h, u
  2. for (int i = 0, short j = 100; i < 100; i++) {
    7 ]7 q# D4 j" f* `5 ^
  3.      ...
    6 O; `8 L" `$ l7 O6 J3 k2 ]
  4. }
复制代码
请大家务必留意这里的语法细节,我们将在后面的封装中大规模使用。$ M" @0 i! b$ `) c


' _, y; x- |( k6 [
  • 另外一个值得注意的是 for 的执行顺序,它可以用下面的流程图来表示:

    & t/ `  z+ a" P; o* o7 v
微信图片_20200905121015.png
容易发现,经过必要的“构造”,我们可以恰好实现一个如同 do { } while(0)一样的效果:3 K( b  ?$ F# J0 T/ g
微信图片_20200905121127.png
9 C7 s* f+ I3 e, H2 T* k, K- x1 q
图中灰色的部分为原本实际的执行流程,而纯黑色的线条以及最下方的虚线箭头则为等效的运行流程。与do {} while(0) 相比,在我们眼中 for 循环的几个关键部分就有了新的意义:

# H6 J* N* F1 q& W# e5 J; v$ u
  • 在执行用户代码之前(灰色部分),有能力进行一定的“准备工作”(Before部分);
  • 在执行用户代码之后,有能力执行一定的“收尾工作”(After部分)
  • 在init_clause阶段有能力定义一个“仅仅只覆盖” for 循环的,并且只对 User Code可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的地方的。9 i7 P) V/ A# x- g; K- g" O3 X  X
    . h. O. a( m7 \' Q# X
微信图片_20200905121130.png

, S; z$ g' n+ E7 v. U% P
【构造using结构】

上面所提到的结构,在C#中有一个类似的语法,叫做 using(),其典型的用法如下:3 b  u1 _' z, R2 w! u
  1. 7 b4 a9 ]' f. m# j0 s. D
  2. using (StreamReader tReader = File.OpenText(m_InputTextFilePath))  A, C6 v# t! y, h1 c4 e# {, m
  3. {
    - R' ~$ @: W# q" S& J
  4.     while (!tReader.EndOfStream)
    4 w8 Q# a/ j$ i" _0 S; Y, _6 i
  5.     {/ ~- _6 V- _4 u( `! H. ?
  6.         ...
    ) Q( A# A0 z( o) J6 t
  7.     }
    7 i  b5 f3 v# i. K! P
  8. }
复制代码
" I9 Y3 w! P3 m( D" _" S, N
以上述代码为例进行讲解:0 z- T1 ^4 e2 x! z. |
  • 在 using 圆括号内定义的变量,其生命周期仅覆盖 using 紧随其后的花括号内部;
  • 当用于代码离开 using 结构的时候,using 会自动执行一个“扫尾工作”,而这个扫尾工作是对应的类事先定义好的。在上述例子中,所谓的扫尾工作就是关闭 与 类StreamReader的实例tReader 所关联的文件——简单说就是using会自动把文件关闭,而不必用户亲自动手。

    6 T/ B2 w: |) Y$ {9 i
* ?8 Q4 J: c( i
是不是闻到了熟悉的味道?不要搞错因果关系——我们正是对C#中的using结构“甚是眼馋”才决定自己动手,用 for 来创造一个——现有C#的using结构才有我们后面的尝试。下图是using所等校流程图,可以看到他比我们此前的结构还少了一个“Before”部分:
- O8 k% n8 D8 Y9 _# Y
微信图片_20200905121133.png
要实现类似using的结构,首先要考虑如何构造一个"至执行一次"的for循环结构。要做到这一点,毫无难度:+ X8 b# j' _( Q' V) B, r  _6 o

  1. 5 I7 ?0 K& @) i; k/ ?
  2. for (int i = 1; i > 0; i++) {
    9 m1 [5 d4 N( t4 U
  3.     ...4 @  Q! B) i" d, [' _1 s! ?& P
  4. }
复制代码

3 G9 t! k* Q  X/ b  J7 U$ h# D, n: f
以此为起点,对比我们的“蓝图”,发现至少有以下几个问题:% l# s+ H0 |/ g3 g- k
3 R' M" @0 b3 F8 O$ M
  • 如何实现 before和after的部分?
  • 现在用的变量 i 固定是 int 类型的,如何允许用户在 init_clause 定义自己的局部变量,并允许使用自己的类型?

    & L7 M( z- i$ |$ v. ~

9 y+ g. W* C$ s* r( P6 D
问题一:如何实现 before 和 after 部分: R6 Z  g, M5 ~% }, h
对比前面的图例,我们知道 before 和 after 的部分实际上分别对应 for 循环的 cond_expression 和 iteration_expression;同时,这两个部分都必须是表达式——由于表达式的限制,能插入在 before 和 after 部分的内容实际上就只能是“普通表达式”或者是“函数”。由于我们还必须至少借助 cond_expression 来实现 “只运行一次” 的功能,如何见缝插针的实现 before 的功能呢?不绕弯子,看代码:
5 d6 G2 A9 S" d  H* N5 Q
  1. ) D# h0 S9 ~7 k/ s; ~  F
  2. //! 假设用户要插入的内容我们都放在叫做 before 和after的函数里# x: k3 \% R: S" f8 G: y1 E$ T
  3. extern void before(void);
    8 c, R2 Y& j- U; o* D
  4. extern void after(void);. e0 s1 u: Z: E" f
  5. , V8 r. P1 f( y7 a! s7 z
  6. for (int i = 1;           //!< init_clause; o6 ^; E) }: ~9 \5 l/ b6 U
  7.      i--?(before(),1):0;  //!< cond_expression
    & I/ J0 B" g4 `2 E2 F: n% x) q* @
  8.      after())             //!< iteration_expression4 `" z( V7 `3 R
  9. {5 Q7 H/ E+ H/ g( T9 A8 ?* ]
  10.      ...
    5 x6 X; r" i4 o  `
  11. }
复制代码
我们知道,cond_expression 只在乎用户表达式的返回值是0还是非0,因此,这里其实真正起作用的本体是 "i--"——第一次判断的时候返回值是1,由于自减操作,第二次判断的时候就是0了——这就完成了让 for 运行且只运行一次的功能。接下来,我们借助一个问好表达式,尝试给 i-- 的结果做一个等效“解释”,即:
  • (i--) ? 1 : 05 k8 {7 ~; i0 |6 b3 R) z

$ V+ O+ x2 A2 ]# z( q用人话说就是,如果 (i--)值是非0的,我们就返回1,反之返回0。这么做的意义是为了进一步通过逗号表达式对 "1" 所在的部分进行扩展:! ~, M# Y+ |3 A0 `  y; \3 }/ g
  1. 8 C$ c) p. c  D% G7 `% }
  2. (i--) ?
    9 y2 ~& N) I" b
  3.       (before(), 1)       //!< 使用逗哈表达式进行扩展
    . l# {+ Y0 C7 z5 N: @
  4. :     0
复制代码
3 \( _' ]+ J) U+ t+ d
由于逗号表达式只管 最右边的结果,忽略所有左边的返回值,因此,哪怕before()函数没有实际返回值对C编译器来说都是无所谓的。同理,由于我们在cond_expression部分已经完成了所有功能,因此 iteration_expression 就任由我们宰割了——编译器原本就对此处表达式所产生的数值并不感兴——我们直接放下 after() 函数即可。
# {7 C: F; s1 V) e# i8 e
至此,插入 before() 和 after() 的问题圆满解决。
' W5 ], W- `) ]/ i4 J- F
7 i  H' f* o- d1 N4 Q
问题二:如何允许用户定义自己的局部变量,并且拥有自己的类型要解决这个问题,首先必须打破定势思维,即:for循环只能用整型变量。实际并非如此,对for来说真正起作用的只有  cond_expression 的返回值,而它只关心用户的表达式返回的 布尔量 是什么——换句话说,有无数种方法来产生 cond_expression,而使用普通的整形计数器,并对其进行判断只是众多方法中的一种。
- q8 {8 z7 ~6 j- F( d0 V4 e$ |
打破了这一定势思维后,我们就从问题本身出发考虑:允许用户用自己的类型定义自己的变量——虽然看似我们并不能知道用户会用什么类型来定义变量,因而就无法写出通用的 cond_expression 来实现“让for执行且执行一次”的功能,然而,你们也许忘记了 init_clause 的一个特点:它还可以定义指针——换句话说,无论用户定义了什么类型,我们都可以在最后定义一个指向该类型的指针:
; Q% G& R8 F# c7 }0 |

  1. % J' o( l, N3 A- i6 P' m
  2. #define using(__declare, __on_enter_expr, __on_leave_expr)   \
    % P: _, P7 `! l$ f
  3.             for (__declare, *_ptr = NULL;                    \
    7 B" y8 {9 r/ N! P3 F; e
  4.                  _ptr++ == NULL ?                            \
    ; a1 P: q* H" }
  5.                     ((__on_enter_expr),1) : 0;               \
    : }  [1 q" G5 X$ \1 `/ B
  6.                  __on_leave_expr                             \
    ' }3 k) [" X& r4 k& z9 L) X/ z. G) N
  7.                 )
      ~% j4 A& S$ p# A' z6 F5 v# Z
复制代码

1 |" k) D; ]8 V! O+ H* Q* ]1 |1 K+ q0 t
为了验证我们的结果,不妨写一个简单的代码:
) e* h9 X; J3 H1 s: Y/ t8 o! I' C
  1. 2 ~0 ^% v0 `) b" Z
  2. using(int a = 0,printf("========= On Enter =======\r\n"), 9 j1 x' f7 x/ l/ i$ H8 I+ q
  3.                 printf("========= On Leave =======\r\n"))
    ' n8 J2 [! X7 i* O  p
  4. {/ d& h1 ~1 G! W. n7 T" ~6 P- R$ |8 ]
  5.     printf("\t In Body a=%d \r\n", ++a);% o; j3 f0 G- c+ F( M6 Z  w4 c
  6. }
复制代码
. o+ a8 \2 f1 w) P8 f6 V
这是对应的执行效果:
微信图片_20200905121136.png

6 O# m3 |4 q% d# w2 v
我们不妨将上述的宏进行展开,一个可能的结果是:
  1. 9 ?4 X9 D; M5 I) a' S& O
  2. for (int a = 0, *_ptr = NULL;
    8 ?% @! t# x+ }2 S! l. J9 s8 o
  3.      _ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0; 9 ^8 ~1 d) G$ W
  4.      printf("========= On Leave =======\r\n") )
    ' n- [& R0 |) ]3 S
  5. {
    ; n# u8 Q6 h+ \* ?5 M4 |  y
  6.     printf("\t In Body a=%d \r\n", ++a);9 l2 j# q) Z5 W/ V# E
  7. }
复制代码

8 J) U4 H  _3 r% p% B8 u
从 init_clause 的展开结果来看,完全符合要求:
  • int a = 0, *_ptr = NULL;
    $ K8 [& E" o1 ]3 S' d6 d; ~8 N

& U; R: z# ]  U5 Z: Y
接下来,为了提高宏的鲁棒性,我们可以继续做一些改良,比如给指针一个唯一的名字:
! i, q* |+ u8 U3 O" s

  1. 3 Q2 t- @7 h4 ]/ D( l8 }
  2. #define using(__declare, __on_enter_expr, __on_leave_expr)                   \* w; \# J& G1 w5 z3 Z; L
  3.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    3 F# a/ p/ @2 l, c- s3 Y
  4.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \, S& G- f  H% y: `  P* O
  5.                     ((__on_enter_expr),1) : 0;                                  \. I) E& ~/ G7 {" W5 X& y
  6.                  __on_leave_expr                                                \8 T3 G/ z# ?$ Q% k8 T. @" V
  7.                 )( c! _# q: M# T+ y! [
复制代码

* i: W; z  X  i
这里,实际上是使用了前面文章中介绍的宏 CONNECT3() 将 “__using_”,__LINE__所表示的当前行号,以及 "_ptr" 粘连在一起,形成一个唯一的局部变量名:9 t& B: {, |: q
  • CONNECT3(__using_, __LINE__,_ptr)
    7 X  T9 F9 I# L
如果你对 CONNECT() 宏的来龙去脉感兴趣,可以单击这里。# J6 u2 M' b9 g$ I

* Y3 R6 [! a  ?3 X# Y3 V
更进一步,如果用户有不同的需求:比如想定义两个以上的局部变量,或是想省确 __on_enter_expr 或者是 __on_leave_expr ——我们完全可以定义多个不同版本的 using:
, L/ e5 b8 o5 O+ e
  1.   t, A, N- x7 N; ]) U
  2. #define __using1(__declare)                                                     \# h% q9 K: B0 w# m. u
  3.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    ( K& o' R) O! F; j
  4.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \
    - P) t2 G' G/ T& H* t4 N+ g  j
  5.                 )
    - H4 c' P% V) h( T: K/ X( g
  6. ) u3 X, {- l8 a& ^# R) F

  7. ) y" t( `  x3 m- X
  8. #define __using2(__declare, __on_leave_expr)                                    \% e. K+ E$ J. Q9 ?
  9.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \/ s* D8 e& T$ F2 |) @8 P# E/ M+ D- r
  10.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \
    * R4 c9 R: F3 p; y. M8 S; n
  11.                  __on_leave_expr                                                \
    & K' N8 o- ^+ [# C$ f1 K$ c: S7 q/ I
  12.                 )0 e* [) }+ X! W8 c" _; {

  13. 7 ]: r; J. m1 C6 P
  14. # _  o: N: p* Z3 t5 q' H
  15. #define __using3(__declare, __on_enter_expr, __on_leave_expr)                   \7 t- x& b( }5 V
  16.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    ; g9 @6 S, W! }8 c$ I  X& G; E
  17.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \
    - X4 Z$ _7 `4 X9 |/ q3 M
  18.                     ((__on_enter_expr),1) : 0;                                  \
    8 k( F8 ]) t; n8 T8 h
  19.                  __on_leave_expr                                                \
    9 Y: h" P4 b. |' G* e5 L
  20.                 )
    7 z* {4 d% ]. ]) F6 R" C% N/ L
  21. 1 j* i. u; n% W# h
  22. #define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr)              \/ f# q; C. H; u0 U% b
  23.             for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL;     \$ `* z; p9 A+ M$ C) a, ^
  24.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \2 s, y4 G: a* B# ^" y8 {5 c
  25.                     ((__on_enter_expr),1) : 0;                                  \0 n  n( n0 y# c  Y
  26.                  __on_leave_expr                                                \
    & m! y! J5 l4 I% E' I
  27.                 )3 Q1 X! E9 [* f8 T6 S# o! V
复制代码

  q' i; y, M1 E3 C
借助宏的重载技术,我们可以根据用户输入的参数数量自动选择正确的版本:
9 e0 r5 l% M7 E" c) S7 B* q
  1. 0 H& }1 }) n# ~* Z) Z' r
  2. #define using(...)  \3 y. T, L7 l7 G5 c4 m* h# R
  3.     CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码

( H2 l3 y6 A9 G9 s! O: n3 }+ ^* ?

% @# a  Z+ o+ ]0 w% E& q8 U
至此,我们完成了对 for 的改造,并提出了__using1, __using2, __using3 和 __using4 四个版本变体。那么问题来了,他们分别有什么用处呢?
0 O: G8 @# b7 h# L6 c, t. P
【提供不阻碍调试的代码封装】

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

  1. 0 {: P" g; [/ E3 ~" ?" {9 @+ ?1 F
  2. #define SAFE_ATOM_CODE(...)                               \
    " J& K3 v+ t4 q# s5 |
  3. {                                                         \" M  k. C! I. G' r9 o0 c
  4.     uint32_t CONNECT2(temp, __LINE__) = __disable_irq();  \
    7 b9 x0 L. p5 \; l" O+ \
  5.     __VA_ARGS__                                           \& H6 V( Q, s5 Y. ~6 ~* t% B& z& O
  6.     __set_PRIMASK((CONNECT2(temp, __LINE__)));            \
    ; @+ Q6 w: _7 Y. c/ @$ u5 o% U
  7. }
复制代码

# J* |% h# _8 j
因此可以很容易的通过如下的代码来保护关键的寄存器操作:, I/ ~3 @/ ^4 u8 P& M( t' T8 e
  1. 7 b1 Q4 |& M8 C! u
  2. /**) q4 v3 x2 ^' Q, E! a. q# G3 s
  3.   \fn          void wr_dat (uint16_t dat)# X# J1 @4 L' a/ r
  4.   \brief       Write data to the LCD controller
    6 e; ^: g* c( _) \& B, N
  5.   \param[in]   dat  Data to write
    2 q& C) |" j6 t: |  s! }' n
  6. */
    : O* H3 U. v) s6 T
  7. static __inline void wr_dat (uint_fast16_t dat) : ?$ U1 K2 Z6 t- n. n; m: h2 w
  8. {5 d# t* h/ E8 l6 ^: F* e
  9.     SAFE_ATOM_CODE (: l! I/ C  V" j8 G+ m
  10.         LCD_CS(0);. h- t1 x8 |. p4 i" d
  11.         GLCD_PORT->DAT = (dat >>   8);   /* Write D8..D15 */& J: G. s0 R6 \+ |1 c' a7 u/ m. O
  12.         GLCD_PORT->DAT = (dat & 0xFF);   /* Write D0..D7 */
      W" k7 x0 i  j. v9 h
  13.         LCD_CS(1);& V& W. H6 W1 J# v
  14.     )& i) s9 I: m/ ]) W3 B
  15. }
复制代码
6 t( U6 J6 D9 G6 R
唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:. s, H: Y- s/ K4 N5 F4 Q
  1. ' Z- A+ i; x+ h; K% V, _
  2. #define SAFE_ATOM_CODE()                                                        \, e" _0 g# ^- U
  3.             __using2(  uint32_t CONNECT2(temp,__LINE__) = __disable_irq(),      \
    - P3 e( K" Y% c% l
  4.                     __set_PRIMASK(CONNECT2(temp,__LINE__)))
复制代码

* d. |3 [: M  n% h4 ?% E$ C
微信图片_20200905121139.png
修改上述的代码为:4 E3 U9 Z& r6 M& t' i( ?

  1. - z/ ]& T/ @1 W+ [" o0 J
  2. static __inline void wr_dat (uint_fast16_t dat)
    $ ~. x  X2 R( i2 v( z  Z# x
  3. {! [7 N0 C* ^3 i: b% b, R/ L2 d4 l
  4.     SAFE_ATOM_CODE() {0 h2 f3 T5 {, v3 ~' D
  5.         LCD_CS(0);
    1 Q% F+ r' }/ t7 x
  6.         GLCD_PORT->DAT = (dat >>   8);   /* Write D8..D15 */- Q, D, J, T: v% K+ w. T8 E1 V
  7.         GLCD_PORT->DAT = (dat & 0xFF);   /* Write D0..D7 */
    8 _. m! D: W% d+ C% b
  8.         LCD_CS(1);/ D/ |- r% X8 A) N- G$ y1 @2 e
  9.     }/ {0 R/ S+ W+ p. n( X/ m) t" e8 ?
  10. }
复制代码

9 p/ ?( c: _2 U/ E1 V/ @' b5 O& [
由于using的本质是 for 循环,因为我们可以通过花括号的形式来包裹用户代码,因此,可以很方便的在用户代码中添加断点,单步执行。至于原子保护的功能,我们不妨将上述代码进行宏展开:
. }5 M& ]5 h9 _/ `' u
  1. 3 A/ |9 q" q8 t: H% u* _- C
  2. static __inline void wr_dat (uint_fast16_t dat); d8 b3 l; V7 G/ h- r5 ^
  3. {) t& \5 B3 ~  v) d. I  o* E; c/ V
  4.     for (uint32_t temp154 = __disable_irq(), *__using_154_ptr = NULL; * B2 I' _7 g) r4 U$ Y
  5.          __using_154_ptr++ == NULL ? ((temp154 = temp154),1) : 0;
    9 L" e7 n7 `8 U6 W0 F
  6.          __set_PRIMASK(temp154) )   C+ W3 ]& P) _; ]$ I' M
  7.     {
    , ^2 F0 m) S* x1 \6 x
  8.         LCD_CS(0);! Z$ A7 |: [: \0 }! P" ?( S
  9.         GLCD_PORT->DAT = (dat >> 8);! U) |9 D! v: o" R: V
  10.         GLCD_PORT->DAT = (dat & 0xFF);
    0 t+ x+ l( j; |# C2 p' Z
  11.         LCD_CS(1);% F, ^& p# m9 w/ v, @5 G
  12.     }
    & ~8 E* X7 G3 \' L
  13. }
复制代码

4 k+ e1 z$ _: b7 j1 h# V
通过观察,容易发现,这里巧妙使用 init_clause 给 temp154 变量进行赋值——在关闭中断的同时保存了此前的状态;并在原本 after 的位置放置了 恢复中断的语句 __set_PRIMASK(temp154)。, U* _/ `2 ]2 D" K& ]/ r
+ s4 \: H* e2 S+ D6 v
举一反三,此类方法除了用来开关中断以外,还可以用在以下的场合:
5 E( k* j. [. W# X% E: P% R
  • 在OOPC中自动创建类,并使用 before 部分来执行构造函数;在 after 部分完成 类的析构。
  • 在外设操作中,在 init_clause 部分定义指向外设的指针;在 before部分 Enable或者Open外设;在after部分Disable或者Close外设。
  • 在RTOS中,在 before 部分尝试进入临界区;在 after 部分释放临界区
  • 在文件操作中,在 init_clause 部分尝试打开文件,并获得句柄;在 after 部分自动 close 文件句柄。
  • 在有MPU进行内存保护的场合,在 before 部分,重新配置MPU获取目标地址的访问权限;在 after部分再次配置MPU,关闭对目标地址范围的访问权限。
  • ……* Y0 _& O% {: L2 m
      i1 B+ n" h+ S& V3 q/ x3 m
3 C' b3 N5 L! a4 r8 H# Y8 U4 v) k
" T% e( p, j' n3 w4 W
【构造with块】7 \, z% f6 d$ w) V' P. M- U

不知道你们在实际应用中有没有遇到一连串指针访问的情形——说起来就好比是:
  • 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机
    . f# f5 ^: y7 K

  S$ q" v5 d' t1 w5 Z, }" ^" ?, n/ l如果我们要操作这里的“手机”,实在是不想每次都写这么一长串“恶心”的东西,为了应对这一问题,Visual Basic(其实最早是Quick Basic)引入了一个叫做 WITH 块的概念,它的用法如下:9 e7 }* ?, ^1 ~$ f6 a4 [

  1. % i( @" q0 K$ M: V# ^" C" }
  2. WITH 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机 % t1 ~  r; ]8 ^7 u! b- x+ `$ X
  3.     # 这里可以直接访问手机的各项属性,用 “.” 开头就行
    2 T2 @3 Z) m" I+ E4 e  F
  4.     . 手机壳颜色 = xxxxx
    ) u! F- r! v! n
  5.     . 贴膜 = 玻璃膜% k3 O7 ]5 f6 t+ H/ [% y( r1 @
  6. END WITH
复制代码
9 l' B% W5 P* u; b* v- A! V
不光是Visual Basic,我们使用C语言进行大规模的应用开发时,或多或少也会遇到同样的情况,比如,配置 STM32 外设时,填写外设配置结构体的时候,每一行都要重新写一遍结构体变量的名字,也是在是很繁琐:
( V6 A5 Z# X( ~3 I6 X4 s, c. B. C
  1. 6 k) M9 p9 p- W
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();  u7 y! x: R1 z( X
  3. s_UARTHandle.Instance        = USART2;1 Z% i; ?1 [# l: U; B
  4. s_UARTHandle.Init.BaudRate   = 115200;
    " |( T. D7 x/ p. \3 N+ ~  I+ p
  5. s_UARTHandle.Init.WordLength = UART_WORDLENGTH_8B;
    ( k/ k2 w# P9 z+ ~2 N/ E2 v
  6. s_UARTHandle.Init.StopBits   = UART_STOPBITS_1;
    / B& O1 Q" e. V
  7. s_UARTHandle.Init.Parity     = UART_PARITY_NONE;
    " _6 X* L% [% a' }
  8. s_UARTHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;; Y( ^0 F/ _$ H3 O0 b
  9. s_UARTHandle.Init.Mode       = UART_MODE_TX_RX;
复制代码
; z, z3 K* @) O) m! g! w4 T  o
入股有了with块的帮助,上述代码可能就会变得更加清爽,比如:
  1. ' W, ]* g# G8 e0 M4 e
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();  s$ q) b2 t9 {- A" g
  3. with(s_UARTHandle) {) e; R, r; B' p% u7 h% T6 N
  4.     .Instance        = USART2;" G' ~% b! |/ c8 P& V; B& @
  5.     .Init.BaudRate   = 115200;
    $ ?5 k; E0 |( n4 f9 C4 K
  6.     .Init.WordLength = UART_WORDLENGTH_8B;
    - w+ k9 `# _; ]8 V2 _
  7.     .Init.StopBits   = UART_STOPBITS_1;
    + _5 n5 P" j% Y% c* N) N
  8.     .Init.Parity     = UART_PARITY_NONE;
    * Q% b) ]1 b! g8 ?) U
  9.     .Init.HwFlowCtl  = UART_HWCONTROL_NONE;0 q4 ]/ G9 i& N/ q' f$ E) V4 Q
  10.     .Init.Mode       = UART_MODE_TX_RX;* @5 f; R! S3 b1 [+ Z
  11. }
复制代码
6 R( U0 Q8 z! M( q+ P
遗憾的是,如果要完全实现上述的结构,在C语言中是不可能的,但借助我们的 using() 结构,我们可以做到一定程度的模拟:9 O1 j+ l6 r/ F

  1. + B7 Z. N! V# O+ [) |, Y
  2. #define with(__type, __addr)  using(__type *_p=(__addr))3 [0 A! \# }0 H( `
  3. #define _ (*_p)
复制代码
7 q+ j0 w" B$ i3 u/ j
在这里,我们要至少提供目标对象的类型,以及目标对象的地址:
# v0 c) ~4 T3 U9 `. [/ S
  1. $ m' s- E* _) D2 Y
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
    2 I* G. j8 _9 S& T  D! A
  3. with(UART_HandleTypeDef &s_UARTHandle) {/ k% Y1 y5 V4 k  Z
  4.     _.Instance        = USART2;
    4 h2 M. @6 j* O4 T( _' b+ c5 S
  5.     _.Init.BaudRate   = 115200;' @7 U6 n' I( ]; M
  6.     _.Init.WordLength = UART_WORDLENGTH_8B;9 W& j  e& u9 [
  7.     _.Init.StopBits   = UART_STOPBITS_1;' g  v. \2 [' b9 H5 U; z; J. h
  8.     _.Init.Parity     = UART_PARITY_NONE;0 k6 l7 K. s. v9 G
  9.     _.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    0 w" U: D$ U6 E1 m8 b- S
  10.     _.Init.Mode       = UART_MODE_TX_RX;
    & h3 @9 m9 K8 g3 x' L
  11. }
复制代码
& Y$ ?6 f/ S3 H) ^1 ]- e. r( }
注意到,这里“_”实际上被用来替代 s_UARTHandle——虽然感觉有点不够完美,但考虑到脚本语言 perl 有长期使用 "_" 表示本地对象的传统,这样一看,似乎"_" 就是一个对 "perl" 的完美致敬了。
" s4 F" t6 E: O7 T& K

  d0 Z* V2 a. `+ V' m1 m6 q  z6 y
【回归本职 foreach】

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

  1. 3 E/ D% Y$ q" a3 A% V/ \
  2. typedef struct example_lv0_t {2 }8 M; E) R$ @7 I
  3.     uint32_t    wA;4 L% v9 T& G8 ?: Q6 E3 S
  4.     uint16_t    hwB;8 p. ?4 b6 e2 F. ?- ?8 ]1 w2 Z
  5.     uint8_t     chC;5 E  Q' R. _2 c/ D0 W
  6.     uint8_t     chID;1 W, D' ~3 U: v# d; i
  7. } example_lv0_t;
      V' X% s5 a4 V( k1 `; A2 u
  8. 3 S, P9 K+ y4 h% l& U% m
  9. example_lv0_t s_tItem[8] = {" f: Y- H7 l  J/ t/ f- _
  10.     {.chID = 0},! ?7 G$ ^5 ^  I$ \. h# J
  11.     {.chID = 1},
    % ]# r- F" p7 ?7 L3 W% }8 F( r
  12.     {.chID = 2},; _; w9 {+ Q  r9 d; f
  13.     {.chID = 3},
    + v* e0 ]% Q8 W0 S- Z1 E
  14.     {.chID = 4},
    1 t! s2 M3 D1 E% _6 s
  15.     {.chID = 5},9 C, c' ?% G  V, F4 Y% N' `
  16.     {.chID = 6},8 S6 Y6 D, R, S& U7 @
  17.     {.chID = 7},1 x; V7 B  ?1 N: I
  18. };
复制代码
  o, U) H4 H9 J2 K& B
我们希望实现一个函数,能通过 foreach 自动的访问数组 s_tItem 的所有成员,比如:
  1. ! I3 h$ P! Y$ O( k) U
  2. foreach(example_lv0_t, s_tItem) {
    & s% S  f- o# z
  3.     printf("Processing item with ID = %d\r\n", _.chID);
    1 b  J- }* V0 d- O
  4. }
复制代码

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

1 K. c8 N1 r9 P" P' p: n* m* ^5 q
这里的难点在于,如何定义一个局部的指针,并且它的作用范围仅仅只覆盖 foreach 的循环体。此时,坐在角落里的 __with1() 按耐不住了,高高的举起了双手——是的,它仅有的功能就是允许用户定义一个局部变量,并覆盖由第三方所编写的、由 {} 包裹的区域:* m2 X, R) R2 a0 W

  1. + z& ?6 g) t4 O7 o
  2. #define dimof(__array)          (sizeof(__array)/sizeof(__array[0]))
    1 b4 g( ]& r7 ^

  3. 2 _$ |" x. \: l. R, N) d
  4. #define foreach(__type, __array)                                               \
    ; T+ f+ m! d2 J/ \; l) b
  5.             __using1(__type *_p = __array)                                         \! |* d. ~" L0 u9 c7 ?
  6.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
    5 H+ o0 K4 M& e$ J: _; {$ g+ j! E
  7.                     CONNECT2(count,__LINE__) > 0;                               \
    : M7 P% o+ e; p  f2 ?2 f
  8.                     _p++, CONNECT2(count,__LINE__)--                            \; R7 I0 K5 A, ^* U0 \$ e
  9.                 )
复制代码

& x2 \3 U$ n. x% i$ g
上述的宏并不复杂,大家完全可以自己看懂,唯一需要强调的是,using() 的本质是一个for,因此__using1() 下方的for 实际上是位于由 __using1() 所提供的循环体内的,也就是说,这里的局部变量_p其作用域也覆盖 下面的for 循环,这就是为什么我们可以借助:: ]! [/ t& [7 }# B: W
  • #define _    (*_p)
    3 A8 U5 t* H: f; I  i+ F/ u
的巧妙代换,通过 “_” 来完成对指针“_p”的使用。为了方便大家理解,我们不妨将前面的例子代码进行宏展开:

  1. ' F- }# z+ a( p' P
  2. for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL;
    " J5 y- y! d; j; g% J9 E& @; Z
  3.      __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
    % H3 o( Y' Q- h% c. s. p
  4.      )
    ' ~* ?% w2 a9 I7 x6 n% C: q
  5.      for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
    + V, Q+ {; r. T, ]
  6.            count177 > 0; + j" `! o7 E8 p6 p8 G
  7.            _p = _p+1, count177-- )
    7 E) }7 i; ^; j: g: S! E' n
  8.      {) V8 A" g8 P1 `6 I/ L# B
  9.         printf("Processing item with ID = %d\r\n", (*_p).chID);
    * F1 H- V' Z7 w6 L9 V, A2 F  z
  10.      }
复制代码
( {6 ^* T5 @5 `. L
其执行结果为:
微信图片_20200905121142.png

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

  1. 0 B1 Z! ~# i; w) E
  2. #define foreach2(__type, __array)                                               \
    3 {  c: e# F8 V! T5 p6 f
  3.             using(__type *_p = __array)                                         \
    # ]. j  h: W  `5 ^/ [
  4.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
    ( q( i7 U4 q" P1 H0 x5 |
  5.                     CONNECT2(count,__LINE__) > 0;                               \
    / d# ]5 O# E. \; A/ D  P7 b" S7 m/ o
  6.                     _p++, CONNECT2(count,__LINE__)--                            \
    1 P- B7 S- n. |2 X0 P: l7 T+ p, R
  7.                 )* Z" l, S6 f2 m% ^. i' g
  8. 4 o( m8 x1 P) u8 i
  9. #define foreach3(__type, __array, __item)                                       \
    ! b" n- F- j" N6 U; q6 _& A
  10.             using(__type *_p = __array, *__item = _p, _p = _p, )                \3 v8 `* E+ U( Q! C# c# L
  11.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \5 ]  X  p6 S- H5 }
  12.                     CONNECT2(count,__LINE__) > 0;                               \
    $ M7 A( ]5 K1 n+ e
  13.                     _p++, __item = _p, CONNECT2(count,__LINE__)--               \4 g6 I" b3 Z) s( {% N2 U
  14.                 )
复制代码

' H( n$ j9 m- f' {5 `" N
这里的 foreach3 提供了3个参数,其中最后一个参数就是用来由用户“额外”指定新的指针的;与之相对,老版本的foreach我们称之为 foreach2,因为它只需要两个参数,只能使用"_"作为对象的指代。进一步的,我们可以使用宏的重载来简化用户的使用:
  1. 2 [- F1 f3 n% t- w2 y
  2. #define foreach(...)        \
    0 y" i4 F: M1 B$ N
  3.     CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码
0 d% g; l5 `; N" j# F4 N
经过这样的改造,我们可以用下面的方法来为我们的循环指定一个叫做"ptItem"的指针:   

  1. - U  ~4 |7 j8 I' j( Q
  2. foreach(example_lv0_t, s_tItem, ptItem) {4 e% e( ?# t8 e& A: \- b! n0 N
  3.     printf("Processing item with ID = %d\r\n", ptItem->chID);5 r: R# l  @, a- \! M
  4. }
复制代码

: j2 q& O( f; {9 p( G: k" C! D
展开后的形式如下:

  1. 9 e* P1 ?" n$ _. X/ |$ Y
  2. for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL; 3 c/ X: L! G% A/ z( Y
  3.      __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
    " C' {- ~9 r: J! B& G' ^
  4.      )
    & B) T/ g  w2 v3 v2 F$ J" ^
  5.      for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0])); 1 T7 k" {1 l' D6 ^8 r1 c
  6.            count177 > 0;
    ( C: p6 @, M  P7 l2 C
  7.            _p = _p+1, ptItem = _p, count177-- ) , [& E! g* P# \3 V' T) k
  8.      {
    , q0 n9 n  W; f# b1 I) U4 A
  9.            printf("Processing item with ID = %d\r\n", ptItem->chID);
    ! |6 }8 h) |( D  l/ |7 X% r
  10.      }
复制代码

9 ~$ {" U  G% P$ D
代码已经做了适当的展开和缩进,这里就不作进一步的分析了。( B# E/ I, V# ?; m7 o& n+ \
【后记】

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

  x! ]4 V  g2 q; h% |' @% d3 [2 m
  • 宏不是奇技淫巧
  • 宏可以封装出其它高级语言所提供的“基础设施”
  • 设计良好的宏可以提升代码的可读性,而不是破坏它
  • 设计良好的宏并不会影响调试
  • 宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using() 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险

    5 l( s! X6 w' I" @/ N  B- \
4 X% D" Q1 g  V; R1 a' \4 o- W

: k; V4 Q& k0 V
赞 收藏 评论0 发布时间:2020-9-5 12:17

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版