找回密码
 注册
关于网站域名变更的通知
查看: 351|回复: 1
打印 上一主题 下一主题

Linux下gcc预编译#if,#elif,#endif以及常用宏定义

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2021-2-9 14:20 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x

8 ^  Y7 ^% R0 ]5 g, L' ]& y3 d  t#if语句会计算它后面的表达式,并检查表达式的结果,如果结果为真(true),就编译后面的语句,如果为假(false),就不编译后面的语句。2 n( c" O9 H% Y8 h5 C8 P( y

  Y* f& l- _, v例如:
1 f4 X% v5 V% f2 ?* h2 c: F% e
! z/ {' I3 T) J#if COUNT5 ^  q$ v1 o- `# B) u
char *desc = "The count is non-zero";
8 n* K2 \% z2 N, p1 g# ?#endif6 [+ j( f: Z0 M, U% Z+ e4 E
% _9 b0 b+ W$ g
0 U7 V4 Y# v( e6 P. w
只有COUNT不为0的情况下才会编译& G  O; B. T, o3 k& q
9 O7 A3 D! c# k7 C- W
char *desc = "The count is non-zero";' G' k' M  @7 }1 T5 e
! [( u+ o+ }8 V  s7 _& |
语句。
7 C2 w7 D! H6 a0 m8 ?* u4 A7 t/ `/ A1 u; X; @( @% Q! ~
下面是#if后面的表达式的一些语法规则:* l" @' b) }' }/ Y0 p) D3 v

! `/ @9 ?- ^( I3 Z# B1)表达式可以包括整形常量和宏,这个宏必须是被赋值过(区别被定义过): P% |1 C) K% e

4 s. J6 Z" ?6 P) |" S/ H9 x2)可以使用圆括号来指定表达式的顺序。
) B* e' Y. s' E7 ^+ }" |- P8 u1 m" Y; h+ J3 s
3)表达式中可以包含C语言中的+,-,*,/,<<和>>算术运行符。这些算术运算使用当前平台的整型的最大表示的值来运算,一般为64位的整型值。
! A$ U2 v4 @/ Z: o6 N4 }3 L5 r" Y$ I* y8 s/ u) d) K' \) _9 K
4)表达式也可以包括<,>,<=,>=和==比较运行符。$ V* O- Y5 K$ {+ G9 w4 A

$ p. _: H( I/ l5)表达式可以包括&&和||逻辑运算符。
8 |& {* U& I) A! c$ i8 V1 \6 S
: m9 a8 G+ f4 L; N- g6)非(!)运算符可以反转表达式的结果,如:#if !(LIMXP > 12)
! M& l, ^, O$ t% y, x) @
  R/ R0 X4 C% X$ Z% B如果LIMXP > 12,那么表达式的结果就为假& c# l7 i! w2 O! P0 P
7 c0 t0 l& d( ^+ u
7)#if也可以和defined语句一起使用,这样可以检查某一个宏是否被定义过(也可以直接使用#ifdef)。如:; L: w, s8 F, g" ]: k! j) S" S
3 n9 |3 B8 N% s; H( K3 S% m
#if defined(MINXP)9 ]. K8 {' K- R) p& R# ?/ x2 F3 d
/ Z, }# F2 ^' i8 C9 w* z4 ?0 j
意思是如果MINXP被定义过,那么表达式就为真。也可以在前面加一个非(!)运算符来取反的结果,如:
9 @8 g# [' w8 k) X9 U9 a  q1 a' O7 B( z( ]2 e5 |( J. a( a
#if !defined(MINXP)8 b$ r! o1 K8 v5 a5 Y+ Q
6 n& {* e# B$ Z  S
意思是如果MINXP没有被定义过,那么表达式就为真。5 j8 o, H' ?* \; C+ S

3 o% t" R3 y  q* q- l8)一个标识符如果没有被定义过,那它的值总是为0,-Wundef选项可以用来产生这个警告信息。) H( c( m$ t: R% r1 x
6 Q# n: _3 \2 ~4 a" K. Z
9)有参数的宏(gcc:预处理语句--#define、#error和#warning )在表达式中也是零值,-Wundef选项可以用来产生这个警告信息。7 F- F# D* m& ^" w9 _

" d9 r6 L2 N( @; k  m& L2 k; o( @10)#else提供另外的选择,如:) q0 r" Z7 v/ f" u
2 q7 M) ~6 Y9 ~" X- G+ ]! D% c/ v5 D
#if MINTXT <= 5) \# b' d0 W3 D: R; s2 |4 B
#define MINTLOG 11! h7 b! m& s7 Q3 c5 y* a
#else
% o& ]2 C' D* z) T2 [- x#define MINTLOG 14: B" e' U' b& a3 m9 Y. V
#endif
8 M% l6 K  r; ^) |( R& J6 ?* E11)#elif可以提供一个或多个的选择表达式,如:
; Z( F8 L; b  t' c! w% U8 z2 p% Y; ]" E+ k
#if MINTXT <= 5, y7 o' z5 A+ I2 e) j8 i4 P
#define MINTLOG 11% r6 |1 t  ~" m% C& L
#elif MINTXT == 6& k3 k# y3 \8 [0 V$ V
#define MINTLOG 12% z8 g& g# G8 ?
#elif MINTXT == 7
) o; t. l1 C: \7 w#define MINTLOG 13
: v8 M& \1 z8 l7 C5 x. f" K6 f#else
% t8 u6 R, U! x8 t1 P; e/ [#define MINTLOG 140 {+ z: M- P2 m: I3 o
#endif6 d8 }. n# V* }: I

5 G0 z7 a3 O( _9 X& a) j- V, \8 J7 I& e/ }0 v) {! H6 u! Q

' c: t! F( I: F#ifdef
- X0 a+ ^: G! ?8 {& G2 z) _0 j; L
在跟#ifdef后面同一行的宏如果被定义过的话,那么#ifdef后面的语句块将会被编译,这个语句块以#endif结束。例如:; h4 T) z/ x2 `( d
6 \* @+ x7 J5 T8 N& R  e
#ifdef MINTARRAY8 U7 w# |( N4 u) Z3 G8 @
int xarray[20];7 \) P3 [& i" N1 i
#endif2 [3 f8 y# T( s* I6 a# P- K

6 G& Y& g% X. F% d
3 Z2 h! V9 A) Y- E9 s" U: P& G0 o' ], d, p4 w
#endif后面的注释语句并不是必须的,但确实一个好的管用法,这样就可以知道是你定义的哪个宏的结束位置。  V- ~5 s& w- g& _
/ h+ P5 M* J+ A% T* w
和#ifdef意思想反的就是#ifndef了。
( z& ^# J  D" K! h  n#if语句# r- J7 G, p( ?: a* [
#if语句会计算它后面的表达式,并检查表达式的结果,如果结果为真(true),就编译后面的语句,如果为假(false),就不编译后面的语句。
) p( k, A8 a3 N# _! ]% f1 b5 q
1 r6 N1 W  a$ t例如:5 e5 M- Z/ ^8 _* O5 k( U/ x5 |. Z

, H  N; @# l; Y7 X6 P3 C      #if COUNT
7 s- _6 D# u8 |2 u8 ~: w      char *desc = "The count is non-zero";
- Y2 `' V$ _: ~# i3 M, \- X      #endif0 K# c; i+ _' Z+ j- ?. ]
; W( s4 m) w' i* ^' P* g- G/ o6 f
1 N1 ^# ~) b$ Z
只有COUNT不为0的情况下才会编译
! Q/ H; P1 E4 V+ h4 o1 f/ K0 K4 s5 j- w, s! Z7 X7 r0 s
char *desc = "The count is non-zero";7 Y1 c0 f1 |. t0 [# J5 {% l
8 `1 `: @5 o; f- A
语句。
! N' A2 ?4 C: p$ I' s0 b1 q) Y1 [' M! a( U5 \9 w* u
下面是#if后面的表达式的一些语法规则:
) p: G" ?. y" }2 G
( ^/ x: T( _- Q% U% k. l/ g- |1)表达式可以包括整形常量和宏,这个宏必须是被赋值过(区别被定义过)
" B! H1 ?/ [" V6 j+ x4 o8 t
; E" D: M  ]8 ?8 \$ V2)可以使用圆括号来指定表达式的顺序。/ r5 C6 p6 ?$ B" x+ y* k% b

7 T: u1 h4 ^  x  S4 T3)表达式中可以包含C语言中的+,-,*,/,<<和>>算术运行符。这些算术运算使用当前平台的整型的最大表示的值来运算,一般为64位的整型值。0 A! ^: C2 _* N- e& @5 U* `3 B6 Z
0 b4 B% I- u) r9 d
4)表达式也可以包括<,>,<=,>=和==比较运行符。* `, t) b7 S( P5 s* a6 ^: G% _: e
) z* {' W2 J9 F) v6 P6 ~
5)表达式可以包括&&和||逻辑运算符。
# b3 t9 d% x/ F5 N; ?0 l6 G0 ~# _) T; \& X  E1 T/ I
6)非(!)运算符可以反转表达式的结果,如:#if !(LIMXP > 12)5 e9 O5 S2 V% V* u

5 i8 g- ]* i% x& d& Z8 D( U1 P6 S      如果LIMXP > 12,那么表达式的结果就为假8 e; `2 {9 _0 F3 Q# ]0 S/ Y. H3 W' `

* j* p9 c) z& L& Y, e! |9 o7)#if也可以和defined语句一起使用,这样可以检查某一个宏是否被定义过(也可以直接使用#ifdef)。如:7 G% X1 E; z/ e9 q

: ~& X% L. ^0 T( ~6 T7 y  w' L     #if defined(MINXP), G+ `; i; ~9 Z5 D1 }7 b" l* T; D

- H) P2 S9 L% E. [     意思是如果MINXP被定义过,那么表达式就为真。也可以在前面加一个非(!)运算符来取反的结果,如:
# m0 m% q8 ~$ y6 E& u" |* E% @5 F
7 q1 h# L6 _( u" u$ J     #if !defined(MINXP)
4 s$ C4 N5 n% f: l! r# a0 G9 T9 c# }4 \+ y# g9 W' j7 R" P
     意思是如果MINXP没有被定义过,那么表达式就为真。#if语句  v. k- A$ [. ^
2 m1 w  P) j3 ^

0 a" I+ H0 f* y2 A5 s#if语句会计算它后面的表达式,并检查表达式的结果,如果结果为真(true),就编译后面的语句,如果为假(false),就不编译后面的语句。
) f. y5 s4 G0 o3 v! Y$ L; R; W" G% v. _6 f" y6 U
例如:
8 p+ n6 c! T+ E" [$ W5 ~5 n6 F, o* d2 C" V! w
#if COUNT- I' \: _4 d9 s% e9 u% a) N
char *desc = "The count is non-zero";
) R) V: B; H/ c* B( s& X#endif
4 C3 b" ]/ o8 }& z. L$ _: ^" I! n; S. q8 D4 Y
7 y8 I: M# r$ A: B& {7 {
只有COUNT不为0的情况下才会编译
, g1 t0 v" i! |- S1 ~1 u) u5 v& w) f9 ]0 k$ h1 k  P5 Z$ x
char *desc = "The count is non-zero";
! U  e: Y6 G0 V( }0 K: S, G; ]) h0 g" p) _! f' P! e( `
语句。
+ W* T5 P. ]. q9 O9 ^3 V  [/ z
$ r4 z: m0 e& g  V8 [- h) A* J下面是#if后面的表达式的一些语法规则:
, B0 Z0 I; c4 p0 a# _* M! f) J& Z9 g. c* `/ k" }$ |9 J+ V
1)表达式可以包括整形常量和宏,这个宏必须是被赋值过(区别被定义过)
+ @# x  F; t0 P7 G3 C
$ N  d- P+ R/ k' b7 _% I0 Q5 E2)可以使用圆括号来指定表达式的顺序。
; S  r, C) f) h- i. L' f& ~$ _
+ i: p1 t* v5 s$ M- o7 i7 W3)表达式中可以包含C语言中的+,-,*,/,<<和>>算术运行符。这些算术运算使用当前平台的整型的最大表示的值来运算,一般为64位的整型值。: F2 [+ J% O" c: ]1 K/ T+ F! b
: U  C! D8 E4 k% r- `
4)表达式也可以包括<,>,<=,>=和==比较运行符。
3 E* K2 H" ^. |& ~- I, h* K$ K: X$ w: a- D/ j
5)表达式可以包括&&和||逻辑运算符。
) H% }9 F# _/ j& q
) X3 e0 u: n3 k7 c5 @0 w+ y" ?1 W* n. C6)非(!)运算符可以反转表达式的结果,如:#if !(LIMXP > 12)' o6 L+ ?0 h5 x* P& w
) k& `7 u# _: m: n: `0 P
如果LIMXP > 12,那么表达式的结果就为假& ]1 m3 n* e+ f5 C* a5 G1 G& ~* V
: S0 C# h$ c# v; P* a& A
7)#if也可以和defined语句一起使用,这样可以检查某一个宏是否被定义过(也可以直接使用#ifdef)。如:
1 r# [1 N( b2 }( n* N2 A' ?# g* d( |; o
#if defined(MINXP)
( n* K' P4 ]2 l, I% J9 X0 `; U' V2 A6 v
意思是如果MINXP被定义过,那么表达式就为真。也可以在前面加一个非(!)运算符来取反的结果,如:. u) x9 ^2 R  q' ?& F7 M3 G2 s
- Z- q* z% a4 s) n: z4 M
#if !defined(MINXP)
  ~/ P# O3 @  i) [/ D2 s3 \
8 P* f. o# Z' ]% d" C8 Q意思是如果MINXP没有被定义过,那么表达式就为真。8 |, X. L. K2 }! }+ \. \6 M
, K8 G) x9 e: Z: w! d+ t, ^" o
8)一个标识符如果没有被定义过,那它的值总是为0,-Wundef选项可以用来产生这个警告信息。
4 s# g" S. @/ O% n! b1 K# c
# T) i3 q$ m* ^. O9)有参数的宏(gcc:预处理语句--#define、#error和#warning )在表达式中也是零值,-Wundef选项可以用来产生这个警告信息。, l6 \4 }+ G3 S, F8 |
+ |  P" P3 I: ?% H
10)#else提供另外的选择,如:
( U0 X* ^+ Q; a0 E: |4 ~$ Z1 @( t+ }! D& n
#if MINTXT <= 5
1 [6 }7 r8 T; m, S+ E#define MINTLOG 11
5 P4 d; y; O4 O7 r& g# O3 \: ~#else! X( y0 U; n$ u
#define MINTLOG 14: t1 k; H$ c7 [/ U5 x' \0 Y
#endif
7 [/ f# z0 y. N11)#elif可以提供一个或多个的选择表达式,如:
5 b/ j0 O$ c+ m* z
' P% X: B- _  N, a#if MINTXT <= 5
+ |' ?+ J2 U1 r7 R* e#define MINTLOG 112 m/ }9 V" `$ E* }
#elif MINTXT == 6
1 V. @6 I8 J$ r) J#define MINTLOG 12
/ x5 F' A' H/ j$ |! ~8 @: [#elif MINTXT == 7
, U2 e8 F9 }3 H) g; x#define MINTLOG 13
; N6 K) ?5 J$ ~' E& W' v: J) |#else, |( }6 o* \! R4 ~1 |5 @
#define MINTLOG 141 f* ]! i1 @( q6 I3 N9 ~
#endif* e) _& f, Z9 }' y% _" g6 g! A. Z% W

+ O4 R3 o8 a& ^* V4 M8 _% l& z
* O: z" x, q  f; X
) u" r, {# T$ h  j#ifdef & r) r; b) L3 U$ e! J
$ ^4 x# v5 g) O" X: I! Y
在跟#ifdef后面同一行的宏如果被定义过的话,那么#ifdef后面的语句块将会被编译,这个语句块以#endif结束。例如:
% o" g9 ]# q& D% N6 z4 C
, e4 E" O8 `+ k1 m3 z2 B$ [- h#ifdef MINTARRAY
( O+ _( w* z9 ^5 [8 b; l, @) W" Z' N# Cint xarray[20];
7 ~3 p5 z. A4 j: h2 m! q6 [0 r#endif; l! W1 l! o2 r* ~' _: ^8 e  N3 X

9 L0 s8 f) h" \7 L8 P7 O7 u
6 ^1 m: W! m& X: _% k
( J$ U7 T+ b7 e- [, e+ u8 m#endif后面的注释语句并不是必须的,但确实一个好的管用法,这样就可以知道是你定义的哪个宏的结束位置。9 S* B! B& i5 B8 Z" [8 F. P

5 E! k* K6 _) Z0 c+ X2 J4 _6 s和#ifdef意思想反的就是#ifndef了。
5 ^% W- R2 h& S- v
; {3 G  j; H) k6 @5 [& G+ [8)一个标识符如果没有被定义过,那它的值总是为0,-Wundef选项可以用来产生这个警告信息。: c7 f2 v, N6 L6 `

+ g+ S; j8 H& ^8 X! N9)有参数的宏(gcc:预处理语句--#define、#error和#warning )在表达式中也是零值,-Wundef选项可以用来产生这个警告信息。) j$ Z, K2 N, O$ F
# G& A# b3 B: h, H* P& o6 S: E' R" o
10)#else提供另外的选择,如:
6 V- h3 v" G6 j3 X1 t0 V8 g9 e, l4 W5 D7 Q
#if MINTXT <= 5! \/ f3 Q' p, n4 Z& k
#define MINTLOG 11
6 v5 W! i5 \1 x  S. a: Y#else7 y# O3 v" b- X7 ^
#define MINTLOG 14
$ a5 g( O* q% f; a#endif8 M1 O1 ?1 B: Q0 v0 [* X% |
11)#elif可以提供一个或多个的选择表达式,如:3 _, c9 Q9 ~3 h; O: s
8 ~) T3 G6 g. I1 p( U0 q' W
#if MINTXT <= 5+ u! T6 o9 g" ^2 F0 m: o2 r
#define MINTLOG 11
+ L5 s* V2 E: `8 \- |5 r! g9 m7 n#elif MINTXT == 6
8 g7 u* V: ?- Q& O$ O#define MINTLOG 12
. I4 D* |0 s9 e: [#elif MINTXT == 7
4 u+ g$ P" b/ p# w1 k, c, o+ T3 q#define MINTLOG 13/ w. }( x! U6 J9 Y: A
#else
& Y5 X+ g8 w# L; g! b#define MINTLOG 14
5 t/ U7 ~# ~- q  f6 g1 k8 S; [#endif
6 u0 L9 Z( a% H6 {% H( E+ D/ R3 t& s0 r

% c; s+ A0 O  O6 B# x( s! E/ i: V6 E0 g
#ifdef
5 Y+ s  y( A4 Z* X; a+ X1 ^  k5 ^4 O. O2 D$ {: U' ?) A' k
在跟#ifdef后面同一行的宏如果被定义过的话,那么#ifdef后面的语句块将会被编译,这个语句块以#endif结束。例如:
- t# n0 p6 a& o' r" V+ D
+ V3 G! _* c; @7 s$ i# D( t#ifdef MINTARRAY2 K3 o) K5 i! u2 T2 F# Z5 c) Z
int xarray[20];! F; Z/ e" v1 w0 {! A. P3 {
#endif7 u; F. r% a( W' t4 w( g2 N8 ?

. D4 A, s4 E# I5 d: p& F3 e 9 v. I* \+ {/ p  h# f
$ P7 R- g& l: `% \  n" E
#endif后面的注释语句并不是必须的,但确实一个好的管用法,这样就可以知道是你定义的哪个宏的结束位置。/ l+ w, Q' E1 u0 m7 p; a- a3 Q! e- B

& ^! y& b. _5 Y) ]和#ifdef意思想反的就是#ifndef了。
4 {! T2 X+ T, S# i* o3 a( y
- h2 r  v: v4 J$ {! f2 K7 e其实这三个都是宏,下面详细的说明:
, F2 O9 g4 ^; \. x2 K& g( m' n  [# p, o- c/ h) L; M
    你所遇到的这几个宏是为了进行条件编译。
! g: `8 F% r/ {. b# @4 U' g    一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。
7 X+ o4 r7 T/ g4 y    条件编译命令最常见的形式为:
% h/ T+ H' V  c$ P, ^    #ifdef 标识符   C! M' \; |  x) k. x2 m
    程序段1 / A0 k$ E3 u( u: u
    #else
* r1 _6 r7 u7 U/ s    程序段2 8 v+ X; d( h1 V+ _
    #endif0 ~2 F6 Y* o8 u/ j
    9 o  r0 ~/ P# f+ O* p" }
    它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。9 f  n) j$ P1 |& M% V  g7 O' N
    其中#else部分也可以没有,即:, M; X  d3 o6 Z( Z0 c/ M! }
    #ifdef
* N$ a. o7 P: Z6 {' D4 ~$ ]; `0 @    程序段1
/ ?8 e& K5 U! |9 w5 l+ @+ c5 w    #denif
2 q. K1 N5 j" F5 n   
. o0 c: F5 [% W2 ?    这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。如果一个C源程序在不同计算机系统上系统上运行,而不同的计算机又有一定的差异。例如,我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译:
2 s7 U1 |6 K; r# ~& |    #ifdef WINDOWS
6 e# d$ m5 c* k    #define MYTYPE long
; m" ?: t/ J% @; g    #else
/ g) a6 B' L9 p& u% B7 E# }' k. f! _    #define MYTYPE float
8 @( e- r5 w5 Y    #endif
# z/ w) T) [/ `8 h+ f1 u, f      ^0 V* y. d; m
    如果在Windows上编译程序,则可以在程序的开始加上" L5 I) q3 m( F" i6 B
    #define WINDOWS
* _6 B5 b- y6 v& z: @   
8 {+ b/ c/ q  `# }6 O; t    这样则编译下面的命令行:3 E$ J+ n9 [; m# R8 V- l8 b3 }
    #define MYTYPE long
, M9 h; q5 @. u# |+ V* @% V    ( ^" t! Z) V: G! M* Q# _
    如果在这组条件编译命令之前曾出现以下命令行:
" x  O& Q- m8 R+ Q  C. C    #define WINDOWS 0: b( Q. A8 `% m0 K0 Y: i
   
! q# n3 e8 m% ^  x9 ^" G    则预编译后程序中的MYTYPE都用float代替。这样,源程序可以不必作任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况,可以根据此思路设计出其它的条件编译。
! ?2 J( j# ?: u* s7 B" K, G0 ~    例如,在调试程序时,常常希望输出一些所需的信息,而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:  V& x/ m% e+ T% R0 W
    #ifdef DEBUG . N: a! a: E, L0 a8 h; Q: k
    print ("device_open(%p)\n", file);
# n  h. I; x0 ^1 i/ H% {. b$ d    #endif1 p' y" M8 Z# C9 v7 v
    # Z( h  ]2 s* s9 h
    如果在它的前面有以下命令行:+ w5 e8 n3 S' x
    #define DEBUG5 D# g1 e6 M) h; f' q
   
# f2 d) k8 H- D( |% ?    则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可。有人可能觉得不用条件编译也可达此目的,即在调试时加一批printf语句,调试后一一将printf语句删除去。的确,这是可以的。但是,当调试时加的printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删改printf语句,只需删除前面的一条“#define DEBUG”命令即可,这时所有的用DEBUG作标识符的条件编译段都使其中的printf语句不起作用,即起统一控制的作用,如同一个“开关”一样。
+ `. Z9 y( U! @  V0 C( Z/ A6 B    有时也采用下面的形式:
. H5 ]0 c( V; f. i' C5 E. [! `# A7 O    #ifndef 标识符 / G9 P6 m( A2 }" s* V' ?2 p
    程序段1
0 m; K  q  D7 r8 ^8 [3 c) Y9 `    #else 8 h1 v! V; ]. X* T7 n! r& b
    程序段2 . p- H7 ^% i- G% E% O( A
    #endif: A  K# v8 ^: w, x# \
    2 @, C# o3 m% |
    只是第一行与第一种形式不同:将“ifdef”改为“ifndef”。它的作用是:若标识符未被定义则编译程序段1,否则编译程序段2。这种形式与第一种形式的作用相反。
  t( t0 [+ O, T  E. h4 {* M- w3 c+ `    以上两种形式用法差不多,根据需要任选一种,视方便而定。
/ ?0 j: G$ W; S+ z7 k- T    还有一种形式,就是#if后面的是一个表达式,而不是一个简单的标识符:; v) f: _# I9 P; g% I
    #if 表达式
! Z, v9 A! ]* E# {    程序段1 / x5 x" l) y' ~" `) _" b
    #else
# g+ h$ k4 f- M+ F/ R8 x9 ]    程序段2
& A  i! z7 k# X' N: z! C% F    #endif8 d3 x( g1 o" `  @& W) y+ }- Y
   
' L  T! O! D6 ~+ ~- N    它的作用是:当指定的表达式值为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。& C1 [# K' G0 s
    例如:输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输出。0 Q7 c; _; H0 c* B1 r
    #define LETTER 1
7 _6 k" p( c& H2 J3 b2 Y    main() ! ~# F( |8 Z3 }: Z/ ~' R% D, O# y
    {
1 ]% \& M1 P) G  a    char str[20]="C Language",c;
8 M9 n6 A% N  ^" ]: A  Z* N    int i=0;
/ H/ ~9 K: [3 f1 ~1 y3 v( y    while((c=str)!='\0'){ 1 r$ r) y. K8 y" k0 W  A
    i++; , H7 N9 ^5 a! f: |6 b" A
    #if LETTER
, \! [, g9 f9 ?; k& j9 }; \! o    if(c>='a'&&c<='z') c=c-32;
9 _3 ~$ x( K/ t* `2 l/ s7 _' T8 @    #else   l+ {' ~- c' n( R
    if(c>='A'&&c<='Z') c=c+32;
9 T% O% r8 J% Q( Z+ Y7 I) R    #endif 3 O( H5 A8 m6 `% G% B4 y
    printf("%c",c);
8 o% {/ Z2 h6 R8 ?/ @    }
; `+ v% [- W) M' ?' X3 @    }
9 h8 w6 n9 X# l0 G& E4 a   
* S1 z- L- O& y( x    运行结果为:C LANGUAGE
& r; D; I$ j/ [5 U& L    现在先定义LETTER为1,这样在预处理条件编译命令时,由于LETTER为真(非零),则对第一个if语句进行编译,运行时使小写字母变大写。如果将程序第一行改为:
/ }/ p! J6 Y- G9 V3 w! B9 k0 d    #define LETTER 0
# J5 D0 k# _! U0 i  ]. \& {   
* w3 N( A& P2 r/ \$ `6 x    则在预处理时,对第二个if语句进行编译处理,使大写字母变成小写字母(大写字母与相应的小写字母的ASCII代码差32)。此时运行情况为:' T4 U" e0 T' i7 Z, m) b" S
    c language
6 L5 S7 w! B* ^4 x9 z( B3 ^    有人会问:不用条件编译命令而直接用if语句也能达到要求,用条件编译命令有什么好处呢?的确,此问题完全可以不用条件编译处理,但那样做目标程序长(因为所有语句都编译),而采用条件编译,可以减少被编译的语句,从而减少目标的长度。当条件编译段比较多时,目标程序长度可以大大减少。
2 Q- T. Q$ N7 ?. ?1 j( s# t 7 Y6 p/ @' v* B: |$ ~
头文件中的#ifndef
1 E6 \# S. c$ [4 ^5 [' N0 l, Y& C% t) f6 w1 t+ p
千万不要忽略了头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两 - A% D+ S. s2 d$ t0 l+ C
个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件
3 A/ |% f6 Z7 U6 ~,于是问题来了,大量的声明冲突。
0 ]; H- x% Z/ _  r% b: Y
  Y9 @; I! W6 S( i8 B还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用
4 R. {6 c$ a0 M  Z3 {) Y管你的头文件会不会被多个文件引用
! ]' Z% ]1 ^9 ]0 v' },你都要加上这个。一般格式是这样的: 6 E3 K6 j1 r2 r8 o- r

" j4 s; e4 B/ o& r+ Z#ifndef <标识> 9 C0 |- z5 n$ D
#define <标识> , C7 q6 O2 s* U: X. W( u

5 b5 m' f3 `# p' K6 M, ~7 W  X8 A& B......
8 _/ q* S  S( n...... 7 R3 U: F& C9 ?/ l
: @& T. R8 Q+ D5 V- }8 h
#endif
" c6 K# x; B- B( l, V; |0 s8 i' _) }  p$ }0 m8 g
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。
+ j+ p- z9 L- [6 O! L/ D' ]标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划
" c; k; ?- Z1 h1 I- \. e线,如:stdio.h
5 A* y7 j) y. J0 X" K+ g% O+ O) z5 Z( z! ~' r+ l) n1 B9 b  O
#ifndef _STDIO_H_
2 f! a' A5 Z1 J! }4 z% @+ @#define _STDIO_H_
& S8 g/ P5 ^$ N" e/ V# n' H7 I+ O: w5 U3 c
...... ' L9 T- `1 @* t

8 Q% I" ~& \3 B# a+ i#endif( ?+ Z7 b4 L, }2 W# C" B
4 v$ d# B* X% S/ {3 n
C语言常用宏定义技巧
, ?4 B; m. \4 t# V: C用C语言编程,宏定义是个很重要的编程技巧。用好了宏定义,它可以增强程序的可读性、可移植性、方便性、灵活性等等。( i$ \4 f; g# G! Q$ Z2 F$ O

! R- g" i8 U% f7 S$ k3 k1.         防止一个头文件被重复包含:" Z9 T. W. c+ I: C3 g) O$ P# M
( H. m! E/ s: S4 E! g) u, d
#ifndef COMDEF_H" p* i0 a* K1 h* i! ]6 Q0 p

( r1 C2 W) }) e  O' V: Z#define COMDEF_H% ~! J) J8 V. I! w9 ^

% o% a6 f0 ?6 |. R+ G//头文件内容
3 S) x; d3 Z& U8 k6 W  H3 W3 m4 E2 a; b3 ^/ c! o, |6 H
#endif# O( r) `/ R& @$ l7 i9 h& s0 H) j
0 I  u2 N: N2 C5 }# J) R# l7 t& B
当你所建的工程有多个源文件组成时,很可能会在多个文件里头包含了同一个头文件,如果借用上面的宏定义就能够避免同一个头文件被重复包含时进行多次编译。因为当它编译第一个头文件时总是没有定义#define COMDEF_H,那么它将编译一遍头文件中所有的内容,包括定义#define COMDEF_H。这样编译再往下进行时如果遇到同样要编译的头文件,那么由于语句#ifndef COMDEF_H的存在它将不再重复的编译这个头文件。4 ~: ]6 Y( c3 v3 p
2 ~2 e& K9 W, T# w: w; P/ P1 F) Z
2.         通过宏定义使得同一数据类型的字节数不因不同的平台或不同的编译器而改变:  {* j: Z! p( D5 A) f

3 w2 D) i9 u" [; i( Ytypedef  unsigned  char      boolean;              
: q' h6 B6 e3 j3 x
' Z2 _- O0 a) a7 |& x
9 V/ F7 _) `$ e* v& \+ q
$ Z9 _2 |$ B0 V, g" B4 }$ @       typedef  unsigned  long  int  uint32;               $ T( S9 E  Y& X, b* F
& u1 V9 v* X7 _9 }' D# Y
       typedef  unsigned  short      uint16;               ; @9 q) D" n8 W, [' [$ h

4 T; W' z' C0 h0 d. N* m4 d       typedef  unsigned  char      uint8;                & P+ j+ X9 Q( c- {) L
8 d- y# s- z4 y& L0 e0 l; A
      
) E- ?, h, o9 g* b5 M9 l
& a6 p' i; y2 L+ Y/ f# Y       typedef  signed  long  int    int32;                 
$ ~* |* B( t' a. R, }: k5 E9 C# B% f9 u
       typedef  signed  short        int16;               
  I" a  m9 T* B
/ |0 S  ]! P6 q. g. y' T       typedef  signed  char        int8;                  + G, ^( E' _" L4 a$ x" k. l/ @

3 ~) j, n5 J6 u  A! K3.         得到指定地址的一个字节或字:0 H$ N  k* [7 U

4 q, e! t5 u( L: Z5 G* _9 B3 a#define  MEM_B(x)  (*((uint8 *)(x)))
  h7 Y0 E9 B+ N: m: y$ f9 ~
6 K( p! [9 J+ A) d, ~! N* w#define  MEM_W(x)  (*((uint16 *)(x)))
6 h8 h, l: F0 x* i5 k/ d
2 e& t! m9 Y0 S9 J5 L% n2 C" ^$ [注:类似于这种有多个字符串组成的宏定义一定要注意加上“()”,因为我们知道宏定义只是一种简单的字符替换功能。+ X2 N1 Q) Q8 J+ ]9 Z- {/ R

  [7 o) U% j: m3 w4.         求最大值和最小值:
  h) G+ @( E% r" H- w7 r( z( L- l/ u8 d
#define  MAX(x,y)  (((x)>(y))?(x):(y))
! S. H- L8 E8 Q8 z1 b' T0 m3 G# ^& P7 t( U' P( K* r9 \2 w$ \
       #define  MAX(x,y)  (((x)<(y))?(x):(y))! Z% ^% m$ k. ]! S3 `+ ^

- i# T6 ]4 s6 F3 ?5.         得到一个结构体中field所占用的字节数:) l9 x  z) u. L

5 q( N/ @( `+ q; I" z#define  FSIZ(type,field)  sizeof(((type *)0)->field), v' k) ~, @( m5 X7 S
) M9 `  R+ z1 a
6.         按照LSB格式把两个字节转化为一个Word:, ?) H- }8 e/ }+ z7 `
; w" p4 J% s4 B: x
#define  FLIPW(ray)   ((((word)(ray)[0])*256)+(ray)[1])
7 Q( j% h* d, {2 g, Y) X3 Y5 g4 l9 Y" i) g2 z
或可以是5 L. z. v, q0 h1 L
6 H* I2 Q% h- i5 J
#define  FLIPW(data1,data2)   ((((word)(data1))*256)+(data2))5 _; M# p3 R- n% f  ]4 r+ @

) O" k% d, ]/ f- Q% V7.         按照LSB格式把一个Word转化为两个字节:) O/ {. @9 C2 f9 G$ g" S( [1 z
$ E" f- {7 }; ]- |4 F' O; w
#define  FLOPW(ray,val)  do{ (ray)[0]=((val)/256); (ray)[1] =((val)&0xFF);}while(0)
, h* u5 x0 g/ e/ R7 \* N$ q# u' e+ F/ [" h, x& ~
或可以是
% L- ]5 I7 k( }7 l7 J& N) d6 b* }- O, O* W0 u
#define  FLOPW(data1,data2,val)  do{ data1=((val)/256); data2 =((val)&0xFF);}while(0)
  ?- U3 {  o6 e
2 A8 x' _' X: i注:这里的do{}while(0)是为了防止多语句的宏定义在使用中出错。比如:: o7 Z6 P: G6 T' f

/ M5 z( T* s/ v/ D4 n( K) bif(…)
2 i- K1 T. d* @0 }  i7 R0 C( @! R& p5 w7 \
6 d, _# S( S# q2 b! D1 \
8 w/ r" F: P: p5 F2 r1 c/ F$ s6 V
       FLOPW(ray,val)" V. H# d) [, M2 L! y
; m* k  P- i$ s, l- X

# t* Q6 Q# G1 P4 d8 u- e
1 y# @9 d# F$ v' \! d' Kelse" f* ~' I# Q" |- c+ \
7 z( E$ {5 q1 s3 n  k
% i' Z' R- `8 y1 y$ \

' y# d+ `. C: c上面的程序代码中要是宏定义FLOPW(ray,val)中没有do{}while(0),这时我们应该将语句改为:
8 e1 P% B7 a8 t% m7 t5 o7 L/ F
! o& R2 ~+ _# Z0 uif(…)2 `" P, `2 s" v" M

2 |7 V2 g- b3 R* M+ o( h{
" V  ?7 c; G  |' w
( C, W% x) r1 b+ M       FLOPW(ray,val)1 F8 M3 }/ O  f8 H; V- O/ W* @2 h

) x8 g, n( f( v) }( ^}
  d  N$ ?; D6 k1 ~2 }" C4 L3 T( T6 k& D  ?) ~: N" M8 ]7 K/ z
else3 A% ^  ]% W3 t) @. s* M( |

, F' B# O( K7 f* }: O- l  o# s{+ b, _9 f# F: i7 ]/ G0 ?

$ Z; O' @4 z( ?3 ~8 r + r6 M% z: K, j& t; R: x! |9 S3 o/ e$ i
' T" P# r' V0 f' s% W1 w
}
# S0 U8 W* v' R1 s
* G  o+ f: k( f" o- L8.         得到一个变量的地址:
1 \5 s1 K" ?/ {8 E6 L
% X5 |( z' @% }#define  B_PTR(var)  ((byte *)(void *)&(var))0 K( k3 v! G' J

+ i# N2 C4 a: f! a1 _0 l#define  W_PTR(var)  ((word *)(void *)&(var))
1 ?; d: K  v: E- ?' N; f2 ~# F& J+ w. t  k6 z: j
注:宏定义语句中void *申明了该地址可以是任何类型变量的地址,byte *申明为单字节变量的地址,word *申明为双字节变量的地址。
; h' }$ W$ H) ?' y
5 s7 Z. Q: G- c0 t. V9.         得到一个高位和低位字节:; b; V1 r& `5 T* Z1 N2 I6 P
; {7 l/ K& w* _- r7 L$ a% O
#define  WORD_LO(xxx)  ((byte)((word)(xxx)&0xFF))
& l* g$ N+ i! s9 T! p! D* J* l4 G9 D2 G" u* n; X4 y1 h
#define  WORD_HI(xxx)  ((byte)((word)(xxx)>>8))
. Q' ]) v( ^: k
' B5 G7 q/ M: N! @- D2 [4 s10.     返回一个比X大的最接近的8的倍数:
- n5 G" s' \. E- H" \9 `
6 ]# ~- D9 v# [; F' v3 r$ N#define  RND8(x)  ((((x)+7)/8)*8)
$ P2 f" I, [- [+ }5 K+ X
$ Q1 f5 U- L0 F8 D% }% o* B0 ?5 ?11.     将一个字母转换为大写:) y$ J' ^; S7 f$ R0 p

, f. K3 |$ h- o#define  UPCASE(c)  ( ((c) >= ‘a’ && (c) <= ‘z’) ? ((c) – 0x20) : (c) )
1 t! y# K4 x& ]4 ?9 V0 p1 O; e9 q# V/ A* B% w8 C
相对应的还有将一个字母转换为小写:3 r2 s# W* y9 H
. }  e& O$ `3 b" v; r
#define  UPCASE(c)  ( ((c) >= ‘A’ && (c) <= ‘Z’) ? ((c) + 0x20) : (c) ): Y8 }7 ]2 K0 k

) X* t) ~4 n6 x9 A2 ~1 }* }3 V注:如果你记不住大写和小写之间的ASCII差值,可以将0x20换成(’a’ - ‘A’),但这里小写的ASCII值大于大写的ASCII值你该要记住的吧。
7 V0 L- P3 W: d9 k' J
( M+ _5 U! i: x$ r12.     防止溢出的一个方法:5 A$ I7 v4 L' E* ^
: \3 r; P% E1 Q. {7 Z8 s
#define  INC_SAT(val)  (val = ((val)+1 > (val)) ? (val)+1 : (val))$ S( k! \% V/ X

7 s# R" }. ^+ I* B# m还可以做成循环计数的(加入计数器val是个无符号整数):
* g; M; s/ h) N  Y5 W( m2 @! K1 W: P
#define  INC_SAT(val)  (val = ((val)+1 > (val)) ? (val)+1 : (0))
/ X" A: {( u* Z4 J1 }7 |# @7 L+ K) E' |9 q3 w7 [
13.     宏中“#”和“##”的用法:
; P- {: N0 A  R0 Q8 _0 X
4 I6 F$ p( g! ^0 R( ]- s5 K6 [$ |一般用法:: x* {0 u9 p- f% A# n4 X& I" W
" ^5 M7 }: ?: P
使用“#”把宏参数变为一个字符串,用”##”把两个宏参数结合在一起) O' F7 b9 I$ S; K/ C

5 v! X! T2 a& i+ |% g/ }+ a例程:$ R& H" f. ~# f7 ?4 m
$ O, c0 b( y4 `0 _+ z$ J9 R6 t
#include
$ K5 n: _9 a- ?; C( M4 X3 t
9 `/ M! k" J$ W. Q3 \#include
; @" W. u; g' B( k# D' G$ i
5 O3 }: z! Y) N- w& g5 @: [ ; X" R; ?1 e. I/ l3 G2 Z; O1 _

" ^! m. P8 y6 s5 f& q# w#define  STR(s)  #s9 @9 E+ I% f) O

! k; `) F- ]/ w5 N& y#define  CONS(a,b)  int(a##e##b)9 K# R# a6 I$ R& t% U7 p7 r
( Y1 X, Y: `/ \: d, o

: i4 N- R3 E8 j8 N7 ^
# K- l! k7 t4 f* ?int main(); Z5 E0 v! v- U# b
  r+ |4 H4 G& U) g, S. ?
{% i. i7 A3 w! I$ E2 E' R

+ C8 H) y* y% X: d4 m, ?. o) T+ S4 \       printf(STR(vck));         //输出字符串“vck”
5 ~, `, t9 w1 Z9 L0 d7 P* L! g9 P" ]# c4 k3 x
       printf(“%d”,CONS(2,3));   //2e3输出:2000
8 s1 S0 ]- X8 p
' ~& B9 a9 I" l6 S5 g+ ^; z       return 0;
0 L9 z- V  ]( c/ v/ Y& V! t/ Q! J8 F- |
}
( }2 _2 ~/ r1 n2 k3 q* Y! {
9 c4 B8 T' J3 F- i" O/ C
5 \8 _* n- B( B6 y9 D, g7 @$ h8 Q5 F- t4 k  t* [& f  p
当宏参数是另一个宏的时候,注意宏定义里有用“#”或“##”的地方宏参数是不会再展开的:2 N% @1 H$ Y0 u( v4 \! H; k4 A; P

7 s* y5 D3 M/ n0 `7 @& g+ E非“#”和“##”情况:
9 _6 h" g5 U6 d6 t0 |; `" Z$ J# e0 N- M+ Z
例程:. x% d( N. X# ^8 j5 Q( u

+ u& L7 d& G3 n/ z. S5 C+ h#define  TOW  (2)
% @' O  H8 S8 ?  p7 e- x  ?) [4 O1 Y5 R, ~& C: ~
#define  MUL(a,b)  (a*b); l* Y  k7 N1 V0 Z& y
- b) [5 }9 Y$ D' r. |: v1 I" ^
7 I% y% O0 K) d2 b! k
) e3 U9 L/ o- a- l) O1 ^" Y" I
printf(“%d*%d=%d”,TOW,TOW,MUL(TOW,TOW));
5 ~( T# L0 n5 w. t! r4 [! I8 ?+ p8 m5 [3 c1 G5 O, u. y
//该行可展开为printf(“%d*%d=%d”,(2),(2),((2)*(2)));
0 Z2 \$ Y0 B+ |' s
3 g" O3 s0 R6 O! k: | 1 m3 I1 ]& ]9 M) ?$ K+ b# Z

8 R% f2 i+ a+ s, }有“#”或“##”情况:
  c* P# X5 _# e
$ S+ u3 O: ~9 Q/ _% ^例程:; H' A. v0 L7 {6 }" u; b7 m" u

9 Z9 Y! p7 n3 p& W#define  A  (2)
( E- @. A5 F1 ^! A. I5 P  E4 {% l
  y6 ~& U% n1 O/ e; b$ @4 C+ b' A#define  STR(s)  #s
3 W7 ]% }" H3 N+ ^3 M2 V
& d- q' U3 i% ~( H+ C( ~#define  CONS(a,b)  int(a##e##b)
0 i( l- r+ h) x3 |7 C9 ^
7 Q$ F- n( P- b3 X4 z4 e 2 J4 \, }9 I/ y4 h- F# Z' a# ?7 k
  s4 R6 F- x! I6 }' B" }0 P
printf(“int max: %s”,STR(INT_MAX)); //INT_MAX 在include
3 y; {' a: L# _7 Y6 b0 X) S) y8 w" u( a4 B7 @
//该行展开为printf(“int max: %s”,”INT_MAX”);
& s% S; C3 S& ^( U" r% Q
9 s1 q2 c; s, m4 h' n, c$ |//即这里只是展开宏STR(s),而下一级宏INT_MAX没有被展开
  j. {# h* {' k1 P3 D' z  [; c' r5 x. X$ j2 @5 h& D2 s0 y- S

; m2 U* n! Z, f; O
, b8 e$ _! ?6 A" s- M# V4 W- wprintf(“%s”,CONS(A,A));
0 C4 _& h4 ?; z5 T6 v/ t5 v6 w. [5 S' ^2 c
//这一行编译将会产生错误,因为它展开为printf(“%s”,int(AeA));
3 X+ B$ j1 B% H/ P
  {) R5 t6 Y2 P9 S8 J# M/ j' A/ R
# X1 i8 T8 ^. {/ n; s' y5 ~5 R& G
为了解决其不能展开的问题,我们可以多加一层中间转换宏以实现所有宏的展开,如果你在编程时不确定你是否用到宏的嵌套问题,最好都加一级中间转换宏,以免产生错误。- n5 C: Z9 [3 p0 T
2 O* @( e/ G+ j+ _& B' p
例程:
& [' [: U# `/ {, z3 y" {, o8 z7 M8 C0 ?; y  V
#define  A  (2), {4 a  p! Z% K% z) l
! t$ R  h: X9 p; o6 a% ^
#define  _STR(s)  #s
$ `6 i9 A% K: b8 Y) u& y( j5 b* o% m# {/ R1 ]
#define  STR(s)  _STR(s)3 L) n7 W+ `8 N- m! J3 F+ T
1 [7 ?- f& H& v+ g( \
#define  _CONS(a,b)  int(a##e##b)  Z8 N# L, J: `# x% b) D9 ^
& {7 C& ~6 M7 X& u* w7 b  H& |; ^. G7 M
#define  CONS(a,b)  _CONS(a,b)
! k! D1 P# [2 o8 X: ]0 Q' G# }; K" ]! Z' D8 ?5 o/ m, q

% T) k* F" H# z  q/ p
$ h7 G1 k( w& h: t( z% J: B, |& Iprintf(“int max: %s”,STR(INT_MAX));
# q% A4 f' ~( o8 I3 U: X
0 `+ p. d" q! y2 C/ l//INT_MAX将被展开,它是int型的最大值,输出为:int max: 0x7fffffff
  V& z( k7 c7 M4 ?" t4 ~0 g! r4 h+ e0 q6 }$ y7 |1 `
printf(“%d”,CONS(A,A));
7 j/ b! X' U# R7 _  N, n+ O# l" M4 ~: H! _# Z. \) O' k  g% a
//两层宏都将能够展开,CONS(A,A)à _CONS((2), (2))àint((2)e(2)),输出为:200

该用户从未签到

2#
发表于 2021-2-9 14:55 | 只看该作者
Linux下gcc预编译#if,#elif,#endif以及常用宏定义
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-24 19:12 , Processed in 0.171875 second(s), 23 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表