嵌入式C关键字
首先需要掌握标准C程序的组成。标准C程序一直由下列部分组成:
1)正文段——CPU执行的机器指令部分,也就是你的程序。一个程序只有一个副本;只读,这是为了防止程序由于意外事故而修改自身指令;
2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量,存放在这
首先需要掌握标准C程序的组成。标准C程序一直由下列部分组成:
1)正文段——CPU执行的机器指令部分,也就是你的程序。一个程序只有一个副本;只读,这是为了防止程序由于意外事故而修改自身指令;
2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量,存放在这里。
3)非初始化数据段(bss段)——在程序中没有初始化的全局变量;内核将此段初始化为0。
注意:只有全局变量被分配到数据段中。
4)栈——增长方向:自顶向下增长;自动变量以及每次函数调用时所需要保存的信息(返回地址;环境信息)。这句很关键,常常有笔试题会问到什么东西放到栈里面就足以说明。
5)堆——动态存储分配。 (用链表进行管理)
在标准C语言中,存储说明符有以下几类:auto、register、extern和static
函数里面声明变量默认就是auto
对应两种存储期:自动存储期和静态存储期。
auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。
关键字extern和static用来说明具有静态存储期的变量和函数。用static声明的局部变量具有静态存储持续期(static storage duration),或静态范围(static extent)。虽然他的值在函数调用之间保持有效,但是其名字的可视性仍限制在其局部域内。静态局部对象在程序执行到该对象的声明处时被首次初始化。
【1】static
1)限制变量或者函数的作用域
2)设置变量的存储域
常见的有:
(1)static局局变量(2)static全部变量(3)static函数
(1)static局部变量(函数体内的局部变量):1、static局部变量只被初始化一次,下一次依据上一次结果值;2、限制了它的使用范围。位于函数体中的静态变量在多次函数调用间会维持其值 (延长了局部变量的生命周期)
(2)static全局变量:1、static全局变量只初使化一次,防止在其他文件单元中被引用;2、 只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。(限制作用域)位于模块内(但在函数体外)的静态变量可以被模块内的所有函数访问,但不能被模块外其他函数访问。也就是说,它是一个本地的全局变量。
(3)static函数:1、static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。2、static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。位于模块内的静态函数只能被此模块内的其他函数调用。也就是说,这个函数的作用域为声明所在的模块。
第一种情况,static延长了局部变量的生命周期,static的局部变量,并不会随着函数的执行结束而被销毁,当它所在的函数被第再次执行时,该静态局部变量会保留上次执行结束时的值。如:
[code]
1 #include <stdio.h>
2
3 void test()
4 {
5 static int j = 1; //这个变量在函数退出后,不会被回收,依然存在
6 printf("%d\n", j);
7 j++;
8 }
9 int main()
10 {
11 test();
12 test();
13
14 return 0;
15 }
输出的结果是:
1
2
对于后面的两种情况,static是对它修饰的对象进行了作用域限定,static修饰的函数以及函数外的变量,都是只能在当前的代码文件中被访问,其它的文件不能直接访问。当多个模块中有重名的对象出现时,不妨把它们用static进行修饰。
强调数据和函数的本地化对于程序的结构甚至优化都有巨大的好处,更大的作用是,本地化的数据和函数能给人传递很多有用的信息,能约束数据和函数的作用范围。在C++的对象和类中非常注重的私有和公共数据/函数其实就是本地和全局数据/函数的扩展,这也从侧面反应了本地化数据/函数的优势。
【2】extern:
extern可置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。另外,extern也可用来进行链接指定。
【3】const:
(1)可以保护被修饰的东西防止意外的修改,增强程序的健壮性。
(2)编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,
这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
(3)const定义的常量在程序运行过程中只有一份拷贝
a. const关键字修饰的变量可以认为有只读属性,但它绝不与常量划等号。如下代码:
const int i=5;
int j=0;
...
i=j; //非法,导致编译错误,因为只能被读
b. const关键字修饰的变量在声明时必须进行初始化。如下代码:
const int i=5; //合法
const int j; //非法,导致编译错误
c. 用const声明的变量虽然增加了分配空间,但是可以保证类型安全。const最初是从C++变化得来的,它可以替代define来定义常量。在旧版本(标准前)的c中,如果想建立一个常量,必须使用预处理器:
#define PI 3.14159
此后无论在何处使用PI,都会被预处理器以3.14159替代。编译器不对PI进行类型检查,也就是说可以不受限制的建立宏并用它来替代值,如果使用不慎,很可能由预处理引入错误,这些错误往往很难发现。而且,我们也不能得到PI的地址(即不能向PI传递指针和引用)。const的出现,比较好的解决了上述问题。
d. C标准中,const定义的常量是全局的。
e. 必须明白下面语句的含义
作为一个程序员,我们看到关键字const时,首先想到的应该是:只读。因为,它要求其所修饰的对象为常量,不可对其修改和二次赋值操作(不能作为左值出现)。看几个例子:
[code]
const int a; int const a;//同上面的代码行是等价的,都表示一个常整形数。 int *const a;//const具有"左结合"性,即const修饰*,那么,不难理解,该句表示一个指向整数的常指针,a指向的整数可以修改,但指针a不能修改。 const int *a;//与下面的这一行等价,根据"左结合"性,const修饰的是(*a),
//也即是一个整数,所以,这两句表示指针指向一个常整数。 int const *a; int const *a const;//根据"左结合"性质,第一个const修饰(*),第二个const修饰(a),因此,这句话表示一个指向常整数的常指针。
合理的使用const关键字,不仅能够让编译器很好的保护相应的数据,还能够直观的向代码的阅读者传递有用信息。
为了迅速弄清语句表达的含义,参考文献[1]介绍了一种简便的方法,其要点就是“逆序读出定义”,如图1所示。
f. 将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。
参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。例子:
void fun0(const int * a );
void fun1(const int & a);
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const int * a,则不能对传递进来的指针所指向的内容进行改变,保护了原指针所指向的内容;如形参为const int & a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
g. 修饰函数返回值,可以阻止用户修改返回值。(在嵌入式C中一般不用,主要用于C++)
h. const消除了预处理器的值替代的不良影响,并且提供了良好的类型检查形式和安全性,在可能的地方尽可能的使用const对我们的编程有很大的帮助,前提是:你对const有了足够的理解。
最后,举两个常用的标准C库函数声明,它们都是使用const的典范。
1.字符串拷贝函数:char *strcpy(char *strDest,const char *strSrc);
2.返回字符串长度函数:int strlen(const char *str);
【4】volatile:
定义:一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
做嵌入式设备开发,如果不对volatile修饰符具有足够了解,实在是说不过去。volatile是C语言32个关键字中的一个,属于类型限定符,常用的const关键字也属于类型限定符。
volatile限定符用来告诉编译器,该对象的值无任何持久性,不要对它进行任何优化;它迫使编译器每次需要该对象数据内容时都必须读该对象,而不是只读一次数据并将它放在寄存器中以便后续访问之用(这样的优化可以提高系统速度)。
这个特性在嵌入式应用中很有用,比如你的IO口的数据不知道什么时候就会改变,这就要求编译器每次都必须真正的读取该IO端口。这里使用了词语“真正的读”,是因为由于编译器的优化,你的逻辑反应到代码上是对的,但是代码经过编译器翻译后,有可能与你的逻辑不符。你的代码逻辑可能是每次都会读取IO端口数据,但实际上编译器将代码翻译成汇编时,可能只是读一次IO端口数据并保存到寄存器中,接下来的多次读IO口都是使用寄存器中的值来进行处理。因为读写寄存器是最快的,这样可以优化程序效率。与之类似的,中断里的变量、多线程中的共享变量等都存在这样的问题。
不使用volatile,可能造成运行逻辑错误,但是不必要的使用volatile会造成代码效率低下(编译器不优化volatile限定的变量),因此清楚的知道何处该使用volatile限定符,是一个嵌入式程序员的必修内容。
一个程序模块通常由两个文件组成,源文件和头文件。如果你在源文件定义变量:
unsigned int test;
并在头文件中声明该变量:
extern unsigned long test;
编译器会提示一个语法错误:变量’ test’声明类型不一致。但如果你在源文件定义变量:
volatile unsigned int test;
在头文件中这样声明变量:
extern unsigned int test; /*缺少volatile限定符*/
编译器却不会给出错误信息(有些编译器仅给出一条警告)。当你在另外一个模块(该模块包含声明变量test的头文件)使用变量test时,它已经不再具有volatile限定,这样很可能造成一些重大错误。比如下面的例子,注意该例子是为了说明volatile限定符而专门构造出的,因为现实中的volatile使用Bug大都隐含,并且难以理解。
在模块A的源文件中,定义变量:
volatile unsigned int TimerCount=0;
该变量用来在一个定时器中断服务程序中进行软件计时:
TimerCount++;
在模块A的头文件中,声明变量:
extern unsigned int TimerCount; //这里漏掉了类型限定符volatile
在模块B中,要使用TimerCount变量进行精确的软件延时:
1. #include “…A.h” //首先包含模块A的头文件
2. //其他代码
3. TimerCount=0;
4. while(TimerCount<=TIMER_VALUE); //延时一段时间(感谢网友chhfish指出这里的逻辑错误)
5. //其他代码
实际上,这是一个死循环。由于模块A头文件中声明变量TimerCount时漏掉了volatile限定符,在模块B中,变量TimerCount是被当作unsigned int类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时是先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount<=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。图3-1的流程图说明了程序使用限定符volatile和不使用volatile的执行过程。
为了更容易的理解编译器如何处理volatile限定符,这里给出未使用volatile限定符和使用volatile限定符程序的反汇编代码:
-
没有使用关键字volatile,在keil MDK V4.54下编译,默认优化级别,如下所示(注意最后两行):
122: unIdleCount=0;
123:
0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
0x00002E18 E1A00005 MOV R0,R5
0x00002E1C E5815000 STR R5,[R1]
124: while(unIdleCount!=200); //延时2S钟
125:
0x00002E20 E35000C8 CMP R0,#0x000000C8
0x00002E24 1AFFFFFD BNE 0x00002E20</span>
-
使用关键字volatile,在keil MDK V4.54下编译,默认优化级别,如下所示(注意最后三行):
122: unIdleCount=0;
123:
0x00002E10 E59F01D4 LDR R0,[PC,#0x01D4]
0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
0x00002E18 E5805000 STR R5,[R0]
124: while(unIdleCount!=200); //延时2S钟
125:
0x00002E1C E5901000 LDR R1,[R0]
0x00002E20 E35100C8 CMP R1,#0x000000C8
0x00002E24 1AFFFFFC BNE 0x00002E1C
可以看到,如果没有使用volatile关键字,程序一直比较R0内数据与0xC8是否相等,但R0中的数据是0,所以程序会一直在这里循环比较(死循环);再看使用了volatile关键字的反汇编代码,程序会先从变量中读出数据放到R1寄存器中,然后再让R1内数据与0xC8相比较,这才是我们C代码的正确逻辑!
【5】struct与typedef
面对一个人的大型C/C++程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是否具备丰富开发经历的标志。
在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改。
用法:
在C中定义一个结构体类型要用typedef:
typedef struct Student
{
int a;
}Stu;
于是在声明变量的时候就可:Stu stu1;
如果没有typedef就必须用struct Student stu1;来声明
这里的Stu实际上就是struct Student的别名。
另外这里也可以不写Student(于是也不能struct Student stu1;了)
typedef struct
{
int a;
}Stu;
struct关键字的一个总要作用是它可以实现对数据的封装,有一点点类似与C++的对象,可以将一些分散的特性对象化,这在编写某些复杂程序时提供很大的方便性.
比如编写一个菜单程序,你要知道本级菜单的菜单索引号、焦点在屏上是第几项、显示第一项对应的菜单条目索引、菜单文本内容、子菜单索引、当前菜单执行的功能操作。若是对上述条目单独操作,那么程序的复杂程度将会大到不可想象,若是菜单层数少些还容易实现,一旦菜单层数超出四层,呃~我就没法形容了。若是有编写过菜单程序的朋友或许理解很深。这时候结构体struct就开始显现它的威力了:
//结构体定义
typedef struct
{
unsigned char CurrentPanel;//本级菜单的菜单索引号
unsigned char ItemStartDisplay; //显示第一项对应的菜单条目索引
unsigned char FocusLine; //焦点在屏上是第几项
}Menu_Statestruct;
typedef struct
{
unsigned char *MenuTxt; //菜单文本内容
unsigned char MenuChildID;//子菜单索引
void (*CurrentOperate)();//当前菜单执行的功能操作
}MenuItemStruct;
typedef struct
{
MenuItemStruct *MenuPanelItem;
unsigned char MenuItemCount;
}MenuPanelStruct;
这里引用所写的菜单程序中的结构体定义,这个菜单程序最大可以到256级菜单。我当初要写一个菜单程序之前,并没有对结构体了解多少,也没有想到使用结构体。只是一层层菜单的单独处理:如果按键按下,判断是哪个按键,调用对应按键的子程序,刷屏显示。这样处理起来每一层都要判断当前的光标所在行,计算是不是在显示屏的顶层,是不是在显示层的底层,是不是需要翻页等等,非常的繁琐。后来在网上找资料,就找到了我师兄编写的这个程序,开始并不知道是他写的,在看源程序的时候看到了作者署名:中国传惠TranSmart。当定义了上述三个结构体之后,菜单程序结构立刻变的简单很多,思路也无比的清晰起来。
基于结构体的多级菜单:https://download.youkuaiyun.com/download/vbvcde/2541605