8、嵌入式C语言的高级用法
1、内存管理
内存的使用是程序设计中需要考虑的重要因素之一,这不仅由于系统内存是有限的(尤其在嵌入式系统中),而且内存分配也会直接影响到程序的效率。因此,读者要对C语言中的内存管理有个系统的了解。
在C语言中,定义了4个内存区间:
(1)、代码区:存放程序中的代码,属性是只读的
(2)、全局变量与静态变量区(静态存储区域):内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。
(3)、栈区:在执行函数时,函数内部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。这种内存方式,变量内存的分配和释放都自动进行,程序员不需要考虑内存管理的问题,很方便使用。但缺点是,栈的容量有限,且当相应的范围结束时,局部变量就不能在使用。
(4)、堆区:有些操作对象只有在程序运行是才能确定,这样编译器在编译时就无法为他们预先分配空间,只能在程序运行时分配,所以称为动态分配。
2、动态内存的申请和释放
当程序运行到需要一个动态分配的变量时,必须向系统申请取得堆中的一块所需大小的存储空间,用于存储该变量。当不再使用该变量时,也就是他的生命结束时,要显示释放它所占用的存储空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。
2.1、malloc函数
在C语言中,使用malloc函数来申请内存
#include <stdlib.h>
Void *malloc(size_t size);
Size代表要动态申请的内存的字节数。若内存申请成功,函数返回申请到的内存的起始地址,若申请失败,返回NULL。使用该函数是,要注意以下几点:
(1)、只关心申请内存的大小。该函数的参数很简单,只有申请内存的大小,单位是字节。
(2)、申请的是一块连续的内存。该函数一定是申请一块连续的内存,可能申请到的内存比实际申请的大。也可能申请不到,若申请失败,返回NULL。一定要记得写出错判断。
(3)、返回值类型是void *。函数的返回值是void *,不是某种具体类型的指针。读者可以理解成,该函数只是申请内存,对在内存中存储什么类型的数据没有要求。因此返回值是void*。在实际编程中,根据实际情况,将void *转换成所需要的指针类型。
(4)、显示初始化。注意,堆区是不会自动在分配时做初始化的(包括清零),所以程序中需要显示的初始化。
2.2、free函数
在堆区上分配的内存,需要free函数显示释放。函数原型如下:
#include <stdlib.h>
Void free(void *ptr);
函数的参数ptr,指的是需要释放的内存的起始地址。该函数没有返回值,使用该函数,也有下面几点需要注意。
(1)、必须提供内存的起始地址。调用该函数时,必须提供内存的起始地址,不能提供部分地址,释放内存中的一部分是不允许的。因此,必须保存好malloc返回的指针值,若丢失,则所分配的堆空间无法回收,称内存泄漏。
(2)、malloc和free配对使用。编辑器不负责动态内存的释放,需要程序员显示释放。因此,malloc与free是配对使用的,避免内存泄漏。
(3)、不允许重复释放。同一空间的重复释放也是危险的,因为该空间可能已经另外分配
(4)、free只能释放堆空间。想代码区、全局变量和静态变量区、栈区上的变量,都不需要程序员显示释放,这些区域上的空间,不能通过free函数来释放,否则执行时会出错。
3、堆和栈的区别
3.1、申请方式
栈是由系统自动分配的。例如,声明函数中一个局部变量“int b;”,那么系统自动在栈中为b开辟空间。堆需要程序员自己申请,并在申请时指定大小。使用C语言中的malloc函数的例子如下所示:
P1=(char *)malloc(10);
3.2、申请后系统的响应
堆在操作系统中有一个记录空闲内存地址的链表。当系统收到程序的申请时,系统就会开始遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间的首地址处记录本次分配的大小。这样,代码中的删除语句才能正确的释放本内存空间。如果找到的堆节点的大小与申请的大小不相同,系统会自动地将多余的那部分重新放入空闲链表中。
只有栈的剩余空间大于所申请空间,系统才为程序提供内存,否则将报异常,提示栈溢出。
3.3、申请大小的限制
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统用链表来存储的空国内存地址,地址是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚视内存,因此堆获得的空间比较灵活,也比较大。
栈是向低地址扩展的数据结构,是-块连续的内存区域。因此,栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示栈溢出,因此,能从栈获得的空间较小。
3.4、申请速度的限制
堆是由malloc等语句分配的内存,一般速度比较慢, 而且容易产生内存碎片,不过用起来很方便。栈由系统自动分配,速度较快,但程序员-般无法控制。
3.5、堆和栈中的存储内容
堆一般在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。
在调用函数时,第一个进栈的是函数调用语句的下一条可执行语句的地址,然后是函数的各个参数,在大多数的C语言编译器中,参数是由右往左入栈的,然后是函数中的局部变量。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始的存储地址,也就是调用该函数处的下一条指令,程序由该点继续运行。
4、编译器优化介绍
由于内在访问速度远不及CPU处理速度,因此为提高计算机整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指今的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度,以上是硬件级别的优化。
软件级别的优化有两种:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有将内存变量缓存到寄存器和调整指令顺序充分利用CPU指令流水线等,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很高。
由编译器优化或者硬件重新排序引起的问题的解决办法是在特定顺序执行的操作之间设置内存屏障( memory barrier ), Linux提供了一个宏用于解决编译器的执行顺序问题。
void barrier(void)
主要是保证程序的执行遵循顺序一致性。有时候写代码的顺序,不一定是最终执行的顺序,这个是与处理器有关的。这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。
4、C语言关键字volatilo
C语言关键字volatile (注意它是用来修饰变量而不是上面介绍的_volatile_ ) 表明某个变量的值可能随时被外部改变(例如,外设端口寄存器值),因此对这些变量的存取不能缓存到寄存器。每次使用时需要重新读取。
该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。对于C语言编译器来说,它并不知道这个值会被其他线程修改,自然就把它缓存到寄存器里面。volatile 的本意是指这个值可能会在当前线程外部被改变,此时编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取。这个关键字在外设接口编程中经常被使用。
5、“memory"描述符
有了上面的知识就不难理解"memory"修改描述符了,“memory"描述符告知GCC以下内容:
(1)、不要将该段内嵌汇编指令与前面的指令重新排序,也就是说在执行内嵌汇编代码之前,它前面的指令都执行完毕。
(2)、不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。
如果汇编指令修改了内存,GCC本身其实察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加“memory", 告诉GCC内存已经被修改,GCC得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache而写到寄存器中的变量值先写回内存,如果以后又要使用这些变量,则再重新读取。当然,使用vlatile也可以达到这个目的。

浙公网安备 33010602011771号