回 帖 发 新 帖 刷新版面

主题:[讨论]关于内存释放的问题


内存分配
2009-05-09 15:29摘要:内存管理向来是C/C++程序设计的一块雷区,大家都不怎么愿意去碰她,但是有时不得不碰它。虽然利用C++中的smart pointer已经可以完全避免使用指针,但是对于对于指针的进一步了解,有助于我们编写出更有效率的代码,也有助于我们读懂以前编写的程序。 
    五大内存分区
    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
    堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
    自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
    全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
    常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)
    
    明确区分堆与栈
    在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
    首先,我们举一个例子:
    void f() { int* p=new int[5]; } 
    这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是: 在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
    00401028   push        14h
    0040102A   call        operator new (00401060)
    0040102F   add         esp,4
    00401032   mov         dword ptr [ebp-8],eax
    00401035   mov         eax,dword ptr [ebp-8]
    00401038   mov         dword ptr [ebp-4],eax
    这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
    好了,我们回到我们的主题:堆和栈究竟有什么区别? 
    主要的区别由以下几点:
    1、管理方式不同;
    2、空间大小不同;
    3、能否产生碎片不同;
    4、生长方向不同;
    5、分配方式不同;
    6、分配效率不同;
    管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
    空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:    
    打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
    碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题, 因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的 可以参考数据结构,这里我们就不再一一讨论了。
    生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
    分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
    分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比 较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆 内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分 到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
    从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态 和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地 址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
    虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
    无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就 算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
    对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?
 


齐尧的观点:

个人认为没有那么多分区,只有堆和栈;编译器在把代码解析为伪汇编代码时就能够确定程序被编译完成为机器码会占用多大的内存空间。因为伪汇编代码跟机器指令几乎时对应的,一条机器指令占用多少内存(对于精简指令的机器来说)由cpu寄存器和操作系统的位数唯一确定。


编译器在编译时认为内存是连续的,从零开始,确定了机器码码量后就可以确定要用多大的栈,或者要用几个栈。编译器会把从零开始的、大于等于机器码量的、栈的最小倍数的存储容量作为栈使用。内存的最大地址作为栈底,也只是在编译时是作为栈使用的,将编译的机器指令压入栈,如果需要多个栈,就多一个将新栈的偏移地址压入寄存器的操作。在程序执行时栈底就变成了某一个执行序列的队首;也就是程序的入口,存储在代码中的指定的栈地址和堆被操作系统用一些复杂的方法映射到可用的内存上,并且表现出连续的性质。如果在指令中有在堆区申请内存的指令可以由申请内存的函数完成查找和分配的任务,堆的可用大小是动态确定的,程序的每次执行堆的大小可能都不同。


其它的几种,内存管理分区编译成机器码后都放在栈里边,也只是在编译时从语言的安全规则上加以限制。

回复列表 (共2个回复)

沙发

看到有许多人问关于内存释放的问题?回答有些是错的,有些模模糊糊的。在网上找了片文章发到着,让感兴趣的看看



学过操作系统原理,和编译原理的都知道;

栈和堆都是最为进程的资源存在的;
编译出来的机器指令集,即程序中的指令所使用的内存都是从零开始的,只不过在执行的时候操作系统用一些复杂的方

法映射到可用的内存上,并且表现出连续的性质。

所以对于32位的机器程序中的指令都有线性地4GB的寻址能力,实际可用的地址很可能小于4GB。遇到到在堆中申请内存

的指令时,直接从堆的最小地址(逻辑地址)处开始分配,如果内存操作系统还有可用的内存,就能分配成功,然后把

该部分内存作为此进程的资源;

当再一次遇到动态分配内存的时候,如果上一次释放了内存,就仍然从堆的最小地址(逻辑地址)处开始分配。如果没

有释放,就从大于等于上次分配的内存容量的内存最小使用量的(32位机器中是4B)最小整数倍加堆的首地址的地址处

开始(这中表达在位图的4字节整数倍中也有)分配新一块的内存,并被作为该进程的资源。也就是说每次分配内存时

都在前次被标记为该进程资源的内存中寻找连续的够用的内存,如果找不到(找不到的原因可能是还在使用中,或者没

有释放),就要从后边新申请内存。

如果此进程还在执行中,以前每次申请的内存不管释放与否都不能有别的进程使用。但是当进程结束后,操作系统会把

分配给此进程的栈和堆都回收了。操作系统的回收效率与操作系统的内存管理能力和进程调度能力有关。假设操作系统

在这两方面都做的很好,则进程结束后此进程使用过的资源又可以被其它的进程使用了。


所以,有一个同行问:
用户自定义的一些栈、队列等结构(程序中动态申请的内存)在程序退出后还占用内存空间吗?这是跟具体的语言实现

有关还是各种语言都一样的? 

某人给出了下面的回答

#include <stdlib.h>

int main()
{
    malloc(52428800);  /* 也就50MB */
    return 0;
}

/* 楼主多运行几次试试  */

我推测回答的人肯定认为运行许多次后,系统的内存会被耗尽。
其实不然,这种简单的内存分配情形,所有的操作系统都能在程序退出后回收。也就是说,你把这个程序运行无数次操

作系统也不会当掉,操作系统当掉也不是此程序造成的。

为什么我们还需要释放动态申请的内存呢?
通过我们上面的分析可知,如果动态申请的内存被释放了,就可能会在此进程已经占用的内存资源中找到新的可用的内存

块;如果不释放,此进程就会新占用内存资源。如果程序用一下就退出的话,几乎看不到不释放内存的危害。如果程序

是服务呢,一开机就开始运行了;如果是某种特殊的程序呢(网络服务器管理程序),每次运行都几个月的话,这种危

险就表现出来了。不断的申请新内存的资源,内存资源总会耗尽的。

所以上边的回答应该是

#include <stdlib.h>

int main()
{
   unsigned long i;
   for( i =0 ; i <= 60000000; i++)
        malloc(52428800);  /* 也就50MB */
    return 0;
}

/* 楼主运行一次试试,如果机器没死请继续增加循环的次数  */

板凳

编程论坛 - 编程爱好者网站
http://www.programbbs.com/?122101
编程论坛,编程爱好者网站,为编程爱好者提供编程语言技术文档,编程控件,编程教程,程序源码下载,编程工具下载等,编程论坛聚集了大量的编程爱好者。

我来回复

您尚未登录,请登录后再回复。点此登录或注册