点击这里给我发消息 点击这里给我发消息

C++内存管理学习笔记(1)

添加时间:2013-12-7
    相关阅读: 技术 程序 C语言 C++ VC 优化 系统
 

/****************************************************************/

/*            学习是合作和分享式的!

/* Author:Atlas                  
/*  转载请注明本文出处:
 

/****************************************************************/

一 C++内存管理

1.内存分配方式

在讲解内存分配之前,首先,要了解程序在内存中都有什么区域,然后在详细分析各种分配方式。

1.1 C语言和C++内存分配区

下面的三张图,图1图2是一种比较详细的C语言的内存区域分法。图3是典型的C++内存分布图,简单易懂;以下内存分配图,区别就是图1和2则分为初始化和未初始化静态变量区,图3中是全局变量区。

C语言(图1和图2):(由地地址到高地址)

a)正文段:用来存放程序执行代码。通常,正文段是可共享的。另外,正文段常常是只读的,一次防止程序由于意外修改其自身。

b)初始化数据段:用来存放程序中已初始化的全局变量。数据段属于静态内存分配。

c)非初始化数据段:通常称为BSS段, 用来存放程序中未初始化的全局变量。BSS是英文Block Started by Symbol(由符号开始的块)的简称。BSS段属于静态内存分配。 在程序开始执行之前,内核将此段中的数据初始化为0或者空指针。

d)堆:堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上 (堆被扩张)/释放的内存从堆中被剔除(堆被缩减)。

e)栈:栈又称堆栈, 存放程序的局部变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,栈用来传递参数和返回值。由于栈 的先进先出特点,所以栈特别方便用来保存/恢复调用现场。

\

图1  典型C语言内存分布区域 (UNIX高级环境编程)                                              

\

图2 典型C语言内存分布区域

 

C++(图3):

根据《C++内存管理技术内幕》一书,在C++中,内存分成5个区,他们分别是堆,栈,自由存续区,全局/静态存续区,常量存续区。

a) 栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

b) 堆:内存使用new进行分配使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。

c) 自由存储区:使用malloc进行分配,使用free进行回收。和堆类似。

d) 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。

e) 常量存储区:存储常量,不允许被修改。

这里,在一些资料中是这样定义C++内存分配的,可编程内存在基本上分为这样的几大部分:静态存储区、堆区和栈区。他们的功能不同,对他们使用方式也就不同。

a)静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。

b)栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

c)堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。

\

图3 典型c++内存区域

总结:C++与C语言的内存分配存在一些不同,但是整体上就一致的,不会影响程序分析。就C++而言,不管是5部分还是3大部分,只是分法不一致,将5部分中的c)d)e)合在一起则是3部分的a)。

1.2区分堆、栈、静态存储区

我们通过代码段来看看对这样的三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。

(1)静态存储区与栈区

   1: char* p = “Hello World1”;     2: char a[] = “Hello World2”;     3: p[2] = ‘A’;     4: a[2] = ‘A’;     5: char* p1 = “Hello World1;”
        这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据“Hello World1”和数据“Hello World2”是存储于不同的区域的。

因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据。

(2)堆与栈区别

我们先通过例子1来直观的说明下栈与堆内存的区别,然后在细致分析例子2中的情况。

例子1:
   1: void fn(){   2:     int* p = new int[5];   3: }

看到new,首先应该想到,我们分配了一块堆内存,那么指针p呢? 它分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中。

注意:这里为了简单并没有释放内存,那么该怎么去释放呢? 是deletep么? NO,错了,应该是delete [ ] p,这是告诉编译器:删除的是一个数组。


例子2:

   1: int a = 0; //全局初始化区   2: char *p1; //全局未初始化区   3: main()   4: {   5:     int b;  //栈   6:     char s[] = "abc"; //栈   7:     char *p2; //栈   8:     char *p3 = "123456";   // 123456\0在常量区,p3在栈上。   9:     static int c =0;     //全局(静态)初始化区  10:     p1 = (char *)malloc(10);  11:     p2 = (char *)malloc(20);  12:         //分配得来得10和20字节的区域就在堆区。  13:     strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。  14: }
例子3:

   1: char* f1()     2: {     3: char* p = NULL;     4: char a;     5: p = &a;     6: return p;     7: }     8: char* f2()     9: {    10: char* p = NULL:    11: p =(char*) new char[4];    12: return p;    13: }

这两个函数都是将某个存储空间的地址返回,二者有何区别呢?f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。所以,当调用f1()函数时,如果程序中有下面的语句:

   1: char* p ;     2: p = f1();     3: *p = ‘a’;

此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。但是,相比之下,f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:

   1: void f()     2: {     3: …     4: char * p;     5: p = (char*)new char[100];     6: …     7: }

这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏(memory leak)。

综合以上两个例子,我们可以总结一下堆与栈到底有哪些区别!

(1)管理方式不同

对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

(2)空间大小不同

空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6.0下面默认的栈空间大小是1M,可以修改这个值。

(3)能否产生碎片不同

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。

(4)生长方向不同

对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。(详见第一部分的内存分配图)

(5)分配方式不同

堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

(6)分配效率不同

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

总结:

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难。

 

咨询热线:020-85648757 85648755 85648616 0755-27912581 客服:020-85648756 0755-27912581 业务传真:020-32579052
广州市网景网络科技有限公司 Copyright◎2003-2008 Veelink.com. All Rights Reserved.
广州商务地址:广东省广州市黄埔大道中203号(海景园区)海景花园C栋501室
= 深圳商务地址:深圳市宝源路华丰宝源大厦606
研发中心:广东广州市天河软件园海景园区 粤ICP备05103322号 工商注册