论C语言如何有效的管理内存资源

作者: 楚乔_0a40 | 来源:发表于2018-06-10 13:37 被阅读3次

    在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。因此在程序设计中,有效地管理内存资源是程序员首先考虑的问题。

    1.什么是程序?什么是进程?

    程序:在磁盘保存的可以运行的文件

    进程:正在运行的程序,存在于内存中

    一个进程空间被划分为以下部分:

    1)代码区:程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。

    注意:"int a = 0;"语句可拆分成"int a;"和"a = 0",定义变量a的"int a;"语句并不是代码,它在程序编译时就执行了,并没有放到代码区,放到代码区的只有"a = 0"这句。

    2)BSS段:保存未初始化的全局变量,BSS段在mian函数执行前会被清0(这就是为什么static变量未初始化默认为0)

    3)全局区:保存全局变量,mian函数执行前分配

    4)堆区:也叫自由区(堆区可能没有,可能有多个,如果没有malloc,就可能没有堆区)。一般比较复杂的数据类型都是放在堆中。堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决上面实验三造成的内存溢出困难。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成,那堆内存如何使用?

    那么我们来看下堆内存的分配和释放:

    calloc和realloc是用来在堆中申请内存空间的函数。

    案例一

    部分分析如下:

    main函数和UpdateCounter为代码的一部分,故存放在代码区

    数组a默认为全局变量,故存放在静态区

    main函数中的"char *b = NULL"定义了自动变量b(variable),故其存放在栈区

    接着"b = (char *)malloc(1024*sizeof(char));"向堆申请了部分内存空间,故这段空间在堆区

    malloc与free

    malloc函数用来在堆中分配指定大小的内存,单位为字节(Byte),函数返回void *指针;free负责在堆中释放malloc分配的内存。malloc与free一定成对使用。看下面的例子:

    运行结果为:

    堆的容量有多大?理论上讲,它可以使用除了系统占用内存空间之外的所有空间。实际上比这要小些,比如我们平时会打开诸如QQ、浏览器之类的软件,但这在一般情况下足够用了。实验二中说到,不能将一个栈变量的地址通过函数的返回值返回,如果我们需要返回一个函数内定义的变量的地址该怎么办?可以这样做:

    可以通过函数返回一个堆地址,但记得一定用通过free函数释放申请的堆内存空间。"int *p = (int *)malloc(sizeof(int));"换成"static int a = 0"也是合法的。因为静态区的内存在程序运行的整个期间都有效,但是后面的free函数就不能用了!

    5)栈区:保存局部变量(包括函数参数),内存分配释放都是自动进行的栈底的地址是用户可访问的最高地址区域,每次在栈内开辟空间,栈顶指针会变小(即地址变小)堆开始分配的地址比较低,与全局区相邻,每次在堆内开辟空间,开辟的地址比上一次开辟的地址大。

    栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。

    每个线程都有自己专属的栈;

    栈的最大尺寸固定,超出则引起栈溢出;

    变量离开作用域后栈上的内存会自动释放。

    //实验一:观察代码区、静态区、栈区的内存地址

    #include "stdafx.h"

    int n = 0;

    void test(int a, int b)

    {

    printf("形式参数a的地址是:%d\n形式参数b的地址是:%d\n",&a, &b);

    }

    int _tmain(int argc, _TCHAR* argv[])

    {

    static int m = 0;

    int a = 0;

    int b = 0;

    printf("自动变量a的地址是:%d\n自动变量b的地址是:%d\n", &a, &b);

    printf("全局变量n的地址是:%d\n静态变量m的地址是:%d\n", &n, &m);

    test(a, b);

    printf("_tmain函数的地址是:%d", &_tmain);

    getchar();

    }

    运行结果如下:

    结果分析:自动变量a和b依次被定义和赋值,都在栈区存放,内存地址只相差12,需要注意的是a的地址比b要大,这是因为栈是一种先进后出的数据存储结构,先存放的a,后存放的b,形象化表示如上图(注意地址编号顺序)。一旦超出作用域,那么变量b将先于变量a被销毁。这很像往箱子里放衣服,最先放的最后才能被拿出,最后放的最先被拿出。

    这段代码没有任何语法错误,也能得到预期的结果:20。但是这么写是有问题的:因为int *p = getx()中变量x的作用域为getx()函数体内部,这里得到一个临时栈变量x的地址,getx()函数调用结束后这个地址就无效了,但是后面的*p = 20仍然在对其进行访问并修改,结果可能对也可能错,实际工作中应避免这种做法,不然怎么死的都不知道。不能将一个栈变量的地址通过函数的返回值返回,切记!

    另外,栈不会很大,一般都是以K为单位。如果在程序中直接将较大的数组保存在函数内的栈变量中,很可能会内存溢出,导致程序崩溃(如下实验三),严格来说应该叫栈溢出(当栈空间以满,但还往栈内存压变量,这个就叫栈溢出)。

    2.对空间的分配

    malloc会在后台维护一个双向链表的数据结构,并且每次malloc会在结束位置有标记(12字节,存有链表信息)越界访问修改会破坏标记,从而导致内存释放出现问题,总而言之,使用malloc时不要越界访问。

    3.内存页面管理(详细查看内存的段页式管理方式)

    操作系统分配物理内存空间时,会按页为单位进行分配。一般系统一页为4096Bytes.操作系统分配好物理空间之后,会给进程

    分配虚拟内存与物理空间一一对应起来,进行内存映射。

    对于malloc来说,操作系统第一次会直接分配33个页面的物理空间,并做内存映射,在以后的malloc操作,如果需要的内存没有

    超过之前分配的33个页面,操作系统不会再次分配新的空间给进程,只有下次malloc时,上次的页面都用完了,系统会一个一个的分配页面。

    测试代码

    4.学习内存管理的目的

    学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:

    如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;

    如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);

    如果需要动态创建数组,则用堆。

    最后的最后

    操作系统在管理内存时,最小单位不是字节,而是内存页(32位操作系统的内存页一般是4K)。比如,初次申请1K内存,操作系统会分配1个内存页,也就是4K内存。4K是一个折中的选择,因为:内存页越大,内存浪费越多,但操作系统内存调度效率高,不用频繁分配和释放内存;内存页越小,内存浪费越少,但操作系统内存调度效率低,需要频繁分配和释放内存。嵌入式系统的内存内存资源很稀缺,其内存页会更小,因此在嵌入式开发当中需要特别注意。

    相关文章

      网友评论

        本文标题:论C语言如何有效的管理内存资源

        本文链接:https://www.haomeiwen.com/subject/pyvfeftx.html