zoukankan      html  css  js  c++  java
  • 深入浅出C语言中的堆和栈

    在谈堆栈的时候,我在这有必要把计算机的内存结构给大家简单的介绍下(高手们可以直接飘过)

    一、 内存结构
     
    每个程序一启动都有一个大小为4GB的内存,这个内存叫虚拟内存,是概念上的,真正能用到的,只是很小一部分,一般也就是在几百K到几百M。我们PC中内存,我们称之为物理内存,也就是256M,512M等,虚拟内存和物理内存的如何转换是由操作系统完成的,我们不需要管它。我们只需要管好我们自己程序的那4GB内存就可以了。
    要管理4GB的虚拟内存,就必须给每个字节分配一个号码,以便程序与访问到其中任何一个字节。这个号码是从0开始顺序递增的,针对于这个号码我们就称之为地址,从0x00000000-0xFFFFFFFF,这样,我们理论上就可以访问其中内存中任何一个字节了。但有一点请注意,系统并不让我们全部都可以用。其中后面2GB的内容是留给系统用的,用户是不可以访问的,而且在前面的2GB也有部分区段不能访问,比如0x00000000就不能访问。具体是哪些区段,不必关心。
    注意:类似于0x12345678或12345678H是10进制数305419896的16进制表示法,他们是一回事,显示16进制是为了方便显示及计算机计算。
    程序都是用来做一些具体的事情,不管做什么事,结构都是很相似。程序启动,就有4GB的虚拟内存,通过CPU的计算,改变内存的内容,最后再复制内存的内容输出,输出的目的地可以是:屏幕、文件、磁盘等外存、端口、网络等。如何输出呢,最后全部都是调用系统的API,由操作系统完成。(这段话,请仔细体会,并牢牢记住)
    所以我们的核心问题就是:如何控制内存,让内存里的值,变成我们想要的结果。
    注意:这里的控制,指读取或写入某段内存的内容。
     
    在虚拟内存中,我们一般将其分为以下几个区域:
    1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 
    操作方式类似于数据结构中的栈。 
    2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回 
    收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。 
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的 
    全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另 
    一块区域。 - 程序结束后由系统释放。 
    4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 
    5、程序代码区—存放函数体的二进制代码。 
     
    示例如下:
    这是一个前辈写的,非常详细 
    //main.cpp 
    int a = 0; 全局初始化区 
    char *p1; 全局未初始化区 
    main() 

    int b; 栈 
    char s[] = "abc"; 栈 
    char *p2; 栈 
    char *p3 = "123456"; 123456在常量区,p3在栈上。 
    static int c =0; 全局(静态)初始化区 
    p1 = (char *)malloc(10); 
    p2 = (char *)malloc(20); 
    分配得来得10和20字节的区域就在堆区。 
    strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456" 
    优化成一个地方。 
     
    二、堆和栈的理论知识 
    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是在编译时就确定的; 
    但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
    比如: 
    #include 
    void main() 

    char a = 1; 
    char c[] = "1234567890"; 
    char *p ="1234567890"; 
    a = c[1]; 
    a = p[1]; 
    return; 

    对应的汇编代码 
    10: a = c[1]; 
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
    0040106A 88 4D FC mov byte ptr [ebp-4],cl 
    11: a = p[1]; 
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
    00401070 8A 42 01 mov al,byte ptr [edx+1] 
    00401073 88 45 FC mov byte ptr [ebp-4],al 
    第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到 
    edx中,再根据edx读取字符,显然慢了。 

    2.7 进一步的比较如下:
    堆是相对于栈的,前面说到栈的大小大概为1MB,而用户能用到的内存大概有2GB,因此除了少量的数据区域和静态区域,以及这2G中被小部分限制的区域处,剩余都是堆的空间,其大小还是接近2G。(接近2G,是我的理解,没有在其它书中看到类似的结论,说错了,你别笑话我。)
     
    堆主要有两个作用:
    1、 欲分配内存空间的大小,或称长度,可以是变量,这意味着这个大小,在分配前,可以随着环境的改变而改变,不要求是定值。注意,这里说的是“分配前”,在分配完了以后,这段内存的大小理论上还是不可以改大小的,除非释放掉或用一些特别的方法。
    2、 可以分配比较大的空间。
     
    堆分配内存,主要通过函数malloc(),释放用free()。比如:
    #include <stdlib.h> /* 要加上该头文件 */
    int size = 10*sizeof(int);
    int* pInt = (int*)malloc(size);
    表示在堆中,分配长度为size的内存,将分配到的那段内存,标识成一系列int型数据,并将这段内存的地址,赋值给一个int型指针pInt,这样,通过pInt就可以控制这段内存了。
    我们知道在栈中,可以通过数组,也能达上述的效果,如下:
    int arr[10];
    他们是有区别的:
    1、 在效率上,前者(指pInt那段内存)是通过系统在整个堆空间中搜索到一段合适的内存,然后把这段内存分配给pInt,而后者只是在临近的位置分配这样大小的内存,这样少了搜索过程,后者显示效率高得多。
    2、 在大小上,前者总共能分配的内存接近2GB,可以说很大了,而后者,其栈总的大小才1MB,所以其分配的大小不可能超过1MB,确切来说,是不能超过1M-分配前栈的大小。换句话说,如果要分配的空间超过1M的话,只能选择前者。
    3、 在作用范围上,前者的内存地址可以用一个指针表示,假如这个指针是全局变量的话,则一直可以控制这块内存了,事实上,只要这块内存不被释放,那么在程序任何地方,它只要知道该内存的地址,则可以控制它。后者在退出函数或其作用域后,该段内存就被收回了。
    4、 在“3、”中,似乎感觉堆很好,但隐藏了一个重要的麻烦,那就是内存释放,因为堆的内存释放是需要手工进行的。如果一不小心,用完后,我忘记释放,那么结果会怎样呢?事实上这段内存则不会被再用到,直到程序结束,比如我申请了200M的内存,没释放,这种浪费还是很可观的。
     
    针对于他们的区别,我们可以有一个结论:
    同时满足:临时性,小的(不超过1M),更快的内存分配用栈,(其实第3条“更快”由于现代机器够快,两种方式都会满足),否则用堆。
     
    这些是栈和堆的区别,在实际工作中非常重要,请充分理解,不理解的话就背下来,这在面试时会经常考到。
     
    如果堆分配不成功的话,则返回NULL。
     
    前面说过,针对于某段在堆中分配的内存,如果不再需要使用了,则应该释放。这个释放用free();
    如下:
    int* pInt = (int*)malloc(size);
    /* 针对于pInt做一些操作 */
    free(pInt);
     
    2.8小结: 
    堆和栈的区别可以用如下的比喻来看出: 
    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就 
    走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自 
    由度小。 
    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由 
    度大。 (经典!) 
  • 相关阅读:
    ios app相互调用
    Nginx连接频率限制
    nginx 基于域名的虚拟主机
    nginx基于多端口的虚拟主机
    Nginx基于多ip的虚拟主机
    多文件上传
    thinkphp5模型关联
    原生查询和查询构造器
    thinkphp5请求和响应
    thinkphp验证器
  • 原文地址:https://www.cnblogs.com/xd-elegant/p/4159284.html
Copyright © 2011-2022 走看看