课程纲要
目录
一.C语言内存管理模型
代码区 静态变量区 栈区 堆区(动态存储区)
- 代码区:存放CPU执行的机器指令,代码区是可共享,并且是只读的。(指令,常量字符串)
- 静态区:所有的 全局变量+静态变量(static)+字符串常量
- 栈区:由编译器自动分配内存(函数的参数,局部变量,自动变量),自动回收。
- 堆区:由程序员分配释放(malloc, free)
栈区生命周期:函数调用结束,内存空间就会释放
静态区生命周期:整个程序结束内存才会释放。注意static修饰词的使用
栈区:
优点:自动申请、自动释放,使用方便,并且能与标识符建立联系使用方便,由于操作系统算法比较完善,因此不会产生内存碎片、内存泄漏的问题。先进后出的特殊为函数的调用提供以及递归支持。安全、方便。
缺点:大小有限,不适合存储大量数据;当函数结束时栈内存就会被释放, 不适合长期存储数据。
堆区:
优点:存储空间够大,适合存储大批量的数据,申请和释放是受管理员控制的,适合长期保存数据。还可以根据程序的实际需要来调整内存的大小。
缺点:需要手动申请释放,使用麻烦;安全性差;在使用过程中可能会造成内存泄漏、产生内存碎片,对编程人员要求高。
当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。
堆区是不会自动在分配时做初始化的(包括清零),所以必须用初始化式来显式初始化。
使用堆内存要注意哪些问题:
1、内存越界
由于申请时的内存大小计算错误,而导致内存越界。
会导致段错误、脏数据(数据丢失受破坏)
2、内存泄漏
在程序运行期间,内存的首地址丢失,造成内存无法再内存期间释放,或者重复申请,而导致可用内存越来越少。
3、内存碎片
如果频繁的申请、释放小块的内存可能会造成申请和释放的不协调,而导致已经释放的内存无法使用。
4、重复释放
指针操作失误、业务流程出现漏洞导致同一块内存多次释放。可能造成段错误。
5、产生野指针
释放完内存后指向他的指针要及时设置为空,否则就会产生野指针,埋下安全隐患。
思考:
我们会想,既然有全局变量区(静态变量区),为什么还要使用堆区呢?直接一个全局变量不就可以了?
实际在使用的时候会发现,堆区其实是很有必要的。比如说,一个板卡串口每隔一定周期会收到一个1k字节的数据,拿到1k的数据后需要通过蓝牙上传给手机,蓝牙一次只传20个,还不能一直使用for不断调用蓝牙发送函数,需要用定时器辅助完成发送。这个时候这个1k的buff就不能是局部,因为不能一直停在uart接受函数这里,需要用到定时器持续蓝牙。如果使用局部的buff,传输uart接受函数执行完毕后这个1k的buff就释放了,定时器来到的时候数据已经没了。如果使用全局,确实不会出现上述的问题,但是你这1k的ram就只能一直做接受uart数据这一件事,那么malloc的神奇就来,我在收uart的时候申请一下1k的buff,蓝牙传输完成以后就可以free掉,这样其他程序可以反复用到这1k的ram空间;
尤其是我们的单片机内存现在还不是很大,随着物联网应用的发展一些包和逻辑占用较多内存。我们可以使用动态分配的这种方法使有限的内存实现更多的功能。
变量的定义(存储类型 数据类型 变量名)
Auto int aa=10;//我们平时定义都将auto省略
(1)Auto (局部变量)说明变量只能在某个程序范围内使用,通常在函数体内或函数中的复合语句中。(默认是随机值,所以我们平时都要求变量要赋初值)
(2)Register 寄存器变量(不常用),想将变量放入CPU寄存器中,这样可以加快程序运行速度。
如果申请不到还是使用一般内存,同auto。
(3)Static 变量 静态存储类型的变量,即可以在函数体内,也可以在函数体外(可以修饰局部变量,也可以修饰全局变量,默认值为0)
二.内存管理应用举例
1.
这是stm32f103RE的Memory map,从这个图可以看出外设的地址,64K的RAM空间,及512K的FLASH。
在平时的开发中,我们要时刻关注这64K,RAM的剩余量,防止程序跑飞。那么我们编译的程序怎么看内存占用,最简单的方式就是直接看编译信息。
程序通过软件进行编译输出如下信息
Program Size: Code=10904 RO-data=940 RW-data=40 ZI-data=1736
Code是代码占用的空间;
RO-data是 Read Only 只读常量的大小,如const型;
RW-data是(Read Write) 初始化了的可读写变量的大小;
ZI-data是(Zero Initialize) 没有初始化的可读写变量的大小。ZI-data不会被算做代码里因为不会被初始化;
烧写的时候是FLASH中的被占用的空间为:Code + RO Data + RW Data
程序运行的时候,芯片内部RAM使用的空间为: RW Data + ZI Data
比如我们经常遇到的内存溢出错误就是RW Data + ZI Data 大于了我们芯片的内存空间。
2.内存分配举例:
3.如下面的程序:
char * get_string(){
char s[] = “hello world”;
return s;
}
void main(void)
{
char *str =NULL;
str = get_string();
}
如上面的程序所示,main函数中执行完get_string时,返回的内容是否是hello world??
答案是返回的内容不是hello world。
我们的“hello world”是一个字符串常量,它位于静态存储区,但是我们把字符串常量赋值给了一个局部变量(char型的数组),局部变量是存放在栈上的,当get_sting函数退出时,局部变量的内存是要被清空的。
那我们改为char *s = “hello world”;这样就可以正常输出了,s是指向静态存储区的内存,静态存储区的生命周期,在程序运行过程是不会清空的。
当然我们也可以加一个static 来改变生命周期。static char s[] = “hello world”;
扩展:static的作用:
- 在修饰变量的时候,static修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。 初始化的时候自动初始化为0;
第二、static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。
第三、static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。