天之道

享受编程的乐趣。
posts - 118, comments - 7, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

栈和堆(转)

Posted on 2013-09-27 09:13 hoshelly 阅读(270) 评论(0)  编辑 收藏 引用 所属分类: C
摘抄自:http://www.cnblogs.com/vamei 

当程序文件运行为进程的时候,进程在内存中得到空间(进程自己的小房间)。每个进程空间按照如下方式分为不同区域:

                    内存空间

Text区域用来储存指令(instruction),来告诉程序每一步的操作。Global Data用于存放全局变量,stack用于存放局部变量,heap用于存放动态变量 (dynamic variable. 程序利用malloc系统调用,直接从内存中为dynamic variable开辟空间)。TextGlobal data在进程一开始的时候就确定了,并在整个进程中保持固定大小

 

Stack()stack frame为单位。当程序调用函数的时候,比如main()函数中调用inner()函数,stack会向下增长一个stack frame。Stack frame中存储该函数的参数局部变量,以及该函数的返回地址(return address)。此时,计算机将控制权从main()转移到inner(),inner()函数处于激活(active)状态。位于Stack最下方的frame和Global Data就构成了当前的环境(context)。激活函数可以从中调用需要的变量。典型的编程语言都只允许你使用位于stack最下方的frame ,而不允许你调用其它的frame (这也符合stack结构“先进后出”的特征。但也有一些语言允许你调用stack的其它部分,相当于允许你在运行inner()函数的时候调用main()中声明的局部变量,比如Pascal)。当函数又进一步调用另一个函数的时候,一个新的frame会继续增加到stack下方,控制权转移到新的函数中。当激活函数返回的时候,会从stack中弹出(pop,就是读取并删除)该frame,并根据frame中记录的返回地址,将控制权交给返回地址所指向的指令(比如从inner()函数中返回,继续执行main()中赋值给main2的操作)。

下图是stack在运行过程中的变化,箭头表示stack增长的方向,每个方块代表一个stack frame。开始的时候我们有一个为main()服务的frame,随着调用inner(),我们为inner()增加一个frame。在inner()返回时,我们再次只有main()的frame,直到最后main()返回,其返回地址为空,所以进程结束。


                                                          stack变化


在进程运行的过程中,通过调用和返回函数,控制权不断在函数间转移。进程可以在调用函数的时候,原函数的stack frame中保存有在我们离开时的状态,并为新的函数开辟所需的stack frame空间。在调用函数返回时,该函数的stack frame所占据的空间随着stack frame的弹出而清空。进程再次回到原函数的stack frame中保存的状态,并根据返回地址所指向的指令继续执行。上面过程不断继续,stack不断增长或减小,直到main()返回的时候,stack完全清空,进程结束。

 

 当程序中使用malloc的时候,heap()向上增长,其增长的部分就成为malloc从内存中分配的空间。malloc开辟的空间会一直存在,直到我们用free系统调用来释放,或者进程结束。一个经典的错误是内存泄漏(memory leakage), 就是指我们没有释放不再使用的heap空间,导致heap不断增长,而内存可用空间不断减少。

由于stack和heap的大小则会随着进程的运行增大或者变小,当stack和heap增长到两者相遇时候,也就是内存空间图中的蓝色区域(unused area)完全消失的时候,进程会出现栈溢出(stack overflow)的错误,导致进程终止。在现代计算机中,内核一般都会为进程分配足够多的蓝色区域,如果我们即时清理的话,stack overflow是可以避免的。但是,在进行一些矩阵运算的时候,由于所需的内存很大,依然可能出现stack overflow的情况。一种解决方式是增大内核分配给每个进程的内存空间。如果依然不能解决问题的话,我们就需要增加物理内存。


堆和栈的区别:


2.1申请方式    
  stack:    
  由系统自动分配。   例如,声明在函数中一个局部变量   int   b;   系统自动在栈中为b开辟空  
  间    
  heap:    
  需要程序员自己申请,并指明大小,在c中malloc函数    
  如p1   =   (char   *)malloc(10);    
  在C++中用new运算符    
  如p2   =   new   char[10];    
  但是注意p1、p2本身是在栈中的。    
   
   
  2.2    
  申请后系统的响应    
  栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢  
  出。    
  堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,  
  会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表  
  中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的  
  首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。  
  另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部  
  分重新放入空闲链表中。    
   
  2.3申请大小的限制    
  栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意  
  思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有  
  的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将  
  提示overflow。因此,能从栈获得的空间较小。    
  堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储  
  的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小  
  受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。    
   
   
   
  2.4申请效率的比较:    
  栈由系统自动分配,速度较快。但程序员是无法控制的。    
  堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.    
  另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是  
  直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。  
     
   
  2.5堆和栈中的存储内容    
  栈:   在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可  
  执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈  
  的,然后是函数中的局部变量。注意静态变量是不入栈的。    
  当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地  
  址,也就是主函数中的下一条指令,程序由该点继续运行。    
  堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。    
   
  2.6存取效率的比较    
   
  char   s1[]   =   "aaaaaaaaaaaaaaa";    
  char   *s2   =   "bbbbbbbbbbbbbbbbb";    
  aaaaaaaaaaa是在运行时刻赋值的;    
  而bbbbbbbbbbb是在编译时就确定的;    
  但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。   

  2.7小结:    
  堆和栈的区别可以用如下的比喻来看出:    
  使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就  
  走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自  
  由度小。    
  使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由  
  度大。   (经典!)  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理