首页 理论教育 堆与动态内存分配在ARM嵌入式C编程中实现

堆与动态内存分配在ARM嵌入式C编程中实现

时间:2023-10-19 理论教育 版权反馈
【摘要】:对于这种在堆中申请了内存而没有释放或者释放不成功,造成动态内存空间丢失,且最终可能导致再也申请不到动态内存空间的情况,称为内存泄漏。简单地说,造成内存泄漏的原因就是申请了内存空间,但是没有正确地释放,其主要问题有3个:没有释放动态内存空间:差错处理时,忘了释放已分配的动态内存空间。

堆与动态内存分配在ARM嵌入式C编程中实现

1.动态内存分配的含义

动态内存分配(Dynamic Memory Allocation)是指在程序执行的过程中动态地分配或者回收存储空间的内存分配方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

通常,编译器在编译时就可以根据变量的类型知道所需内存空间的大小,从而系统在适当的时候为它们分配确定的存储空间,这种方式称为静态内存分配。而有些数据块的大小只在程序运行时才能确定,这样编译时就无法为它们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方式称为动态内存分配。

例如定义一个float型数组:float ARMscore[200],这就在编译时确定和预留了200×sizeof(float)大小的空间(事实上,C语言在编译时需要确定数据变量的大小)。但是,在使用数组的时候,总有一个问题:数组应该多大?在很多情况下,并不能确定要使用多大的数组,比如上例,可能并不知道要定义的数组到底有多大,那么就要把数组定义得足够大,以避免后续访问时空间不够。这样,程序在运行时就申请了固定大小的足够大的内存空间。另一方面,即使知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,又必须重新修改程序,扩大数组的存储范围。这种静态内存分配的方法存在的缺陷是:在预留最大空间时会浪费大量的内存空间,而当定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。

但如果用动态内存分配的方式定义变量就能解决这个问题,可以看出动态内存分配相对于静态内存分配的两个特点:

(1)不需要预先分配和确定存储空间;

(2)分配的存储空间可以根据程序的需要在运行过程中扩大或缩小。

2.C语言中的malloc()与free()函数

在C语言中,动态存储区是由malloc()函数和free()函数管理的内存区域,这个存储区一般也称为堆(heap)。在嵌入式系统中的具体实现上,堆可以用一个静态数组表示,这个静态数组的内存空间在编译的时候由编译器分配,也可以由程序员指定一段没有被编译器和操作系统使用的空闲内存区域来实现。

malloc()函数和free()函数是ANSI C标准定义的标准库函数。不幸的是,malloc()函数的内部数据结构很容易被破坏,而由此引发的问题十分棘手。发生内存错误是非常麻烦的,编译器不能自动发现这些错误,通常在程序运行时才能捕捉到,并且这些错误大多没有明显的征状,时隐时现,增加了改错的难度。最常见的问题来源是向malloc()函数分配的区域写入比所分配的还多的数据,一个常见的bug是用malloc(strlen(s))而不是malloc(strlen(s)+1)。其他问题还包括使用指向已经释放了的内存的指针、释放未从malloc()函数获得的内存、两次释放同一个指针、试图重分配空指针等等。

malloc()函数的原型为:void∗malloc(unsigned int size)。其作用是在内存的堆中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储区域起始地址的指针。还有一点必须要注意的是,当函数未能成功分配存储空间(如内存不足)时就会返回一个NULL指针,所以在调用该函数时应该检测返回值是否为NULL并执行相应操作。下面列举一个动态内存分配的例子:

free()函数是malloc()函数的逆操作,free()函数的参数为将要释放的空间的指针。思考一下,free()函数只传入了首指针而没有存储空间的大小,操作系统如何知道需要释放多大的空间?其实,在调用malloc()函数时,操作系统或函数内部会默认在用户可用的物理内存前面加上一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的,是感知不到的。那么当用户需要free()函数时,free()函数会通过首指针退回到这个结构体中,并找到该内存块的大小,这样就可以正确地释放内存了。

3.动态内存分配的使用陷阱

1)内存泄漏

下面通过一段代码来分析内存泄漏。

这段代码申请了2个缓冲区,分别用指针p和q表示。现考虑一种情况:如果程序中p的1 024字节的空间分配成功,在分配q的2 048字节空间时malloc()函数未能分配成功,则按照上面的代码将直接return NULL;这时p所指向的1 024字节空间将永远被“遗忘”,再也不会有人去引用它或释放它,它将永远占据堆中的空间。对于这种在堆中申请了内存而没有释放或者释放不成功,造成动态内存空间丢失,且最终可能导致再也申请不到动态内存空间的情况,称为内存泄漏。

所以,上述程序的正确写法如下:

有些人可能会有一个错觉,上面的代码中指针p和q都是临时变量,当退出函数时p和q这两个变量将自动消亡,因此它们所指向的动态内存空间也将自动被释放。这是非常错误的知识,要知道p和q是两个指针变量,它们存放的只是两个地址,这些地址通过调用malloc()函数后被保存在p和q之中,它们并不是动态内存本身。因此,函数退出后,保存这些地址的p和q会消亡,但是它们指向的动态内存空间并不会自动被释放。

简单地说,造成内存泄漏的原因就是申请了内存空间,但是没有正确地释放,其主要问题有3个:

(1)没有释放动态内存空间:差错处理时,忘了释放已分配的动态内存空间。

(2)程序员沟通问题:由于程序复杂性的增加,现在的嵌入式软件往往采用团队开发模式,在程序员沟通的过程中,往往会出现一些误会与推诿。比如:A程序员在其代码中分配了一块内存,B程序员的代码将使用这块内存,但是他们没有沟通好到底由谁来释放这块内存,这就非常容易造成内存泄漏。

(3)动态内存空间释放不成功:这个问题最复杂,也最难被发现。由于free()函数是根据malloc()函数分配的空间头部的控制信息来进行释放的,因此free()函数只能释放由malloc()函数返回的指针所指向的内存空间。如果free()函数的入口参数不是正确的指针或者malloc()分配空间的头部信息被破坏(往往是因为其他人往动态内存空间写数据溢出造成的),这都将造成free()函数无法正确释放内存空间。

通过分析内存泄漏的原因,现提出几种避免内存泄漏的建议和方法:

(1)一般情况下,子函数通过malloc()函数分配的内存空间在所有return语句之前都应该通过free()函数及时地释放。(www.xing528.com)

(2)一定要保存malloc()函数返回的动态内存区首指针,这是正确释放这块内存的必要条件。

(3)避免在访问动态内存区时发生数据溢出的情况,程序员要特别小心数组的越界以及strcpy()、memcpy()、sprintf()等标准库函数在往动态分配的内存空间写数据时的边界条件。因为对这块动态内存空间的写越界不仅有可能破坏其他动态内存空间中的数据,也有可能破坏相邻动态内存空间的头部信息,从而造成free()函数的失败。

(4)对于团队开发的情况,应该本着“谁申请谁释放”的原则,也就是由A程序员申请的动态内存空间最好由A程序员负责释放,这样每个程序员各司其职,保证自己申请的动态内存空间在不需要时被正确释放。

2)“野”指针

“野”指针是指那些不知道指向什么内容或者指向的内容已经无效的指针。在这里需要注意的是,“野”指针并不是空指针NULL,空指针的物理含义是不指向任何内容,而“野”指针要么随机地指向一段内存区域,要么所指向的内容已经无意义。相对于空指针,“野”指针的问题要复杂得多。用一个if条件判断就可以非常简单地知道一个指针是否为空,但是在“野”指针面前,if判断显得无能为力。产生“野”指针的主要原因如下:

(1)指针在初始化之前就被直接引用。这个问题主要是针对局部变量,因为大多数编译器在处理全局变量时,会为全局变量静态分配内存空间,并且要么以程序中的初值对其进行初始化,要么以零对没有初值的全局变量进行初始化。比如ARM公司的ARMCC编译器就将全局变量分为两个段,一个是有初值全局变量的RW段,另一个是没有初值的全局变量的ZI(Zero Initialized)段。因此,对于全局指针变量不用担心初值的问题。但是对于局部变量就不同了,这是因为编译器要么用CPU的通用寄存器存储局部变量,要么用栈空间存储局部变量。不管哪一种,局部变量的初值都是随机的,对于局部指针变量而言,这就意味着没有用初值去初始化的这个指针可能指向任何地方,这就是“野”指针。

(2)一个合法的指针所指向的内存空间已经被释放了,但是这个指针的值并没有被置为NULL,如果通过这个指针继续访问这块已经被释放的内存空间,后果可能是非常危险的。请看下面这段代码:

这段代码似乎没有任何问题,但是请注意代码中的for循环,当程序释放了pwnd指针后进入下一次循环时,for循环语句中重新引用了pwnd指针:pwnd=pwnd->Sibling。pwnd所指向的内存空间已经被释放,但是又重新通过pwnd指针引用了其所指向的Sibling。这时已经不能保证pwnd所指向的内存空间的内容是否没有被其他代码所破坏,因此这个引用是非常危险的。

(3)返回局部变量的指针。一旦离开函数,局部变量所占用的栈空间将被退栈,其所表示的局部变量也将不复存在,因此返回这些局部变量的指针是没有意义的。如果程序通过这个指针继续访问栈中的内容,得到的结果是不能保证的。

3)规避动态内存分配的使用陷阱

动态内存分配是C语言中构建动态数据结构的关键,比如通过动态内存空间构建链表、树和图等。当然通过静态的数组同样可以构建这些数据结构,但是对于有一定使用周期的数据而言,在编程实践中更多的是采用动态内存空间,这样可以最大效率地利用有限的存储空间。但是使用动态内存空间会带来一系列潜在危险,比如前面所介绍的动态内存储空间的申请问题、内存泄漏和“野”指针问题等。如何规避动态内存空间的陷阱,是每一个程序员必须认真对待的。下面根据实际的编程经验总结避免这些问题的方法:

(1)检查动态内存分配是否成功后再引用该指针。

编程新手常犯内存分配未成功却使用它的错误,因为他们没有意识到内存分配会不成功。常用的解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用“assert(p!==NULL)”进行检查。如果用malloc()函数来申请内存空间,则应该用“if(p==NULL)”或“if(p!=NULL)”进行防错处理。

(2)对于分配成功的动态内存空间需要将其初始化后再使用。

free()函数在释放动态内存空间时并不对该内存空间清零,因此在下一次由malloc()函数分配这块空间时,其中的内容依然保持着原来的值。所以在使用这块内存空间之前应该对其进行初始化。

(3)特别小心内存空间的访问越界。

例如在使用数组时经常发生下标多1或者少1的操作。特别是在for循环语句中,循环次数很容易出错,导致数组操作越界。

(4)用sizeof来计算结构体的大小;分配内存空间时“宁滥勿缺”(宁可多申请,也不要少申请)。

(5)总是释放由malloc()函数返回的指针所指向的内存空间。

程序员必须在调用malloc()函数分配成功后保存好这个指针,否则将无法正确地释放这块内存空间,从而造成内存泄漏。

(6)进行错误处理时不要忘记释放其他已分配内存空间。

(7)对于被释放的动态内存空间,最好立刻为指向这块内存空间的指针变量赋值NULL,这样可以避免继续引用这个指针造成“野”指针。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈