|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
1.static关键字( B" e5 h- f4 y
这个关键字前面也有提到,它的作用是强大的。" Z2 Y* J& A5 t$ A2 ?. x# h2 ?4 @
要对static关键字深入了解,首先需要掌握标准C程序的组成。% @" D' Q7 A! k. A: b
标准C程序一直由下列部分组成:
* z b' e+ a7 E: K4 Q 1)正文段——CPU执行的机器指令部分,也就是你的程序。一个程序只有一个副本;只读,这是为了防止程序由于意外事故而修改自身指令;- _3 t6 g' p7 N9 ~$ p1 y7 s
2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量,存放在这里。
5 w, K0 L$ l* Z( R8 \7 N 3)非初始化数据段(bss段)——在程序中没有初始化的全局变量;内核将此段初始化为0。) |# g- `: t) k# y; \
注意:只有全局变量被分配到数据段中。
1 W- S* R: k, w% s$ k 4)栈——增长方向:自顶向下增长;自动变量以及每次函数调用时所需要保存的信息(返回地址;环境信息)。这句很关键,常常有笔试题会问到什么东西放到栈里面就足以说明。6 m5 s/ i! q% P& d6 `' N
5)堆——动态存储分配。
- M( H& L8 N2 W; H / @" M+ N% |& e# Z
在嵌入式C语言当中,它有三个作用:
% P4 {6 v3 i) `! Y8 m9 D作用一:在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。6 v0 w {" L$ N5 z
这样定义的变量称为局部静态变量:在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。也就是上面的作用一中提到的在函数体内定义的变量。除了类型符外,若不加其它关键字修饰,默认都是局部变量。比如以下代码:0 K: a! z! ^' U8 @" D
void test1(void)3 X/ z) L( E. z. X6 |( J
{
9 ~" F' |( s. T5 w# k" o, U4 S% p unsigned char a;
4 f6 }; q- L% @7 L. ]" y static unsigned char b;
$ ]: w* i) I4 Y6 D …
. y7 B4 ?: X' k7 `7 m; t a++;
6 Q- K) y$ {+ I3 l- ^% D b++;
0 D+ a. |" `* i& R) a}
l( P+ c* z; L2 ]0 z+ |在这个例子中,变量a是局部变量,变量b为局部静态变量。作用一说明了局部静态变量b的特性:在函数体,一个被声明为静态的变量(也就是局部静态变量)在这一函数被调用过程中维持其值不变。这句话什么意思呢?若是连续两次调用上面的函数test1:
2 u( u7 q8 O5 w void main(void)6 L, a1 e( v( J8 P0 |* n
{
9 e' o+ Q' x( R" E …6 _/ A3 p6 I7 l+ |
test1();
0 X. u, m9 H; C! n test1();
" M) o# m8 M7 Y* y; w9 t, T/ I; j …
7 l- T/ T) v7 Q! i, |) } }
/ I( e- `* R" t3 [/ G然后使程序暂停下来,读取a和b的值,你会发现,a=1,b=2。怎么回事呢,每次调用test1函数,局部变量a都会重新初始化为0x00;然后执行a++;而局部静态变量在调用过程中却能维持其值不变。
# m5 C6 P) F# b3 x: e$ e通常利用这个特性可以统计一个函数被调用的次数。
; B4 v7 W8 O( ]5 I1 d! @1 E( j v2 L1 E声明函数的一个局部变量,并设为static类型,作为一个计数器,这样函数每次被调用的时候就可以进行计数。这是统计函数被调用次数的最好的办法,因为这个变量是和函数息息相关的,而函数可能在多个不同的地方被调用,所以从调用者的角度来统计比较困难。代码如下:
1 K$ C, t$ d0 Q' o! B- Z% z5 H; x: c) ^) Q, C5 g: h
. N2 o4 I+ D6 x8 H& l
void count();9 E. U0 E' r& {. G4 P
int main()
$ Q" l/ {/ V m! \{- [8 N7 ?1 z; x4 J" `: p! d0 q
int i;2 p' \3 p2 P D# z) D6 l' I
for (i = 1; i <= 3; i++)
" ~3 \" k d X0 ~) P3 X {$ {" V+ I# _" I$ c
count();
% G+ ~$ l* @5 B# O B {- M( n1 U5 t: g
return 0;3 x0 s: Z5 \2 |. [8 N: N
}
. J, T6 _8 I# B8 _6 ivoid count()' f: u* d2 ^+ M% y. d; c
{7 x1 [# o/ w% y9 Z4 T
static num = 0;
* @ Z6 Q$ `! r; P) d+ H num++;% D; l9 Q$ R* M0 W
printf(" I have been called %d",num,"times/n");
+ C d" R) i7 T l( N' v}
1 N$ g# p1 j! F; b输出结果为:
% L. _4 v8 I( i. E$ L1 ~9 _I have been called 1 times.
X p. M9 P) G. II have been called 2 times.
& }- o6 z) @$ z% TI have been called 3 times.
* }5 l% w/ o3 z) m$ { # d+ C6 y C5 C3 Y/ a: e, Z
看一下局部静态变量的详细特性,注意它的作用域。! T5 B' S' e! u! e7 P
1)内存中的位置:静态存储区2 Z# I- K4 [* d0 l- ~! Q/ f$ b
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)! c0 X/ y* C+ v3 X" B: ^
3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。
) L# ^# }5 o N* v 注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置,从原来的栈中存放改为静态存储区。但是局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问。
0 c# y1 w* Z; j, ^ 5 s, y4 x; i* b) `3 l. {9 t
作用二:在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。% \* e) e, \/ m6 \* n# s6 t
这样定义的变量也称为全局静态变量:在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。也就是上述作用二中提到的在模块内(但在函数体外)声明的静态变量。
+ f" v) B; Q: ~3 Z定义全局静态变量的好处:
: e# @5 x4 q% l/ z& W- u; l<1>不会被其他文件所访问,修改,是一个本地的局部变量。
2 F h/ x% P7 F6 y J<2>其他文件中可以使用相同名字的变量,不会发生冲突。
8 P% X d7 `& s) l, S; U4 d0 N全局变量的详细特性,注意作用域,可以和局部静态变量相比较:
c2 L* ^. E; d8 v, @/ Q6 n5 A& \; f1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
; |& a/ h) u/ N7 S+ I' n0 J. A 2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)2 D. ~) ~+ j. C2 M7 O
3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
8 d3 Z- o2 e- s当static用来修饰全局变量的时候,它就改变了全局变量的作用域(在声明他的文件之外是不可见的),但是没有改变它的存放位置,还是在静态存储区中。
0 C) E7 ^3 K) U: ~3 }2 b
; Q3 S( R& Q8 `6 J作用三:在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
1 f* w5 c$ X/ C5 J p8 k! V ^2 x! l. H2 J, f
这样定义的函数也成为静态函数:在函数的返回类型前加上关键字static,函数就被定义成为静态函数。函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
1 {/ O3 U6 z9 Q- s4 c) `定义静态函数的好处:! a$ r" b! x/ {
<1> 其他文件中可以定义相同名字的函数,不会发生冲突2 M' I5 M2 {/ X; O) G) m9 Q
<2> 静态函数不能被其他文件所用。它定义一个本地的函数。
/ ]( e- v+ r3 A$ B; x8 P这里我一直强调数据和函数的本地化,这对于程序的结构甚至优化都有巨大的好处,更大的作用是,本地化的数据和函数能给人传递很多有用的信息,能约束数据和函数的作用范围。在C++的对象和类中非常注重的私有和公共数据/函数其实就是本地和全局数据/函数的扩展,这也从侧面反应了本地化数据/函数的优势。% i* @1 z; I* s# G
& p1 v& u( k% N! b) J; g最后说一下存储说明符,在标准C语言中,存储说明符有以下几类:4 [4 i: |1 a y. l" V
auto、register、extern和static9 B! d$ T( a3 C$ B1 H
对应两种存储期:自动存储期和静态存储期。 m: _+ B9 S' P3 [: Q% c
auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。
0 G P4 R$ N6 |" J1 |9 H7 ^/ W关键字extern和static用来说明具有静态存储期的变量和函数。用static声明的局部变量具有静态存储持续期(static storage duration),或静态范围(static extent)。虽然他的值在函数调用之间保持有效,但是其名字的可视性仍限制在其局部域内。静态局部对象在程序执行到该对象的声明处时被首次初始化。% Q1 @9 Y- t! K; q+ K% I- _
2. const 关键字
/ u5 r5 z4 B t( P" @' O7 `3 f! ]const关键字也是一个优秀程序中经常用到的关键字。关键字const 的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。合理地使用关键字const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
0 k! m% x/ R1 u; q& T深入理解const关键字,你必须知道:) Y! V2 S& Z0 n% s3 G, d
a. const关键字修饰的变量可以认为有只读属性,但它绝不与常量划等号。如下代码:
* e% \0 R! w) @$ Vconst int i=5; ?# x( J, \1 x; o' ~% @$ y
int j=0;5 m' L/ M. R( J. j5 e% O
...
0 E* D4 V1 W, Q* L, M) ?( b; { i=j; //非法,导致编译错误,因为只能被读$ a) m* s8 s% x. p1 r
j=i; //合法
1 N# v, [: L, l' L% e0 N3 Kb. const关键字修饰的变量在声明时必须进行初始化。如下代码:8 G( x7 b6 f# e6 g8 Y( U2 P- ?
const int i=5; //合法! o2 a) V5 w. L1 x+ G
const int j; //非法,导致编译错误
X; X/ R. Q* H3 V3 d% Dc. 用const声明的变量虽然增加了分配空间,但是可以保证类型安全。const最初是从C++变化得来的,它可以替代define来定义常量。在旧版本(标准前)的c中,如果想建立一个常量,必须使用预处理器:) x7 t6 h; j- c, b( b
#define PI 3.14159
/ K* M% H8 O o. J8 H. v$ x) Q# F此后无论在何处使用PI,都会被预处理器以3.14159替代。编译器不对PI进行类型检查,也就是说可以不受限制的建立宏并用它来替代值,如果使用不慎,很可能由预处理引入错误,这些错误往往很难发现。而且,我们也不能得到PI的地址(即不能向PI传递指针和引用)。const的出现,比较好的解决了上述问题。+ {4 n; E8 X, H! a4 {
d. C标准中,const定义的常量是全局的。' R+ g z6 {5 L- q, D
e. 必须明白下面语句的含义,我自己是反复记忆了许久才记住,方法是:若是想定义一个只读属性的指针,那么关键字const要放到‘* ’后面。
6 \1 j% A# E) g. K9 \char *const cp; //指针不可改变,但指向的内容可以改变
9 D& _# W2 R5 ~- L4 S7 d& I7 K$ X9 zchar const *pc1; //指针可以改变,但指向的内容不能改变
, s, {! \4 k1 b& r9 tconst char *pc2; //同上(后两个声明是等同的)( g5 w$ o$ V2 D$ s+ |6 X
f. 将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。' @! ]5 O B8 t9 k* H
参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。例子:
- I$ s U$ r2 j6 @/ |void fun0(const int * a );
) r8 o$ G* U V" N* M0 i) V- S( pvoid fun1(const int & a);
+ s; C' i `- v6 X6 ?& y- y调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const int * a,则不能对传递进来的指针所指向的内容进行改变,保护了原指针所指向的内容;如形参为const int & a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。6 @4 O7 K: `% E. J; e' D) ~8 {% X
g. 修饰函数返回值,可以阻止用户修改返回值。(在嵌入式C中一般不用,主要用于C++)
* v- R( u% ~: S6 C) X h. const消除了预处理器的值替代的不良影响,并且提供了良好的类型检查形式和安全性,在可能的地方尽可能的使用const对我们的编程有很大的帮助,前提是:你对const有了足够的理解。7 d X: D3 s& R8 O
最后,举两个常用的标准C库函数声明,它们都是使用const的典范。
$ o( \/ ~6 @9 d2 `+ N- Z6 j 1.字符串拷贝函数:char *strcpy(char *strDest,const char *strSrc);
$ C ~: q; V" v0 H1 b% u 2.返回字符串长度函数:int strlen(const char *str);0 { R3 {* \* M
9 E* g3 e: P& H6 E d2 m( b3. volatile关键字
# P& L6 C' L& K z2 f- q( B一个定义为volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。% D6 M5 `- U' w& m. q
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
7 ]! G+ ^0 V* Jstatic int i=0;
. {% w+ G' L1 \, s0 W
0 L! _# _! G2 b- r( `
5 r C4 R; ~& d( Z1 p* I cint main(void)
% G, ^3 {! n- n- |# y) W{ ; }) I& }8 R' ~) b5 l
... ; f8 c, s& i0 V
while (1) + F, G4 J5 b6 a+ G& z* q# [/ K" y" D
{
9 |' C+ s e9 {$ f7 ~7 D/ \ if (i) - ^6 ]7 a5 @+ }: O6 Q
dosomething(); 0 ^9 I) y9 z- U
}
j* N2 ~6 Q+ b' }" [( C7 x} # q" {4 i5 a9 x0 d/ ?; ^
3 E5 p8 Q; d% d# o# @
$ d7 \( i0 c2 ?% T, ]/* Interrupt service routine. */ h# c/ @3 T: i# z1 O9 i
void ISR_2(void) % w8 l6 W, f, J* a z
{ ' y8 V9 O' Y1 v2 G# j
i=1; ' p# p+ X: c8 E$ G$ O$ z/ K+ O0 T/ n
} ! Q3 O& _2 T1 r5 |, b
^. K7 I6 S$ Q' {$ b! h* w1 S7 C1 ?" _+ Y6 s
程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。 u+ i1 e$ z( j0 w
2 s/ Z8 ?* E- y& |1 y1 }
. T0 v i3 c2 D# m3 Q4 Y* l2 i+ i
如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。 ; K W: Q# ]5 F# Q8 ~8 d, H
) M m4 k& {( U# |" V! f. |1 \
/ B7 Z+ w# i( S& ~# |
一般说来,volatile用在如下的几个地方:
0 X, k7 C6 T, j: d2 V$ |8 B' S1 H: {
% U/ p: q( c9 T
( o5 A- j. b3 f" S3 h/ B7 \1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
A0 l1 J+ i C- e7 \4 d
9 P' q3 p. g& { V: m/ [1 o, Y7 E0 k# o! E, Q& Q' S
2、多任务环境下各任务间共享的标志应该加volatile;
9 ~, ?3 s/ e' M; o( t9 A I) r1 w) f0 x; `
& v: b5 D$ v9 K/ {* n9 q0 v3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;
0 D+ m- n- }% O# l不懂得volatile 的内容将会带来灾难,这也是区分C语言和嵌入式C语言程序员的一个关键因素。为强调volatile的重要性,再次举例分析:
; Q# Y7 r# W3 x; Q代码一: $ @# V, r' R; T( e3 d A. J
int a,b,c;
- G7 E9 F {& |; h8 y//读取I/O空间0x100端口的内容 9 h2 A l+ {7 r0 Z
a= inword(0x100);
8 R# m5 G; g5 }. ^6 L9 J0 d: \% ab=a; ( [3 N- } j! c2 I# x, ^! m4 E1 _
a=inword(0x100)
( g3 w( ?: Z1 L$ rc=a;
& h9 ?6 u1 K4 a代码二:
. N; a( n5 U: C& [' L8 W; r: L" J volatile int a; 9 G1 M N7 Z0 l, L: w
int a,b,c; & b) w8 H9 ?, J w: q
//读取I/O空间0x100端口的内容 : ^8 k% B$ k! [
a= inword(0x100);
6 ]% a4 F/ m% @# a& m/ y5 Rb=a; 0 q" S5 O/ P8 m( p
a=inword(0x100) # {: ?9 h( V4 K9 V( C7 o: W. N
c=a;
- G( U# @5 ]( m' R在上述例子中,代码一会被绝大多数编译器优化为如下代码:. z: f0 r* t; n
a=inword(0x100)) C8 j: ^% W3 e+ N" ?1 s
b=a;3 N1 g U' L5 W5 A* x7 e. J
c=a;- w! e0 x0 h( U& r( w" @3 g) m
这显然与编写者的目的不相符,会出现I/O空间0x100端口漏读现象,若是增加volatile,像代码二所示的那样,优化器将不会优化掉任何代码.6 D8 @' Y+ T3 i. o @
从上面来看,volatile关键字是会降低编译器优化力度的,但它保证了程序的正确性,所以在适合的地方使用关键字volatile是件考验编程功底的事情.# P2 [# e- x. u9 v
' a$ K3 B: J# [" F* C4.struct与typedef关键字
6 R( Y% c5 \/ s9 t! V2 q$ H, r6 T j面对一个人的大型C/C++程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是否具备丰富开发经历的标志。( O3 U' _# _. ?0 T3 L+ T
在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
5 N& Z+ \) ^ U经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改。
) I5 B* m# O- v8 q! r4 J用法:/ p6 ?8 o- _ d+ C
在C中定义一个结构体类型要用typedef:4 L4 ^4 n- v1 m( J5 N
typedef struct Student( K. Z. O' ~; Y9 l4 c( `% I# f- t9 S
{- A3 k! B" ]% K1 O; K8 Y
int a;6 F: f* [- r/ y1 [5 `
}Stu;# z. T0 ~! C+ e J
于是在声明变量的时候就可:Stu stu1;* j# f! K8 z/ o9 ~$ k
如果没有typedef就必须用struct Student stu1;来声明
2 M( O& w1 Z, J4 O! K这里的Stu实际上就是struct Student的别名。 T/ O6 K+ i; a1 k7 _& [ [
另外这里也可以不写Student(于是也不能struct Student stu1;了)
" A4 r+ m8 _" S) A, @3 L% G# ^+ ytypedef struct3 E/ Y6 i$ d1 f5 L1 R+ D% G
{/ Z) m; t4 |: i7 I7 a
int a;
$ P. t3 V t' @ R, U}Stu;
. ]/ b. O) W- C8 C, t) gstruct关键字的一个总要作用是它可以实现对数据的封装,有一点点类似与C++的对象,可以将一些分散的特性对象化,这在编写某些复杂程序时提供很大的方便性.: }, G$ g( e% I3 _8 v1 Y
比如编写一个菜单程序,你要知道本级菜单的菜单索引号、焦点在屏上是第几项、显示第一项对应的菜单条目索引、菜单文本内容、子菜单索引、当前菜单执行的功能操作。若是对上述条目单独操作,那么程序的复杂程度将会大到不可想象,若是菜单层数少些还容易实现,一旦菜单层数超出四层,呃~我就没法形容了。若是有编写过菜单程序的朋友或许理解很深。这时候结构体struct就开始显现它的威力了:! M! J5 b ?( C# }& C3 I0 T
//结构体定义4 I A2 p3 }2 R
typedef struct
1 Y* Y; x1 a: l{6 N3 U2 W' u O A- Z, e
unsigned char CurrentPanel;//本级菜单的菜单索引号
9 h) g$ j6 T5 o% b" Q* H& x' W6 Tunsigned char ItemStartDisplay; //显示第一项对应的菜单条目索引" r5 K2 J& J/ G* N/ \7 B) b
unsigned char FocusLine; //焦点在屏上是第几项
& y% m- L: ^! p: B3 l, y; V}Menu_Statestruct;5 _( M9 G4 \7 I( z
- R+ W5 d- Y5 k, Y& u
typedef struct
9 q6 \5 e6 T* G& L{
# f/ } d1 D( ~( eunsigned char *MenuTxt; //菜单文本内容
: Y# r) D. t' o' g( z uunsigned char MenuChildID;//子菜单索引& E+ m+ f; i5 h) L \6 @ Q
void (*CurrentOperate)();//当前菜单执行的功能操作, y! D q( `: o5 [1 J# X! r! O
}MenuItemStruct;
3 `: `6 W2 u0 H1 i& R 4 S% A" g) O8 }' {6 z! [5 k
typedef struct$ ]; A4 O* k* u Y
{. q2 N/ P5 _* f
MenuItemStruct *MenuPanelItem;
# C/ a a. X% W6 uunsigned char MenuItEMCount;
) {, |% B8 u3 x* ]# S' o# V: f}MenuPanelStruct;; b s: `% |/ M6 X- _1 B
6 d! o& G" ]; n: ]. u, ]
|
|