C语言中动态内存的堆和栈

来源:互联网 发布:淘宝中老年男士羽绒服 编辑:程序博客网 时间:2024/03/28 22:34

         一直有人抱怨动态内存的堆和栈到底有啥区别,具体到数据的存放,到底哪些放在堆里,哪些放在栈中,还是放到了别的什么地方,针对,大家不断问到的问题,我也收集了一些资料,来和大家讨论一下。
         在C语言程序运行的过程中,需要内存来存放数据。这里说的内存主要分为两类:静态的存储区和动态的存储区,如下图所示:

        alloclmode

        其中,静态数据存储区分为三类:只读数据区(RO Data), 已初始化的读写数据区(RW Data)和未初始化读写数据区(BSS)。这三类都是在程序的编译-连接阶段确定的,在程序运行阶段是不会改变的,其大小和位置在程序运行过程中都是固定的。

        而动态存储区分为堆和栈。它们都是在程序运行的过程中动态分配的。其大小在程序运行的过程中将动态变化。在目前常见的体系结构和编译系统中,一种典型的动态内存的管理形式为:栈内存从高向底地址分配,而堆内存则反过来。从内存管理实现的角度来看,堆使用链表实现,而栈使用线性存储的方式。栈是编译器管理的。堆是由程序调用具体的库函数管理的。

        先来着重讲栈:栈是先进后出,一般大家都会操作,这里需要提醒的是,在实际处理的过程中,栈内存可能使用满栈和空栈两种情况,这是由处理器的体系结构决定的。在满栈情况下,栈指针当前的位置是已经使用的栈区域;在空栈的情况下,栈指针当前的位置是没有使用的栈区域。所以,按照满栈处理的情况:在入栈的时候,要先移指针,后放入数据,出栈时,要先出数据,在移动指针。而按照空栈的出理方式,入栈时,先放入数据,在移动指针,出栈时,先移动指针,在出数据。这里特别注意,是不一样的。

        stack

        需要说明的是,这些情况都是由处理器的硬件结构决定的,与程序的编写没有关系,甚至连编译器都不知道这些事情。

        在C语言中,体现栈空间使用的例子是参数的传递,返回值的使用以及自动变量的空间。参数进栈的顺序是倒着来的,即在函数参数表中,先进后面的的,再进前边的,为什么呢?你这样想,栈是从高地址空间向低地址空间递减,把后面的先进去,就放在了高地址空间,这样程序在按照顺序访问参数的时候,还是按照从低地址向高地址的顺序访问,这样一来,访问的时候就是按照参数表中的顺序来访问了。同时在栈中分配自动变量时,是和参数的声明顺序是一样的。即先声明的放在高地址空间中。

        说完了栈,现在来说说堆。在一般的编译系统中,堆内存的分配方向和栈内存是相反的(即从低到高),是通过调用C语言的库函数完成的,是由用户决定的,因此何时分配,何时释放的逻辑完全使用程序的设计者来控制。从而出现了后面“谈其色变”几大问题:

        1)内存泄漏:申请了一块内存,但是没有释放,结果程序结束了,它也没有回收,导致其他的程序要不能用。

        2)野指针:指一个内存指针已经被释放(通过free或者realloc),但是该指针依然在使用。那么该怎么做呢?正确的堆内存使用方法是当内存被释放后,将内存的指针置为NULL,并在程序中要使用的时候判断该内存指针是否为NULL,如果是NULL, 则认为内粗已经被释放,不对内存进行访问。

        3)非法释放指针:从原则上讲只有被malloc(),calloc()或realloc()分配并通过返回值返回返回的内存才能被释放,否则释放除此以外的内存都是非法的。即使有一个指针是*p是malloc,那么对p1=p++,这个时候free(p1)也是不合法的,但free(p)确实可以的。同样释放函数中的局部变量也是非法的.还有一种情况是,对一个堆内存释放两次也是错误的用法。因为free()函数是不能释放未分配的堆内存。在程序使用free释放内存之后,应该将指针置为NULL,free一个NULL地址是没有问题的。另外,如果释放的是从函数参数传入的指针,由于无法通知外部这个指针已经被释放,在别的地方如果指针再次被释放,就会发生多次释放同一个指针的错误。

       上面说了那么多,那么堆和栈综合起来比较有那些不同呢?我们说,函数的返回值是保存在栈上的,但返回值可以是一个指针,它可以指向静态存储区,可以指向堆内存的地址,同样也可以指向函数调用者的栈空间(主调函数),但是不能指向一个函数内部栈内存区域的地址(被调函数)。

       另外一个需要说明的问题是,函数参数同样可是数组,但值得注意的是数组作为参数使用的时候,将当作指针处理,而不会像结构体那样把整个内容全部压入栈,实际入栈的仅仅是一个数组地址的指针。如把int a[100]作为参数的时候,并没有将数组的100个元素全部入栈,,而是把数组的指针a放入了栈,实际处理过程与指针是相同的。即void func(int a[100])和void func(int* a)这两种处理方式是一样的。这也说明数组的sizeof()运算将返回数组的大小,同样数组名是不能进行自增自减操作的。然而,对于参数中的数组,这个时候是可以进行自增减以及其它赋值操作,对它进行sizeof将永远返回指针的大小4字节(因为刚说了,在函数参数中,他就被当成个个指针处理)。



原创粉丝点击