嵌入式C关键字

本文深入探讨了嵌入式C语言中的关键字,包括static、extern、const、volatile的使用方法及其注意事项,同时介绍了struct与typedef的应用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

嵌入式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 }

输出的结果是: 

对于后面的两种情况,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

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值