操作系统之堆和栈的区别

程序的内存分配

在C/C++程序中,占用的内存分为几个部分:

栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆区 (heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域 - 程序结束后有系统释放

文字常量区 :常量字符串就是放在这里的。 程序结束后由系统释放

程序代码区:存放函数体的二进制代码

贴上自己照着敲的一段代码:

//
//  main.cpp
//  堆和栈的区别
//
//  Created by staff on 16/3/27.
//  Copyright © 2016年 staff. All rights reserved.
//

#include <iostream>
int a = 0;    //全局初始化区
char * p1;    //全局初始化区
int main(int argc, const char * argv[]) {
    // insert code here...
    int b;    //栈
    char s[] = "abc";    //栈
    char *p2;    //栈
    char *p3 = "123456";    //字符串"1234456"在常量区,p3在栈上
    static int c = 0;    //全局静态区
    p1 = (char*)malloc(100);    //分配的100(字节)存储在堆上,但是p1和p2本身是在栈上
    p2 = (char*)malloc(100);    //同上
   
    strcpy(p1, "123456");
    //同样的,字符串是在常量区,这段代码执行的功能和p3的功能是一样的,所以编译器可能将p1和p3指向同一个地方
   
    return 0;
}

(PS:这里建议大家自己敲一遍,加深自己的印象)

在这个程序中,我们可以发现(教大家一些小技巧)

  • 变量前不加static关键字:

    1. 在函数体内定义的变量存储在栈;
    2. 使用到了c中的malloc或者c++的new等关键字的分配了内存的存储在堆上;
    3. 像是定义的const * char或者const string的字符串则存储在文字常量区;
    4. 当你定义了一个函数名的时候(例如在.h文件中),这些函数体则存储在程序代码区;
  • 变量前加static关键字:

    1. 在函数体外定义的变量,无论加不加static关键字,都存储在全局初始化区;
    2. 有关键字static自然存储在全局静态区;

我们还发现,p1,p3自己本身存储在栈上,但是程序malloc的一段内存是存储在堆上;

堆和栈的理论知识

当我们了解了它们分配的位置以后,我们接下来看看,它们是怎么运作的(数据结构~)

栈是为执行线程留出的内存空间(当线程创建的时候,操作系统会为每个系统级的线程分配栈);

当你执行函数的时候,会分配线程,就像这个:

栈顶会为局部变量和数据预留块,当函数执行完毕,块就没有用了,可能在下次的函数调用的时候再被使用,栈通常都是采用后进先出的方式预留空间;

因此最近的保留块通常最先被释放,从栈中释放块不过是指针的偏移(这里和数据结构中的栈的意思类似)

在代码底层会直接的支持栈的数据结构,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作;

  1. 栈和sp寄存器(堆栈指针寄存器,堆栈指针寄存器在堆栈操作中使用,PUSH和POP指令是从SP寄存器得到现行堆栈段的段内偏移量,所以称SP寄存器为堆栈指针,SP始终指向栈顶)一起工作,最初 sp 指向栈顶(栈的高地址);

  2. 当用 push 压栈时,sp 值减少(向低地址扩展)。当用 pop 弹栈时,sp 值增大。存储和获取数据都是 CPU 寄存器的值;

  3. 如果函数有参数的话,在函数调用之前,会将参数压栈。函数中的代码通过 sp 的当前位置来定位参数并访问它们;

  4. 当函数被调用时,CPU使用特定的指令把当前的 IP (译者注:“instruction pointer”,是一个寄存器,用来记录 CPU 指令的位置)压栈。即执行代码的地址。CPU 接下来将调用函数地址赋给 IP ,进行调用。当函数返回时,旧的 IP 被弹栈,CPU 继续去函数调用之前的代码。

  5. 当进入函数时,sp 向下扩展,扩展到确保为函数的局部变量留足够大小的空间。如果函数中有一个 32-bit 的局部变量会在栈中留够四字节的空间。当函数返回时,sp 通过返回原来的位置来释放空间。

这张图很好的说明上面的步骤,可以发现:

  1. 通过sp寄存器的移动,分配内存空间;
  2. 当需要保存数据的时候,将它们压入栈中,不需要的时候,出栈,则可以回到原来的地址;
  3. 先压入参数,再压入调用函数地址,才进入到函数内部;
  4. 当函数内部调用另一个函数的时候,会压入当前执行的地址,待执行另一个函数完后,会出栈,则可以回到原来的地址;
  5. sp寄存器通过出栈返回原来的地址释放内存空间;

注意:

每一次新调用的函数都会分配函数参数,返回值地址、局部变量空间、嵌套调用的活动记录都要被压入栈中。函数返回时,按照正确方式的撤销

堆的数据结构并不是由系统支持的,而是由函数库提供的基本的malloc/realloc/free函数维护了一套内部的堆数据结构(所以才需要程序员自己释放内存,否则会造成内存泄漏);

堆包含了一个链表来维护已用和空闲的内存块;

申请内存:
当程序需要在堆中分配内存的时候,会从内部堆中寻找可用的内存空间,通过链表找到符合大小的内存块(链表遍历的方向是由低地址指向高地址),但是由于堆是不连续的内存区域,当找不到合适的内存区域的时候,则会利用系统调用来动态增加程序数据段的内存大小;
释放内存:

当系统受到程序的释放内存的申请的时候,会遍历该链表,寻找第一个空间大于所申请的堆结点,然后该结点会从链表中删除,并将该结点的空间释放给内存,这片内存空间又会返回到堆结构中,会经过内存块的组合,以便适合下次内存分配申请;(这里面如果没有管理内存分配在释放内存时很容易会造成内存碎片)

注意:

每一个线程都有一个栈,但是每一个应用程序通常都只有一个堆(尽管为不同类型分配内存使用多个堆的情况也是有的);

在多线程环境下每一个线程都可以有他自己完全的独立的栈,但是他们共享堆。并行存取被堆控制而不是栈;

栈和堆使用的函数是不同的,这里要注意alloca函数是分配在栈上的,别混了!栈空间分为静态和动态,释放函数是没有的,因为自动释放;

区别(PS:这里是很多面试官都喜欢问的问题)

内存分配方面:

堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式是类似于链表。可能用到的关键字如下:new、malloc、delete、free等等。

栈:由编译器(Compiler)自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

申请方式方面:

堆:需要程序员自己申请,并指明大小。在c中malloc函数如p1 = (char *)malloc(10);在C++中用new运算符,但是注意p1、p2本身是在栈中的。因为他们还是可以认为是局部变量。
     
栈:由系统自动分配。 例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。

系统响应方面:

堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

大小限制方面:

堆:是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

栈:在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

效率方面:

堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

栈:由系统自动分配,速度较快。但程序员是无法控制的。

存放内存方面:

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

栈:在函数调用时第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈,然后是函数中的局部变量。 注意: 静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

存取效率方面:

堆:char *s1 = "Hellow Word";是在编译时就确定的;

栈:char s1[] = "Hellow Word"; 是在运行时赋值的;用数组比用指针速度要快一些,因为指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上直接读取。

总结

到了这里,相信应该了解堆和栈的区别了吧,我就总结一下吧;

栈是操作系统提供的功能,有系统支持,自然快速高效,但是缺点是存储的空间不够大;而堆是关于申请内存和释放内存的函数的事,由程序自己控制,这里速度就会较栈慢些;

有人会想,能否判断一个指针指向的是栈还是堆?
贴上网址:http://www.zhihu.com/question/29538036
其实我的答案是,应该可以,但是~(程序不是除了栈就是堆的啊啊啊)

posted @ 2017-02-15 06:41  banananana  阅读(2837)  评论(0编辑  收藏  举报