`

嵌入式C语言笔记03——函数指针,内存陷阱,堆栈

阅读更多
嵌入式C语言笔记03——函数指针,内存陷阱,堆栈

函数指针
1. 函数指针的声明与引用
函数指针即指向函数地址的指针。利用该指针可以知道函数在内存中的位置。因此,也可以利用函数指针调用函数。
<类型> (*函数指针变量名) 函数的参数列表

在C语言中,正如数组名就是数组第一个元素的首地址,函数名就是函数的入口地址,因此可以用已定义的函数的函数名作为初值赋给一个相应的函数指针。
程序员可以通过函数指针调用函数,当然程序员必须保证这个函数指针是有初值的。

int * function(int);
int *(*fp)(int);
int *ptr;

fp = function;  //为函数指针fp赋值,使他指向函数funtion()
ptr=(*fp)(3);  //通过函数指针调用函数,与funtion(3)效果一样。
ptr=fp(4);  //这也是通过函数指针调用函数,与(*fp)(4)的效果一样
2. 函数指针的作用
1) 多态
多台指用一个名字定义不同的函数,这函数执行不同但类似的操作,从而实现“一个接口,多种方法”。利用函数指针实现多态是很多系统软件常用的方法,比如再操作系统中为了能够支持不同硬件设备的统一管理,往往会定义一个内部的数据结构,这个结构中定义了具体的硬件操作函数的函数指针。当然针对不同的硬件设备,这些函数指针指向不同的操作函数。当上层软件需要访问某个设备时,操作系统将根据这个数据结构调用不同的操作函数,这就使得虽然底层的操作函数不相同,但是上层软件却可以统一。
如Linux内核中的file_operations结构

struct file_operations {
       struct module *owner;
       loff_t (*llseek) (struct file *, loff_t, int);
       ssize_t (*read) (struct file *, char *, size_t, loff_t *);
       ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
       int (*readdir) (struct file *, void *, filldir_t);
       unsigned int (*poll) (struct file *, struct poll_table_struct *);
       int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
       int (*mmap) (struct file *, struct vm_area_struct *);
       int (*open) (struct inode *, struct file *);
       int (*flush) (struct file *);
       int (*release) (struct inode *, struct file *);
       int (*fsync) (struct file *, struct dentry *, int datasync);
       int (*fasync) (int, struct file *, int);
       int (*lock) (struct file *, int, struct file_lock *);
         ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
          loff_t *);
         ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
          loff_t *);
    };
结构中的每一个成员名字都对应一个系统调用。
2) 回调(call-back)
通常情况下的函数调用顺序是用户的函数调用操作系统的函数,上层的函数调用底层的函数,而所谓回调是指由操作系统来调用用户编写的函数。由于操作系统代码在用户代码之前就已经编译完成,因此由操作系统法器的回调一般都必须通过将用户编写函数的函数指针传递给操作系统,再由操作系统实现回调。
3) 多线程
将函数指针传进负责建立多线程的API中。在一个多任务系统中,每个任务从本质商讲可以理解为是一个拥有自己独立堆栈的函数,在用户需要穿件一个新任务或是线程时,需要调用由操作系统提供的API函数。
在μC-OS/II中,这个创建函数一般需要为新任务创建1的任务控制块(TCB)并未其申请专属于该任务的堆栈控件,然后将任务的入口地址作为返回地址填写到任务堆栈中,构建一个新的堆栈。创建函数将任务堆栈的当前指针填写到TCB的一个域中,将任务的状态置为就绪并且链接到响应的就绪队列中,这时如果操作系统允许任务调度,则调用调度器选择合适的任务进行运行。如果调度器选择了新创建的任务运行,则只要根据TCB中的堆栈指针就可以模拟出一个终端返回的出栈过程,新的任务得到CPU的过程就方法是从一个终端中返回一样。
内存陷阱
C语言中对内存的分配方式主要有4种:
1) 从静态存储区域分配。内存在程序编译时就已经分配好了。
2) 在栈上创建。在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数结束时这些存储单元自动被释放掉
3) 从堆上分配,亦称动态内存分配。
4) 系统程序员清楚地知道系统中每个程序单元在存储器的位置,程序员通过绝对地址对这些存储器控件进行访问

1. 局部变量的注意要点
1) 不要对临时变量作取地址操作,你永远不知道编译器把这个变量放到了哪儿,或许在寄存器中呢。
2) 不要返回临时变量的地址或临时指针变量,因为堆栈中的内容是不确定的!
3) 不要申请大的临时变量数组或结构,临时变量是在堆栈中实现,嵌入式的堆栈空间没你想象的那么大
2. 动态存储区的注意要点
1) 在差错处理时,忘了释放已分配的动态内存空间,比如下例
char *fun(){
	char *p,*q;
	if((p=malloc(1024))== NULL) return NULL;
	if((q=malloc(1024))== NULL) return NULL;

	return p;
}
 
程序中如果p申请成功,q没有申请成功,那么给p的空间将永远消失了。

2) 一定要保存好malloc()函数返回的动态内存区的首指针,这是我们正确释放这块内存的必要条件。
3) 避免在访问动态内存区时发生数据溢出的情况,程序员要特别小心数组的访问越界以及strcpy()等标准库函数在往动态分配的存储区写数据时的边界条件。因为对这块动态存储区的写越界不仅有可能破坏其他的动态存储区的数据,也有可能破坏相邻动态存储区的头部信息,从而造成free()函数的失败。
4) 对于团队开发的情况,应该本着“谁申请谁释放”的原则。
5) 对于被释放的动态内存区,最好立刻将指向这块内存区的指针变量赋值为NULL,这样可以避免继续对这个指针的引用而造成“野”指针
6) 使用sizeof来计算结构体的大小;分配内存时宁滥勿缺。

堆栈
对于传统CISC处理器对于堆栈的操作有专门的压栈与退栈的指令,并且处理器的硬件会自动完成函数调用与终端处理的返回地址入栈的做。但由于RISC处理器通常采用Load/Store体系,也就是除了Load/Store两类指令外,其他所有的指令都不能访问存储器,处理器的硬件也不会自动地完成堆栈的入栈和出栈操作,比如ARM就需要在最后用MOV PC,LR。
堆栈的作用:
1. 利用堆栈传递函数调用的参数
C语言的参数压栈顺序是从右到左,对于一些RISC处理器的C编译器来说,会首先采用CPU内部的通用寄存器进行传参,如ARM会使用r0到r3来进行,多于4个时才采用堆栈进行函数调用的参数传递。
2. 利用堆栈保存函数调用的返回地址
3. 利用堆栈保存在被调函数中需要使用的寄存器的值
4. 利用堆栈实现局部变量
编译器的压栈过程:
1. 先压参数
2. 再压程序的返回地址
3. 将被调用函数中可能用到的寄存器的值保存起来。
4. 保存被调函数的一些局部变量。
5. 函数的返回值x存放在r0中(ARM),由r0来传递给被调函数。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics