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

C语言中for的用法

[复制链接]
gaosmile 发布时间:2020-9-19 09:59
【说在前面的话】

通过本系列前面两篇文章的学习,我们掌握了宏的基本语法和使用规则,讽刺的是这些所谓的“基本语法和规则”却恰恰是正规C语言教育中所缺失的。本文的内容将建立在前面构筑的基础之上,以for功能的挖掘和封装为契机,手把手的教会你如何正确使用宏来简化日常开发,增强C语言的可读性、降低应用开发的难度、同时还尽可能避免宏对日常代码调试带来的负面影响。
% g' S; ~+ G& \" c: j
在开始本文的内容之前,如果你还没有阅读过前面两篇文章,可以单击下面的链接:5 Y" V* m  G# D" p
- B: W; ~9 w! t7 O( E) {1 v$ s
基础必修1:【为宏正名】本应写入教科书的“世界设定”基础必修2:【为宏正名】什么?我忘了去上“数学必修课”!
应用范例1:【为宏正名】99%人都不知道的"##"里用法
% M$ t1 a- l' K
【被低估的价值】

想必大家对C语言中的 for 循环结构并不陌生。根据C/C++语法网站cppreference.com 的介绍,for 的语法结构如下:
  1.   P$ X& q+ w" _8 u! @
  2. for ( init_clause ; cond_expression ; iteration_expression )
    . X) O$ J' D, W/ l
  3.     loop_statement
复制代码

& a7 N' w8 E! w9 o" X, W- w$ p
+ g9 U9 g" g; @这里,我并不想假设大家对 for 结构一无所知,并介绍一堆教科书上已有的内容。然而,在 for 的语法结构中有几个大家容易忽视的地方,而它们恰恰是本文后续各种“展开”的基础:$ q0 U+ O( [6 b( s

3 g; b1 K* q  h1 R' |
  • for 循环中的 cond_expression 和 interation_expression 都必须是表达式,而不能是直接的语句。: ?  P0 G7 ?7 M5 n, R! Q' m) L" ~8 y& A
  • for 循环中第一个部分 init_clause 一开始是用来放置给变量赋值的表达式;但从ANSI-C99开始,init_clause 可以被用来建立局部变量;而局部变量的生命周期覆盖且仅覆盖整个for循环——这一点非常有利用价值,也是大家容易忽略的地方。6 @; Z8 W1 m5 P  B# ]5 N; ]

    - L+ I# v+ ~, [% M

8 v$ |/ ~* f& l( ^: u+ N& t# v

为了说明这一点,我们不妨举几个例子。首先在C99标准之前,如果你要在 for 循环中使用一个循环变量,你只能在进入 for 之前将其定义好:1 _0 @3 U, [6 `( f3 L: D7 _) G  o
  1. % m* K2 m" l! h5 l* |
  2. int i = 0;
    & x8 P' Q" a4 A  b: E
  3. ...% q" @5 M4 o! J$ V( L
  4. for (i = 0; i < 100; i++) {* n, O* q4 a; H/ B' ]; ~
  5.     ...
    6 R" ^6 h, [) D6 S) v+ ~
  6. }
复制代码
2 O! i1 _9 a1 F& b' y8 ?9 M
0 c2 f' c9 ^, t- d& R8 z' O
如你所见,虽然我们可以在 init_clause 的位置对变量赋值,但它并不是必须的——多少一点鸡肋是不是?也许更鸡肋的是,你可以在 init_clause 这里完成更多的赋值操作,比如:
6 m2 r: e& ~- i/ ^( e! ?

  1. * g  O3 [. o, K. E1 |
  2. int i = 0, j,k;
    - d* f' D# z- ^0 T* h
  3. ...: v7 Y+ T' b* I9 d. w
  4. for (i = 0, j = 100, k = 1; i < 100; i++) {% W7 t. a. w) b& _
  5.     ...
    $ ~0 J  Y  _$ \! y0 ^) t. m  b, f, j
  6. }
复制代码

" i, ?: \+ y4 z. N
) B: w5 m  V  `( ?$ I实际上,明眼人都可以看出,init_clause 中所作的事情完全可以放置到 for 循环之前去完成,还可以避免“使用逗号进行分隔” 这样让人不那么习惯的使用方式。也许是意识到这一点,C99允许在 init_clause 里定义局部变量,而正是这一点,完全改变了 for 的命运(关于这一点,我们将在随后的内容中详细介绍)。现在,上述代码可以等效的改写为:8 X  s$ t3 C- v
  1. 8 q  u/ m. u/ r+ b6 E
  2. for (int i = 0, j = 100, k = 1; i < 100; i++) {7 c; I. _' V7 A$ _  f! Y
  3.     ...
    0 e7 s% f& |. X$ I9 r. L
  4. }
复制代码
" G0 J, J4 }- `3 A- B
9 Q$ ?4 \/ Q4 B% a* k0 x
需要强调的是,这里仍然有一个小小的限制,即:init_clause 里虽然可以定义局部变量,但这些变量只能是同一类型的,或者是指向这一类型的指针。因此下面的写法是非法的:0 L  e2 K+ g6 C' L6 h
  1. 1 `, B+ S+ v- t) J
  2. for (int i = 0, short j = 100; i < 100; i++) {
    2 R2 A, P( Q' D
  3.      ...$ ^+ O% o8 j, P# [2 z6 k
  4. }
复制代码
. N# V5 V/ _7 I- l9 D4 c, O  l! C

8 K; r0 _. ]0 m' q8 L  X, d而这样的写法是合法的:
4 ^# E' G% d% ?  W; c- s. f
  1. 8 d$ o8 _2 `  \: p/ k
  2. for (int i = 0, *p = NULL; i < 100; i++) {7 _! e3 L3 D9 i5 f5 K, H% C3 c" N
  3.      ...
    # H) y* t) w2 \. j
  4. }
复制代码
& ?- c' y( u  h# x2 g

9 q: D+ K7 n  N$ n请大家务必留意这里的语法细节,我们将在后面的封装中大规模使用。2 f) G$ Q% ^) _% |. W3 J3 R% Z

/ O7 d" I% M. l( m8 y
  • 另外一个值得注意的是 for 的执行顺序,它可以用下面的流程图来表示:

    5 e) Y% f' _" x$ e$ K
微信图片_20200919095356.png
容易发现,经过必要的“构造”,我们可以恰好实现一个如同 do { } while(0)一样的效果:1 D0 I( C: U8 G8 @  j
微信图片_20200919095402.png

4 ^( [4 n% X6 l  W5 w6 K
图中灰色的部分为原本实际的执行流程,而纯黑色的线条以及最下方的虚线箭头则为等效的运行流程。与do {} while(0) 相比,在我们眼中 for 循环的几个关键部分就有了新的意义:
  P/ o2 B; A, U
  • 在执行用户代码之前(灰色部分),有能力进行一定的“准备工作”(Before部分);
  • 在执行用户代码之后,有能力执行一定的“收尾工作”(After部分)
  • 在init_clause阶段有能力定义一个“仅仅只覆盖” for 循环的,并且只对 User Code可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的地方的。% y/ n5 R8 V5 K) b, l2 J

    3 F, v- [$ j$ m% x1 j
微信图片_20200919095405.png
3 g5 ^' w" U( ]6 N/ b- E
【构造using结构】

上面所提到的结构,在C#中有一个类似的语法,叫做 using(),其典型的用法如下:
9 z- f7 l$ D' `0 w# d0 x
  1. ( H5 F% P) s% C7 ~) ?# W
  2. using (StreamReader tReader = File.OpenText(m_InputTextFilePath)); H8 v9 L% j7 Q# @
  3. {. w. L" ?1 v+ b8 P! }! J2 E& R
  4.     while (!tReader.EndOfStream)
    9 r, M' f- X, U. Z& ]
  5.     {
    ; H; k1 P# u1 M& L% A: W
  6.         ...6 L' U' U5 _! K# A  s/ i: G
  7.     }+ }0 g0 d! K2 L9 v
  8. }
复制代码
3 N; A% D' k9 X7 Z, L) a
以上述代码为例进行讲解:8 _4 f: j; d) g/ w1 q
  • 在 using 圆括号内定义的变量,其生命周期仅覆盖 using 紧随其后的花括号内部;
  • 当用于代码离开 using 结构的时候,using 会自动执行一个“扫尾工作”,而这个扫尾工作是对应的类事先定义好的。在上述例子中,所谓的扫尾工作就是关闭 与 类StreamReader的实例tReader 所关联的文件——简单说就是using会自动把文件关闭,而不必用户亲自动手。
    # {: L9 `$ T" G" }
0 c, y, v7 C  x0 _
是不是闻到了熟悉的味道?不要搞错因果关系——我们正是对C#中的using结构“甚是眼馋”才决定自己动手,用 for 来创造一个——现有C#的using结构才有我们后面的尝试。下图是using所等校流程图,可以看到他比我们此前的结构还少了一个“Before”部分:
; c/ l% N- X7 n. x: _: Y
微信图片_20200919095408.png
要实现类似using的结构,首先要考虑如何构造一个"至执行一次"的for循环结构。要做到这一点,毫无难度:  u5 p2 u2 a* m0 z

  1. " b; C9 T; K6 _9 m
  2. for (int i = 1; i > 0; i++) {
    9 ]$ d) n7 A$ ~6 k
  3.     ...
    * g* s& j" N3 d
  4. }
复制代码

. i/ P* p* D$ N
以此为起点,对比我们的“蓝图”,发现至少有以下几个问题:* ~1 U$ \7 l* n5 g* ^! b2 d8 @% X

+ [; g3 a/ g7 y/ \$ S2 i: A3 j
  • 如何实现 before和after的部分?
  • 现在用的变量 i 固定是 int 类型的,如何允许用户在 init_clause 定义自己的局部变量,并允许使用自己的类型?

    ( k/ i( R9 q# l" N9 j- X0 u
1 q& }. C- ?' x9 p, c1 I, @  a+ M0 o' ^
问题一:如何实现 before 和 after 部分! O) a8 ~8 Z0 a$ h" d
对比前面的图例,我们知道 before 和 after 的部分实际上分别对应 for 循环的 cond_expression 和 iteration_expression;同时,这两个部分都必须是表达式——由于表达式的限制,能插入在 before 和 after 部分的内容实际上就只能是“普通表达式”或者是“函数”。由于我们还必须至少借助 cond_expression 来实现 “只运行一次” 的功能,如何见缝插针的实现 before 的功能呢?不绕弯子,看代码:8 z) M! t; b5 e, h6 u% D, k7 \

  1. ' E4 _! A+ L7 L' c* P$ l
  2. //! 假设用户要插入的内容我们都放在叫做 before 和after的函数里
    % x7 m. J7 P+ r/ F2 h* `% w
  3. extern void before(void);( m; p1 K/ j; p* }3 p: z& C
  4. extern void after(void);
    ; {7 P% B7 d  F+ d. F

  5. 9 [6 m1 Y- m* q0 F2 s- `
  6. for (int i = 1;           //!< init_clause; }8 P) ^7 C# q1 U! q1 @
  7.      i--?(before(),1):0;  //!< cond_expression0 f7 Z# r0 ^3 q' E2 B
  8.      after())             //!< iteration_expression- W, L) N7 o+ k5 u1 B, o
  9. {
    ; f  G6 B$ U  x# Z" C
  10.      ...3 w% ?7 g1 b6 n; S8 i& s
  11. }
复制代码
% H: Q3 q$ b0 n, S' T( O
; _1 z6 w6 B* s
我们知道,cond_expression 只在乎用户表达式的返回值是0还是非0,因此,这里其实真正起作用的本体是 "i--"——第一次判断的时候返回值是1,由于自减操作,第二次判断的时候就是0了——这就完成了让 for 运行且只运行一次的功能。接下来,我们借助一个问好表达式,尝试给 i-- 的结果做一个等效“解释”,即:
0 Q3 n+ G, w- n  S8 y% c$ k! f
  1. . o- I6 r; r- f: ~' K) X8 z/ B1 O
  2. (i--) ? 1 : 0
复制代码
( Z9 f! c7 y( A+ n6 e

2 o8 m( `2 r$ ]4 p用人话说就是,如果 (i--)值是非0的,我们就返回1,反之返回0。这么做的意义是为了进一步通过逗号表达式对 "1" 所在的部分进行扩展:0 y1 R. `/ L9 n* M7 {
  1. , h$ F0 I+ i4 @' Q' m  f# U) _  m
  2. (i--) ?
    ! _" A$ a4 G1 b! W+ U5 g# V3 M+ c
  3.       (before(), 1)       //!< 使用逗哈表达式进行扩展
      ~' S( W  G" k' B9 N" l0 b
  4. :     0
复制代码
5 P. J  i  M8 B( m4 g6 ?' V2 |8 d
由于逗号表达式只管 最右边的结果,忽略所有左边的返回值,因此,哪怕before()函数没有实际返回值对C编译器来说都是无所谓的。同理,由于我们在cond_expression部分已经完成了所有功能,因此 iteration_expression 就任由我们宰割了——编译器原本就对此处表达式所产生的数值并不感兴——我们直接放下 after() 函数即可。
, {; V! S- G6 G) l
至此,插入 before() 和 after() 的问题圆满解决。
8 w. }/ R% C/ z
8 _- O  s3 Y1 b. m7 y
问题二:如何允许用户定义自己的局部变量,并且拥有自己的类型要解决这个问题,首先必须打破定势思维,即:for循环只能用整型变量。实际并非如此,对for来说真正起作用的只有  cond_expression 的返回值,而它只关心用户的表达式返回的 布尔量 是什么——换句话说,有无数种方法来产生 cond_expression,而使用普通的整形计数器,并对其进行判断只是众多方法中的一种。+ r2 S1 W7 j9 b$ U1 [) P( g  k
打破了这一定势思维后,我们就从问题本身出发考虑:允许用户用自己的类型定义自己的变量——虽然看似我们并不能知道用户会用什么类型来定义变量,因而就无法写出通用的 cond_expression 来实现“让for执行且执行一次”的功能,然而,你们也许忘记了 init_clause 的一个特点:它还可以定义指针——换句话说,无论用户定义了什么类型,我们都可以在最后定义一个指向该类型的指针:' T9 W9 A; z1 v

  1. ) _' `; ~6 X/ F! `9 Z  R
  2. #define using(__declare, __on_enter_expr, __on_leave_expr)   \# i5 _) c$ a5 S+ e* I7 t$ ~
  3.             for (__declare, *_ptr = NULL;                    \
    0 H! `, o. W) Q7 ^8 W+ L; M- u# z
  4.                  _ptr++ == NULL ?                            \$ ^, ?$ ^$ S9 f
  5.                     ((__on_enter_expr),1) : 0;               \% K0 X; _1 c- ^
  6.                  __on_leave_expr                             \
    " X: v/ [  `$ ^$ b7 h6 u) m7 ]
  7.                 ); W! |' E, p8 T6 }6 Z
复制代码
9 k  J) l; U! \6 @
为了验证我们的结果,不妨写一个简单的代码:1 ?3 g& H/ L0 L1 s" s) u
  1. 5 j4 Y% ~2 @' @( y9 J1 Y4 q
  2. using(int a = 0,printf("========= On Enter =======\r\n"),
    ; t$ u4 ~- b4 i& Q
  3.                 printf("========= On Leave =======\r\n")) ; t. a" s+ t0 a; V' _7 m! M' r8 h" w! S7 U
  4. {
    / t) q$ h7 |, l6 l' m
  5.     printf("\t In Body a=%d \r\n", ++a);
    - k8 G  [. D5 v
  6. }
复制代码

6 x8 o) o: @9 d8 y$ l7 h
这是对应的执行效果:
微信图片_20200919095412.png

% Y# q, N% r' |: w1 B( B
我们不妨将上述的宏进行展开,一个可能的结果是:

  1. ! S2 _- O! O( ?: t1 [; w/ N6 ^9 R8 ]$ E
  2. for (int a = 0, *_ptr = NULL; # B) e; M: L% o# q/ f
  3.      _ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0; 1 V" q0 Y/ P5 f. |5 ^
  4.      printf("========= On Leave =======\r\n") )
      m* `' g1 }. T8 F; Z
  5. {: `# V# _  E, F1 ~$ K" L
  6.     printf("\t In Body a=%d \r\n", ++a);) H- x1 h9 Y0 [& W0 O/ I' y
  7. }
复制代码
从 init_clause 的展开结果来看,完全符合要求:

  1. 5 |5 W3 y0 N! I, i4 r' m! z
  2. int a = 0, *_ptr = NULL;
复制代码

2 ^4 Y4 M7 O& F0 b. G! ^( I1 Y
接下来,为了提高宏的鲁棒性,我们可以继续做一些改良,比如给指针一个唯一的名字:
. G+ ^1 O$ ?. t. s- H, }

  1. 4 D; x( L9 x2 z" `; t, |" I" V
  2. #define using(__declare, __on_enter_expr, __on_leave_expr)                   \
    2 a+ l; N/ C( m9 G
  3.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    : I9 z; _  y9 ^
  4.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \% |! J) n7 t7 A. c8 C2 N
  5.                     ((__on_enter_expr),1) : 0;                                  \$ A, ], n( t6 W1 |
  6.                  __on_leave_expr                                                \# b3 ]! a7 @6 d) G& l0 k
  7.                 )+ ]) }+ w+ z) f8 G, T
复制代码

- Y% ^7 c/ g; u. a" k7 m: @
这里,实际上是使用了前面文章中介绍的宏 CONNECT3() 将 “__using_”,__LINE__所表示的当前行号,以及 "_ptr" 粘连在一起,形成一个唯一的局部变量名:0 ~) x" G- W4 [0 l" z0 U
  1. * h& Y0 c" \% d: b; I
  2. CONNECT3(__using_, __LINE__,_ptr)
复制代码

+ Z3 l! _; I7 S8 {
如果你对 CONNECT() 宏的来龙去脉感兴趣,可以单击这里。8 d6 f2 m' ^6 ^  P' G3 w: S

: a3 t$ X' k/ V6 y6 _
更进一步,如果用户有不同的需求:比如想定义两个以上的局部变量,或是想省确 __on_enter_expr 或者是 __on_leave_expr ——我们完全可以定义多个不同版本的 using:8 {+ ?, r' Q2 z& p4 D7 t

  1. - ?7 r( n" @  ?' e2 k8 o( T. D
  2. #define __using1(__declare)                                                     \4 G$ a  ~5 `9 {1 }5 l
  3.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    7 }: ^4 a, ~/ @! k1 c* K2 ^
  4.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \+ M' J0 X8 t) W: J" H2 R4 E
  5.                 )% a; }0 E; D0 {

  6. 8 L2 R( t6 r# V. O, s

  7. $ u( H! [0 _5 j' o5 |$ b
  8. #define __using2(__declare, __on_leave_expr)                                    \" e8 Q8 k9 N8 c& I& ^
  9.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
      h& }8 l; b2 ^2 U) X5 M
  10.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \3 u1 q0 W3 t! \4 n* {6 w
  11.                  __on_leave_expr                                                \1 o$ M) O% M$ m; F% ?6 Z1 u' p
  12.                 )! J/ G3 S; M% S5 A) Z+ {  A
  13. 8 ~1 o: u/ x; q  r4 A
  14. " }% B" ^7 J7 L; o7 T( v1 x
  15. #define __using3(__declare, __on_enter_expr, __on_leave_expr)                   \8 i  S2 F: N/ J4 I$ W+ W- ]! K# c7 l
  16.             for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
    ' ~; ^2 g- p! j& w
  17.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \( X) b, K) P% @8 m
  18.                     ((__on_enter_expr),1) : 0;                                  \5 X+ a$ H3 W7 ]4 e5 G
  19.                  __on_leave_expr                                                \* ~) \6 l2 j8 p. b( l3 Z# H  ^
  20.                 )- Y5 ?( Y; O- E% [9 q

  21. . C" s7 p8 w0 ?, L+ `
  22. #define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr)              \
    8 o1 U! v7 z, n( \, J4 r) p8 N8 o5 c
  23.             for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL;     \
    % Q/ M* l, f( b; {  n
  24.                  CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \
    6 `0 X5 X0 }: d! T' U7 [2 ]7 W9 y
  25.                     ((__on_enter_expr),1) : 0;                                  \$ i, j7 F- z8 a# h' D
  26.                  __on_leave_expr                                                \
    7 Q% W  a6 R+ ~% l
  27.                 )+ A0 H2 @7 H- ^+ [# |
复制代码

+ m& i$ o7 S5 _$ Z
借助宏的重载技术,我们可以根据用户输入的参数数量自动选择正确的版本:- T) c& b; _" U$ G! C! n( b7 B

  1.   {: T, u0 d8 e8 g
  2. #define using(...)  \; P& ^0 b4 e& h$ d/ r
  3.     CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码

9 {" j  J8 }. I3 M6 E
至此,我们完成了对 for 的改造,并提出了__using1, __using2, __using3 和 __using4 四个版本变体。那么问题来了,他们分别有什么用处呢?
. J7 [0 u2 ]6 f' o9 j% M
【提供不阻碍调试的代码封装】

前面的文章中,我们曾有意无意的提供过一个实现原子操作的封装:即在代码的开始阶段关闭全局中断并记录此前的中断状态;执行用户代码后,恢复关闭中断前的状态。其代码如下:
1 K% Q: k% \- C6 A5 L
  1. 8 \2 k5 X9 [; a, a, E0 I8 ]9 p6 h
  2. #define SAFE_ATOM_CODE(...)                               \
    2 s% V  b3 F0 v0 `8 Y% v
  3. {                                                         \. R4 _% M+ r7 s" Y9 k
  4.     uint32_t CONNECT2(temp, __LINE__) = __disable_irq();  \
    ; J! F+ i. U9 Q# g
  5.     __VA_ARGS__                                           \7 G* o+ r1 ?# n0 U- l
  6.     __set_PRIMASK((CONNECT2(temp, __LINE__)));            \# ]8 J- H( {, y( R8 |% w2 O
  7. }
复制代码

- j4 Y) m+ c6 {0 g7 i1 o- B
因此可以很容易的通过如下的代码来保护关键的寄存器操作:
  `$ Z( i7 ~1 O  ^- l$ ?

  1. - B" M8 i$ ]( j2 T- F' y! s
  2. /**
    . j/ i0 N* }* T- t; H2 y7 |
  3.   \fn          void wr_dat (uint16_t dat)3 C. w, u8 ?0 S* ]& a
  4.   \brief       Write data to the LCD controller4 Q* \" P. |( Z( W
  5.   \param[in]   dat  Data to write/ X1 [! x  G: B5 t! T4 t  b0 p
  6. */
    ( l2 F; m. k) P
  7. static __inline void wr_dat (uint_fast16_t dat) / Z: t. x; R) d0 u
  8. {/ F/ L# c+ i3 ^& N- h9 u* A* a  F
  9.     SAFE_ATOM_CODE (2 r  K& x, C$ W5 _  D+ }
  10.         LCD_CS(0);8 v* l3 Q0 ~; W* `9 b
  11.         GLCD_PORT->DAT = (dat >>   8);   /* Write D8..D15 */
    3 R& ]* c+ V/ R
  12.         GLCD_PORT->DAT = (dat & 0xFF);   /* Write D0..D7 */, H9 [: L1 T! |: a( n. g
  13.         LCD_CS(1);7 }1 u. n9 u# C/ h; T' r4 {
  14.     )" @# {( g: n# \. ~4 O" c% x
  15. }
复制代码

3 F+ h! K% B8 l7 ?6 M; A2 I
唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:- |( o  ~8 u& w+ `8 {. q
  1. 8 N/ J. T0 o& U9 ^4 D; [1 t. X
  2. #define SAFE_ATOM_CODE()                                                        \
      R. i; l- k" g! I- a
  3.             __using2(  uint32_t CONNECT2(temp,__LINE__) = __disable_irq(),      \# t  s$ T7 F! c: t0 F; x
  4.                     __set_PRIMASK(CONNECT2(temp,__LINE__)))
复制代码

$ Y( Q" o& {1 |- _7 O7 j
微信图片_20200919095416.png
修改上述的代码为:7 B/ U2 c$ y  {9 J6 |. R( E

  1. " a- R% n. P: q9 H* w
  2. static __inline void wr_dat (uint_fast16_t dat)
    8 B' h& N+ ~" O# W
  3. {- @" a: Y$ ~" D
  4.     SAFE_ATOM_CODE() {
    * @* A- b' ~' M& {
  5.         LCD_CS(0);
    4 r* H: Z4 I! B5 t  W/ O) v
  6.         GLCD_PORT->DAT = (dat >>   8);   /* Write D8..D15 */
    + l! o7 t9 u+ f/ r5 _! \& ^1 W! `% j
  7.         GLCD_PORT->DAT = (dat & 0xFF);   /* Write D0..D7 */
    : k) l) U0 i$ m0 b. f+ ]
  8.         LCD_CS(1);. ]/ j2 U, r7 ]+ ~) D' q/ S
  9.     }
    . `( D8 z; [/ G3 H7 L7 G% Z
  10. }
复制代码

! u  X: a6 o& m* {2 m! H
由于using的本质是 for 循环,因为我们可以通过花括号的形式来包裹用户代码,因此,可以很方便的在用户代码中添加断点,单步执行。至于原子保护的功能,我们不妨将上述代码进行宏展开:
% M$ e. V6 c; N$ V

  1. 4 D2 z9 {9 g: M' [2 B8 d
  2. static __inline void wr_dat (uint_fast16_t dat)& m, x/ X: f& `- `( c
  3. {4 ]- F/ c* ^6 `% b, a; a" s, O
  4.     for (uint32_t temp154 = __disable_irq(), *__using_154_ptr = NULL;
    , _- A$ w1 _+ \0 y
  5.          __using_154_ptr++ == NULL ? ((temp154 = temp154),1) : 0; 5 o! M! Q9 R4 D  C6 `  g; f* \
  6.          __set_PRIMASK(temp154) )
    ' T' j4 _! G  G- f! U
  7.     {0 X" X9 A7 j# @& _+ |# j" H
  8.         LCD_CS(0);% U2 D  Q% k! S5 [" g
  9.         GLCD_PORT->DAT = (dat >> 8);2 ?) u" X  {4 j3 R- t" j
  10.         GLCD_PORT->DAT = (dat & 0xFF);  O  Q! D/ F) i, O( r5 M" f' e
  11.         LCD_CS(1);1 U3 K: h( n( J; o1 O
  12.     }
    ; J* T- a. `7 A, m
  13. }
复制代码
* T% t9 ^' |& v
通过观察,容易发现,这里巧妙使用 init_clause 给 temp154 变量进行赋值——在关闭中断的同时保存了此前的状态;并在原本 after 的位置放置了 恢复中断的语句 __set_PRIMASK(temp154)。
* x+ s7 s) ~: x8 ~# Y, M
# N: M& ~3 S1 E- M( Z0 h3 M
举一反三,此类方法除了用来开关中断以外,还可以用在以下的场合:! e' V8 L0 z) l% }* M. Q
  • 在OOPC中自动创建类,并使用 before 部分来执行构造函数;在 after 部分完成 类的析构。
  • 在外设操作中,在 init_clause 部分定义指向外设的指针;在 before部分 Enable或者Open外设;在after部分Disable或者Close外设。
  • 在RTOS中,在 before 部分尝试进入临界区;在 after 部分释放临界区
  • 在文件操作中,在 init_clause 部分尝试打开文件,并获得句柄;在 after 部分自动 close 文件句柄。
  • 在有MPU进行内存保护的场合,在 before 部分,重新配置MPU获取目标地址的访问权限;在 after部分再次配置MPU,关闭对目标地址范围的访问权限。
  • ……
    ! ~( ]( X8 g. N5 s; J

    ! u" }* O' x9 k* D1 x9 C) t: n; {

. X! Z- C9 b" }( i
' |" }' u% m# ~  N1 L% U6 j
【构造with块】! ]- M+ {  V4 {; z4 n' O

不知道你们在实际应用中有没有遇到一连串指针访问的情形——说起来就好比是:  G# o( m9 |& h5 `
  1. 4 L4 R+ B) i, g
  2. 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机
复制代码
, D3 h* B( t) d! H* b

) E8 }/ Z0 s' [3 U8 s' L0 p8 B" a如果我们要操作这里的“手机”,实在是不想每次都写这么一长串“恶心”的东西,为了应对这一问题,Visual Basic(其实最早是Quick Basic)引入了一个叫做 WITH 块的概念,它的用法如下:5 r* O7 Z, b9 |6 p# K
  1. ) [+ r/ C: Q" e2 M; n1 W
  2. WITH 你邻居的->朋友的->亲戚家的->一个狗的->保姆的->手机 " o" P5 s% a* Y) o: Q0 D$ ~/ I
  3.     # 这里可以直接访问手机的各项属性,用 “.” 开头就行
    1 u5 e3 ]: e, V4 M
  4.     . 手机壳颜色 = xxxxx0 q+ f( y; V* S- F4 |# ]
  5.     . 贴膜 = 玻璃膜$ @3 T! v  }9 P) J6 W# J
  6. END WITH
复制代码
( {7 B, h. ^- f5 [- y# {. `0 n
不光是Visual Basic,我们使用C语言进行大规模的应用开发时,或多或少也会遇到同样的情况,比如,配置 STM32 外设时,填写外设配置结构体的时候,每一行都要重新写一遍结构体变量的名字,也是在是很繁琐:
( {. W0 K& @( x  p) t
  1. . d; N! r: C+ c% F
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
    9 Z$ a0 a2 D$ b1 C8 j, D# f
  3. s_UARTHandle.Instance        = USART2;' E+ C  X( p& |/ ~
  4. s_UARTHandle.Init.BaudRate   = 115200;& R0 |& f1 Z) X8 \0 ?. d2 ~/ M) B
  5. s_UARTHandle.Init.WordLength = UART_WORDLENGTH_8B;
    + k) E- b+ {2 e5 L5 D1 u" |- n, P
  6. s_UARTHandle.Init.StopBits   = UART_STOPBITS_1;/ j3 ]; Q0 c4 W8 h& I6 u$ t
  7. s_UARTHandle.Init.Parity     = UART_PARITY_NONE;9 Q9 X* k  v& k
  8. s_UARTHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;3 D& t: \7 n* C& P; x1 ^
  9. s_UARTHandle.Init.Mode       = UART_MODE_TX_RX;
复制代码
" M( }- H3 G3 g( j
入股有了with块的帮助,上述代码可能就会变得更加清爽,比如:

  1. " [$ }3 U6 ]8 t6 e3 Q! H
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
    % ?4 v) B  l4 [' B8 q1 e1 B5 F! e
  3. with(s_UARTHandle) {0 S9 D- ^/ t- ^, D) Y' f
  4.     .Instance        = USART2;
    $ n+ t. k$ f# o2 C
  5.     .Init.BaudRate   = 115200;
    - R) k9 m( Y7 Q+ x* ~. [  e6 ^" U
  6.     .Init.WordLength = UART_WORDLENGTH_8B;
    7 e" J0 _0 x, O) a3 ^# a3 F1 b
  7.     .Init.StopBits   = UART_STOPBITS_1;( U' u& D: V5 F# B1 @; ?9 j
  8.     .Init.Parity     = UART_PARITY_NONE;# L* M2 J* n6 o1 D
  9.     .Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    ) _' n: t% O7 N2 U5 ~0 R7 G
  10.     .Init.Mode       = UART_MODE_TX_RX;* V$ U8 ]6 w, e5 [
  11. }
复制代码

" _1 s" Y# N/ U4 D9 [
遗憾的是,如果要完全实现上述的结构,在C语言中是不可能的,但借助我们的 using() 结构,我们可以做到一定程度的模拟:  L+ u/ ?, a+ I( Q% w
  1. 0 s% Y% I! z9 {" R: ?1 ]& e' f
  2. #define with(__type, __addr)  using(__type *_p=(__addr)). i) }1 T, b. U) `
  3. #define _ (*_p)
复制代码

3 ^9 M; X  O7 D5 `2 g  }' J
在这里,我们要至少提供目标对象的类型,以及目标对象的地址:: U4 @" s' b# Q' _5 I

  1. . z  c- O. C: \3 l& z% A
  2. static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();; d7 b. O4 W  ~6 L
  3. with(UART_HandleTypeDef &s_UARTHandle) {# y4 X; }3 [$ o4 R1 Y! W7 A
  4.     _.Instance        = USART2;) l6 ^: N8 B) W  O  }. k. M
  5.     _.Init.BaudRate   = 115200;, I- u3 ~* J. h1 q. ^% X8 R
  6.     _.Init.WordLength = UART_WORDLENGTH_8B;7 m0 X0 z/ S8 _0 M
  7.     _.Init.StopBits   = UART_STOPBITS_1;8 d& T$ ^4 Z1 ]- ?0 D/ G3 @
  8.     _.Init.Parity     = UART_PARITY_NONE;
    & E: g- B3 e" ~
  9.     _.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    9 `! r( ]. X: f3 K! [8 m
  10.     _.Init.Mode       = UART_MODE_TX_RX;. T3 S% n2 y3 t5 Q
  11. }
复制代码

1 c1 M1 O7 _$ T
注意到,这里“_”实际上被用来替代 s_UARTHandle——虽然感觉有点不够完美,但考虑到脚本语言 perl 有长期使用 "_" 表示本地对象的传统,这样一看,似乎"_" 就是一个对 "perl" 的完美致敬了。
0 d0 h) Z% y. Y" d) w) B3 G

" }: w- ?8 P: X: N# d  C
【回归本职 foreach】

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

  1. 8 W* o8 [4 S% E" F4 e) c
  2. typedef struct example_lv0_t {
    % x/ [" `  f0 |. T! @, J& f
  3.     uint32_t    wA;' d1 ^# \0 ^8 P) {
  4.     uint16_t    hwB;
    2 }' r2 G# }$ a' l& a
  5.     uint8_t     chC;/ o6 \# S3 B" \" v2 S6 R( f
  6.     uint8_t     chID;+ s0 b8 s+ S% B- R1 D
  7. } example_lv0_t;4 F* p% W. p! r( ^2 g6 E

  8. ! |4 ?) O% D- W% p# \) O
  9. example_lv0_t s_tItem[8] = {& g# n0 G. p2 b& Z6 T0 Q5 Z  g
  10.     {.chID = 0},
    & k# F, {( C% ]4 F, p! q& L- e. ^
  11.     {.chID = 1},
    ( I9 @0 Q% g+ i# ]2 D
  12.     {.chID = 2},$ [0 v/ U5 z* e. j9 R' h
  13.     {.chID = 3},, [, _- c) ~: f3 f5 v( w
  14.     {.chID = 4},
    . [' n: k6 X* u$ S% ?
  15.     {.chID = 5},% G+ Z: |  Z  f$ q# T; K+ C+ p/ Q* H
  16.     {.chID = 6},+ S1 S/ B; [# b+ i& @! Z# {0 A! U
  17.     {.chID = 7},% B+ p; J9 y7 ]
  18. };
复制代码
4 A: q( ^' x( c2 ]$ ^
我们希望实现一个函数,能通过 foreach 自动的访问数组 s_tItem 的所有成员,比如:

  1. ! K4 H, r% j. i! }& e$ U2 j
  2. foreach(example_lv0_t, s_tItem) {$ i7 P( n" j) [1 G9 E
  3.     printf("Processing item with ID = %d\r\n", _.chID);
    % J/ P1 T8 Y" M6 F
  4. }
复制代码

# S% Q  o' _1 {
跟With块一样,这里我们仍然“致敬” perl——使用 "_" 表示当前循环下的元素。在这个例子中,为了使用 foreach,我们需要提供至少两个信息:目标数组元素的类型(example_lv0_t)和目标数组(s_tItem)。
+ j- o* \# N9 P$ ^4 y9 g
) ?* c7 o! L/ f
这里的难点在于,如何定义一个局部的指针,并且它的作用范围仅仅只覆盖 foreach 的循环体。此时,坐在角落里的 __with1() 按耐不住了,高高的举起了双手——是的,它仅有的功能就是允许用户定义一个局部变量,并覆盖由第三方所编写的、由 {} 包裹的区域:
( T' F, ]  C9 q% l  o2 n: X5 o

  1. 2 g! o* X) d4 ~) W( A
  2. #define dimof(__array)          (sizeof(__array)/sizeof(__array[0]))
    2 ^1 |3 a; f3 j* z
  3. 6 W) e& \+ d" q% L# A; c: F& m' P7 J. q
  4. #define foreach(__type, __array)                                               \
      |" k3 I3 Y" l3 R
  5.             __using1(__type *_p = __array)                                         \
    + y8 {: v0 q3 M1 t$ d- j: ^# q3 j
  6.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \" p# f3 g7 j! I; H+ I" l; i) L2 [
  7.                     CONNECT2(count,__LINE__) > 0;                               \/ x0 ?8 O6 M( S, ?
  8.                     _p++, CONNECT2(count,__LINE__)--                            \
    4 U- {7 a( a" V$ H- k: P( `
  9.                 )
复制代码

( `% Z2 s$ F  }" r
上述的宏并不复杂,大家完全可以自己看懂,唯一需要强调的是,using() 的本质是一个for,因此__using1() 下方的for 实际上是位于由 __using1() 所提供的循环体内的,也就是说,这里的局部变量_p其作用域也覆盖 下面的for 循环,这就是为什么我们可以借助:( l+ V: Q( j; ]) Y% F
  1. . |2 J0 z$ P8 a
  2. #define _    (*_p)
复制代码
6 M9 U) E3 h1 H) {
的巧妙代换,通过 “_” 来完成对指针“_p”的使用。为了方便大家理解,我们不妨将前面的例子代码进行宏展开:

  1. % l5 u% J: J7 {3 S
  2. for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL; " j. \/ N; _/ l# u
  3.      __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;% L; u9 d, H9 R3 e
  4.      )
    ! ^5 E; O$ ?( s# @9 \2 N
  5.      for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
    $ Q( l) u% |% I/ C
  6.            count177 > 0; ' A5 t1 k7 e+ l# k8 H" W* K
  7.            _p = _p+1, count177-- )
    7 u, r0 b9 g* g' |
  8.      {
    ' n5 ]/ Y9 ?: @; t
  9.         printf("Processing item with ID = %d\r\n", (*_p).chID);) _& c" ~' D( N, j# n8 S1 }: ~3 N
  10.      }
复制代码

% L( P- Q2 k6 u! l* l
其执行结果为:
微信图片_20200919095418.png

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

  1. * H! A0 U1 r8 V) P- ^3 e1 C+ y
  2. #define foreach2(__type, __array)                                               \
    8 H5 a/ z7 j6 N  r1 f
  3.             using(__type *_p = __array)                                         \: l1 Q( s1 l3 Z7 @" c; J# f
  4.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \6 V$ m; |' h; ]6 s( x8 B5 ~
  5.                     CONNECT2(count,__LINE__) > 0;                               \
    % M9 K! T9 s: A
  6.                     _p++, CONNECT2(count,__LINE__)--                            \$ @3 h5 x+ W. a* S: h
  7.                 )
    - ]  }( q4 q9 L- K0 ~

  8. 8 m/ p7 L/ t! D; x2 T  m1 t; h
  9. #define foreach3(__type, __array, __item)                                       \) Q' F8 ]7 B  T. z- K3 ?) J
  10.             using(__type *_p = __array, *__item = _p, _p = _p, )                \3 ?3 _$ O( A4 S4 w5 X8 X
  11.             for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
    : d3 `! B# t/ B4 B
  12.                     CONNECT2(count,__LINE__) > 0;                               \
    ; n, a1 g$ z3 j' A% ~! C. R
  13.                     _p++, __item = _p, CONNECT2(count,__LINE__)--               \
    6 i; W2 |. V* ~
  14.                 )
复制代码
* s& g+ {6 M4 \6 M2 m) D
这里的 foreach3 提供了3个参数,其中最后一个参数就是用来由用户“额外”指定新的指针的;与之相对,老版本的foreach我们称之为 foreach2,因为它只需要两个参数,只能使用"_"作为对象的指代。进一步的,我们可以使用宏的重载来简化用户的使用:
  1. ' |8 E4 B+ y- @7 M& c% g
  2. #define foreach(...)        \9 m3 C: I# B7 L# Z  E
  3.     CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
复制代码
) Z* b; y& L4 r7 \* }/ x/ ?, A
经过这样的改造,我们可以用下面的方法来为我们的循环指定一个叫做"ptItem"的指针:   

  1. 0 @# m4 E/ p2 l, G# L
  2. foreach(example_lv0_t, s_tItem, ptItem) {
    , Z" D1 _# e' D+ O
  3.     printf("Processing item with ID = %d\r\n", ptItem->chID);
    * `' M. I1 b; B
  4. }
复制代码

- F$ H/ Z# \/ H/ }  h
展开后的形式如下:

  1. ; {% c9 v3 u$ f3 Q1 V& c; k
  2. for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL;
    4 b* E3 D7 S8 F' P4 O$ P
  3.      __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
    + m+ U6 y+ ~6 ?
  4.      )8 Q7 R0 U* u' F; E
  5.      for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
    5 T& x" p5 @2 _- N* c
  6.            count177 > 0;
    6 b3 f  n) j7 Z. f9 [3 L7 g
  7.            _p = _p+1, ptItem = _p, count177-- )
    0 V9 r! \# ~* N
  8.      {
    / y/ a9 v, u8 Q( z% a1 q
  9.            printf("Processing item with ID = %d\r\n", ptItem->chID);
    ' M6 z7 ^+ Z5 y
  10.      }
复制代码

6 F4 i" I8 j. h1 H5 ^* M/ X
代码已经做了适当的展开和缩进,这里就不作进一步的分析了。$ v* t% f3 Z0 M1 y  f* M# N7 U
【后记】

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

  ^$ b2 z3 ~6 g( |8 b4 f) B
  • 宏不是奇技淫巧
  • 宏可以封装出其它高级语言所提供的“基础设施”
  • 设计良好的宏可以提升代码的可读性,而不是破坏它
  • 设计良好的宏并不会影响调试
  • 宏可以用来固化某些模板,避免每次都重新编写复杂的语法结构,在这里,using() 模板的出现,避免了我们每次都重复通过原始的 for 语句来构造所需的语法结构,极大的避免了重复劳动,以及由重复劳动所带来的出错风险

    + ~- Z! {- e. k9 q; ]- ^9 D
+ M* i& N' F! g  M: h' g
% w: c- t! j7 D! H7 P) H  f
赞 收藏 评论0 发布时间:2020-9-19 09:59

举报

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