栈 和 堆

来源:互联网 发布:ubuntu安装mysql5.6 编辑:程序博客网 时间:2024/06/06 02:22

一般来说,程序就是与数据打交道,在执行某一功能的时候,将该功能所需要的数据加载到内存中,然后在执行完毕的时候释放掉该内存。

 

数据在内存中的存放共分为以下几个形式:

 

1、栈区(stack)—— 由编译器自动分配并且释放,该区域一般存放函数的参数值、局部变 

   量的值等。

 

2、堆区(heap)—— 一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系

   统回收。

 

3、寄生器区—— 用来保存栈顶指针和指令指针。

 

4、全局区(静态区)(static)—— 全局变量和静态变量的存储是放在一块的,初始化的全

   局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另

   一块区域。程序结束后由系统释放。

 

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

 

6、程序代码区—— 存放函数体的二进制代码。

 

函数参数和局部变量存放在栈中,当函数运行结束并且返回时,所有的局部变量和参数就都被系统自动清除掉了,为的是释放掉它们所占用的内存空间。全局变量可以解决这个问题,但是全局变量永远不会被释放,而且由于全局变量被所有的类成员和函数所共享,所以它的值很容易被修改。使用堆可以一举解决这两个问题。

 

堆是采用匿名的方式来保存数据的。你只能通过指针才能访问到这些匿名的数据。因此它的安全性是最好的。同时由于堆区中的内存是由程序员来分配和释放的,所以它的自由度也是最高的。

 

接下来我们着重讨论下堆和栈之间的区别,通过两者之间的比较,我们可以明白为什么堆可以解决以上两个问题。

 

注:好多时候我们都把堆和栈放在一起说,比如堆栈,其实他们是不同的,至于为什么把他们混合在一起说,这是个历史问题,这里就不深究了。

 

1、内存申请方式上的不同

 

(一)栈

 

由系统自动分配:例如我们在函数中声明一个局部变量int a,那么系统就会自动在栈中为变量a开辟空间。

 

(二)堆

 

需要程序员自己申请,因此也需要指明变量的大小。

 

2、系统响应的不同

 

(一)栈

 

只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将提示overflow,也就是栈溢出。

(二)堆

 

系统收到程序申请空间的要求后,会遍历一个操作系统用于记录内存空闲地址的链表,当找到一个空间大于所申请空间的堆结点后,就会将该结点从记录内存空闲地址的链表中删除。并将该结点的内存分配给程序,然后在这块内存区域的首地址处记录分配的大小,这样我们在使用delete来释放内存的时候,delete才能正确地识别并删除该内存区域的所有变量。另外,我们申请的内存空间与堆结点上的内存空间不一定相等,这时系统就会自动将堆结点上多出来的那一部分内存空间回收到空闲链表中。

 

3、空间大小的不同

 

(一)栈

 

WINDOWS下,栈是一块连续的内存的区域,它的大小是2M,也有的说是1M,总之该数值是一个编译时就确定的常数。是由系统预先根据栈顶的地址和栈的最大容量定义好的。假如你的数据申请的内存空间超过栈的空间,那么就会提示overflow。因此,别指望栈能存储比较大的数据。

(二)堆

 

堆是不连续的内存区域。各块区域由链表将它们串联起来,关于链表的知识将在后面的章节中讲解。这里只需要知道链表将各个不连续的内存区域连接起来,这些串联起来的内存空间叫做堆,它的上限是由系统中有效的虚拟内存来定的。因此获得的空间比较大,而且获得空间的方式也比较灵活。

 

4、执行效率的不同

 

(一)栈

 

栈由系统自动分配,因此速度较快。但是程序员不能对其进行操作。

 

(二)堆

 

堆是由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来很方便。

 

5、执行函数时的不同

 

(一)栈

 

在函数调用时,第一个进栈的是被调用函数下一行的内存地址。其次是函数的参数,假如参数多于一个,那么次序是从右往左。最后才是函数的局部变量。

 

由于栈的先进后出原则,函数结束时正好与其相反,首先是局部变量先出栈,然后是参数,次序是从左到右,这时所有的变量都已出栈,指针自然地指到第一个进栈的那行内存地址,也就是被调用函数的下一行内存地址。程序根据该地址跳转到被调用函数的下一行自动执行。

至于栈内数据为什么要先进后出,这个原理可以用叠盘子来做比喻,你将一个个盘子放在另一个盘子之上依次将它们叠高,取走的时候必须是从最上面的盘子开始,你不可能直接抽出最下面的盘子,因为傻子也会知道那样做会摔碎所有的盘子。


 

(二)堆

 

堆是一大堆不连续的内存区域,在系统中由链表将它们串接起来,因此在使用的时候必须由程序员来安排。它的机制是很复杂的,有时候为了分配一块合适的内存,程序员需要按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有满足条件的空间,那么就要向系统发出申请增加一部分内存空间,这样就才有机会分到足够大小的内存,然后将计算后的数值返回。显然,堆的运行效率比栈要低得多,而且也容易产生碎片。但是好处是堆可以存储相当大的数据,并且一些细节也可以由程序员来安排。

 

总结

 

从上面可以看出,栈的内存小,但是效率高,不过存储的数据只在函数内有效,超出函数就消失了。堆的可存储空间可非常大,但是容易产生内存碎片,效率也较低,好处是灵活性比较强。比如说我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择。

 

由于堆和栈各有优缺点,因此好多时候我们是将堆和栈结合使用的,比如在存储一些较大数据的时候,我们将数据存放在堆中,却将指向该数据的指针放到栈中。这样可以有效地提高程序的执行速度,避免一些不该有的碎片。不过,一般来说,假如不是特大的数据,我们都是使用栈,比如:函数调用过程中的参数,返回地址,和局部变量都存放到栈中。这样可以大大加快程序的运行速度。

 

用指针创建堆中空间

 

从第二部分我们了解到堆的好处是可以存放比较大的数据,而且存储的数据只要不是程序员手动将其释放那么就会永远保存在堆中。不象栈,存储的数据只在函数内有效,超出函数就消失了。也不象全局变量,保存的数据只有程序结束才会释放,而且很容易被修改。

 

既然了解了它的许多好处,接下来的问题就是如何使用它,我们知道堆是一大堆不连续的内存区域,在系统中由链表将它们串接起来,它不象栈,你可以为它其中的某个内存单元命名,为了数据隐秘起见,堆中的每个内存单元都是匿名的,因此你必须先在堆中申请一个内存单元的地址,然后把它保存在一个指针中。这样你只有使用该指针才可以访问到该内存单元的数据。

 

换句话说,你不知道安徽出版社的地址,但是你从朋友那里得到了它的电话号码,然后你给他们打电话,如果对方接起电话,那么你就成功了访问了安徽出版社。这时即使你忘记了安徽出版社的电话号码,即使对方已挂断电话,你也可以按下重拨键,再次通过电话访问安徽出版社,因为电话已经记录了你上次拨打的号码。在这种情况下,电话号码就相当于地址,你不需知道地址,因为电话已经帮你记录下了地址,你只需要按下重拨键,电话这个指针就会根据重拨键中记忆的电话号码自动指向安徽出版社,然后你就可以访问安徽出版社了。

 

再打个比方,你要去博物馆,但是不知道地址,这时你就可以叫个出租车带你去博物馆,你不需要知道博物馆的地址,出租车司机的脑子中保存着博物馆的地址,你只需要告诉他你要去博物馆,那么他马上就会拉着你去博物馆。在这个例子中,出租车司机脑子中记录的博物馆的地址相当于堆中某单元的内存地址,你不需要知道堆中该单元的地址,你只需要类似出租车司机一样的指针,那么你就可以访问堆中该单元的数据了。

 

采取这种匿名的内存访问方式,而不是使用公开的全局变量,好处是只有使用特定的指针才能访问特定的数据。这样就避免了任何试图修改它的非法操作。

 

要做到这一点,我们首先得创建一个堆,然后定义一个指向该堆的指针。这样就只能通过该指针才能访问堆中数据。

 

C++中使用关键字new创建一个堆并分配内存,在new后面跟一个要分配的对象类型,编译器根据这个类型来分配内存。我们来看示例:

 

     int *p;

     p=new int;

 

第一行定义了一个指向整型的指针变量p,第二行用new创建在堆中创建一个int类型的内存区域,然后将该区域的内存地址赋给指针变量p。这样p所指向的就是这块新建的内存区域。在这里要注意的是,new int在堆中被编译器分配了4个字节的空间。假如是new double那么就要被分配8个字节的内存空间。

 

另外,这两句也可以合并为一句,如:

 

int *p=new int;

 

这样在定义指针p的同时初始化了它的值为一个在堆中新建的int型存储区的内存地址。你可以象使用普通指针一样使用它,并把值赋给它所指向的内存空间。

 

*p=1;

 

我们用一段小程序来演示一下它的使用:

 

 

#include <iostream>

using namespace std;

int main()

{

     double *p=new double; 

     *p=1.62; 

     cout<<*p;

     return 0;

}

 

注意:由于你的计算机的内存是有限的,因此可能会出现没有足够内存而无法满足new的请求,在这种情况下,new会返回0,该值被赋给指针后,那么该指针就是一个空指针,空指针不会指向有效数据。New除了返回空值之外,还会引发异常,在后面的异常错误处理中将会讲解。

 

 

 

用指针删除堆中空间

 

由于使用new创建的内存空间不会被系统自动释放,因此假如你不去释放它,那么该区域的内存将始终不能为其他数据所使用,而指向该内存的指针是个局部变量,当定义该指针的函数结束并返回时,指针也就消失了,那么我们就再也找不到该块内存区域,就象重拨键中记录的号码一样,假如重拨键中的号码自动消失了,那么我们将再也访问不到安徽出版社了。同样的道理,假如指向该内存区域的指针自动消失了,计算机就再也找不到该区域的内存了,就好象是丢失了这块内存一样,我们把这种情况叫做内存泄露。这种糟糕的情况将一直持续到程序结束该区域的内存才能恢复使用。因此假如你不需要一块内存空间,那么就必须对指向它的指针使用关键字delete

 

 

#include <iostream>

using namespace std;

int main()

{

     int *p=new int; 

     delete p; 

     return 0;

}

 

 

这将释放指针所指向的内存,而不会释放指针,因此你还可以使用该指针。

 

#include <iostream>

using namespace std;

int main()

{

     int *p=new int; 

     delete p; 

     p=new int;

     delete p;

     return 0;

}

 

但是请不要再次对该指针进行删除

 

#include <iostream>

using namespace std;

int main()

{

     int *p=new int; 

     delete p; 

     p=new int;

     delete p;

     delete p;

     return 0;

}

 

运行后程序崩溃了

 

因为p所指向的内存区域已经被释放,如果再进行释放,将会使程序崩溃。

 

不过,假如我们将该指针赋为0的话,那么删除一个空指针将是安全的。

 

#include <iostream>

using namespace std;

int main()

{

     int *p=new int; 

     delete p; 

     p=new int;

     delete p;

     p=0;

     delete p;

     return 0;

}

 

 

接下来我们用一个程序来总结一下本节所讲的堆中内存。

 

#include <iostream>

using namespace std;

int main()

{

     int *p=new int; 

     *p=3600;

     cout<<*p<<endl;

     delete p;

     cout<<*p<<endl;

     p=0;

     p=new int;

     *p=8;

     cout<<*p<<endl;

     delete p;

     return 0;

}


0 0