CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx

上传人:b****6 文档编号:17292219 上传时间:2022-11-30 格式:DOCX 页数:11 大小:27.22KB
下载 相关 举报
CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx_第1页
第1页 / 共11页
CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx_第2页
第2页 / 共11页
CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx_第3页
第3页 / 共11页
CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx_第4页
第4页 / 共11页
CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx_第5页
第5页 / 共11页
点击查看更多>>
下载资源
资源描述

CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx

《CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx(11页珍藏版)》请在冰豆网上搜索。

CC++内存分配的详细讲解包括堆栈数据段等Word文档下载推荐.docx

//全局(静态)初始化区

p1=(char*)malloc(10);

//分配得来得10字节的区域在堆区

p2=(char*)malloc(20);

//分配得来得20字节的区域在堆区

strcpy(p1,"

);

//123456{post.content}放在常量区,编译器可能会将它与p3所指向的"

优化成一块

}

二.在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

1.栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。

里面的变量通常是局部变量、函数参数等。

2.堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。

如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

3.自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)

三.谈谈堆与栈的关系与区别

具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。

这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。

这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。

因为栈的这种特点,对栈的使用在程序中是非常频繁的。

对子程序的调用就是直接利用栈完成的。

机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。

C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。

和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。

基本的malloc/realloc/free函数维护了一套内部的堆数据结构。

当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。

当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。

这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:

1.系统调用可能不支持任意大小的内存分配。

有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);

这样的话对于大量的小内存分类来说会造成浪费。

2.系统调用申请内存可能是代价昂贵的。

系统调用可能涉及用户态和核心态的转换。

3.没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

堆和栈的对比

从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;

而栈是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。

栈是系统数据结构,对于进程/线程是唯一的;

堆是函数库内部数据结构,不一定唯一。

不同堆分配的内存无法互相操作。

栈空间分静态分配和动态分配两种。

静态分配是编译器完成的,比如自动变量(auto)的分配。

动态分配由alloca函数完成。

栈的动态分配无需释放(是自动的),也就没有释放函数。

为可移植的程序起见,栈的动态分配操作是不被鼓励的!

堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。

1.碎片问题:

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以>

参考数据结构,这里我们就不再一一讨论了。

2.生长方向:

对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;

对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

3.分配方式:

堆都是动态分配的,没有静态分配的堆。

栈有2种分配方式:

静态分配和动态分配。

静态分配是编译器完成的,比如局部变量的分配。

动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

4.分配效率:

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:

分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。

显然,堆的效率比栈要低得多。

明确区分堆与栈:

在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。

首先,我们举一个例子:

voidf()

{

int*p=newint[5];

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?

他分配的是一块栈内存,所以这句话的意思就是:

在栈内存中存放了一个指向一块堆内存的指针p。

在程序会先确定在堆中分配内存的大小,然后调用operatornew分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:

00401028 

push 

14h

0040102A 

call 

operatornew(00401060)

0040102F 

add 

esp,4

00401032 

mov 

dwordptr[ebp-8],eax

00401035 

eax,dwordptr[ebp-8]

00401038 

dwordptr[ebp-4],eax

这里,我们为了简单并没有释放内存,那么该怎么去释放呢?

是deletep么?

澳,错了,应该是delete[]p,这是为了告诉编译器:

我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

好了,我们回到我们的主题:

堆和栈究竟有什么区别?

主要的区别由以下几点:

1、管理方式不同;

2、空间大小不同;

3、能否产生碎片不同;

4、生长方向不同;

5、分配方式不同;

6、分配效率不同;

管理方式:

对于栈来讲,是由编译器自动管理,无需我们手工控制;

对于堆来说,释放工作由程序员控制,容易产生memoryleak。

空间大小:

一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。

但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。

当然,我们可以修改:

打开工程,依次操作菜单如下:

Project->

Setting->

Link,在Category中选中Output,然后在Reserve中设定堆栈的最大值和commit。

注意:

reserve最小值为4Byte;

commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;

由于没有专门的系统支持,效率很低;

由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。

所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。

所以,我们推荐大家尽量用栈,而不是用堆。

另外对存取效率的比较:

代码:

chars1[]="

aaaaaaaaaaaaaaa"

char*s2="

bbbbbbbbbbbbbbbbb"

aaaaaaaaaaa是在运行时刻赋值的(位于栈上);

而bbbbbbbbbbb是在编译时就确定的(位于堆上);

但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

chara=1;

charc[]="

1234567890"

char*p="

a=c[1];

a=p[1];

return;

对应的汇编代码

10:

004010678A4DF1movcl,byteptr[ebp-0Fh]

0040106A884DFCmovbyteptr[ebp-4],cl

11:

0040106D8B55ECmovedx,dwordptr[ebp-14h]

004010708A4201moval,byteptr[edx+1]

004010738845FCmovbyteptr[ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了.

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,编写稳定安全的代码才是最重要的

static用来控制变量的存储方式和可见性

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题:

如果想将函数中此变量的值保存至下一次调用时,如何实现?

最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

static的内部机制:

静态数据成员要在程序一开始运行时就必须存在。

因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。

这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;

二是类定义的内部实现,那里有类的成员函数定义;

三是应用程序的main()函数前的全局数据声明和定义处。

静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。

类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错误的。

它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。

消除时的顺序是初始化的反顺序。

static的优势:

可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。

静态数据成员的值对每个对象都是一样,但它的值是可以更新的。

只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。

引用静态数据成员时,采用如下格式:

<

类名>

<

静态成员名>

如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。

PS:

(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。

(2)不能将静态成员函数定义为虚函数。

(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember函数指针”。

(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就产生了一个意想不到的好处:

成为一个callback函数,使得我们得以将C++和C-basedXWindow系统结合,同时也成功的应用于线程函数身上。

(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。

(6)静态数据成员在<

定义或说明>

时前面加关键字static.

(7)静态数据成员是静态存储的,所以必须对它进行初始化。

(8)静态成员初始化与一般数据成员初始化不同:

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;

初始化时不加该成员的访问权限控制符private,public等;

初始化时使用作用域运算符来标明它所属类;

所以我们得出静态数据成员初始化的格式:

数据类型>

静态数据成员名>

=<

值>

(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。

这里有一点需要注意:

我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?

不会,我们的编译器采用了一种绝妙的手法:

name-mangling用以生成唯一的标志。

补充:

newdelete[],基本类型的对象没有析构函数(例如int,char),所以回收基本类型组成的数组空间deletedelete[]都是应该可以如:

intp=newint[10],deletep和delete[]p都可.但是对于类对象数组(如stringstrArr=newstring[10]),只能delete[].对new的单个对象,只能delete不能delete[]回收空间.

一个典型的嵌入式平台动态内存管理机制

当前,绝大多数嵌入式平台上的软件都采用C语言编写。

除了代码简洁、运行高效之外,灵活操作内存的能力更是C语言的重要特色。

然而,不恰当的内存操作通常也是错误的根源之一。

如“内存泄漏”——不能正确地释放已分配的动态内存,就是一种非常难于检测的存错误。

持续的内存泄漏会使程序性能下降到最终完全不能运行,进而影响到所有其它有动态内存需求的程序,在某些相对简单的嵌入式平台上甚至会妨碍操作系统的运转。

再如“写内存越界”,一种不合法的写内存操作,极可能破坏到本程序中正在使用的其它数据,严重的时候还可能对其它正在运行的程序甚至整个系统造成影响。

为此,本文介绍一个增强的、可定制的动态内存管理模块(以下不妨简称Fense),在C语言提供的内存分配函数基础上,增加了对动态内存的管理功能;

能记录软件运行过程中出现的内存泄漏信息,同时也具一定的监测内存操作的能力;

可以发现绝大多数对动态内存的写越界错误。

Fense的设计原理

通过设立一个双向链表(structHead*stHead)来保存所有被分配的动态内存块的信息。

链表中的每个节点对应一个动态内存块,节点中包括此内存大小、分配发生时所在的源文件名和行号以及被释放的时候,Fense又从st_Head中删除之,检查st_Head中的节点即可得到未被释放的本节点的数值校验和等。

Fense将每一个分配的动态内存块插入到链表st_Head中;

当此内存放内存块信息。

链表节点结构定义如下:

structHead{

charfile;

/分配所在源文件名*/

unsignedlongline;

/*分配所在的行号*/

size_tsize;

/*分配的内存大小*/

intchecksum;

/*链表节点校验和*/

structHeadprev,next;

/*双链表的前后节点指针*/

};

/*全局的双向链表*/

structHead*st_Head=NULL;

为了检测写越界的错误,Fense在用户申请的内存前后各增加了一定大小的内存作为监测区域,并初始化成预定值。

这样,当程序发生越界写操作时,预定值就会发生改变,Fense即可检测到错误。

Fense的具体实现

Fense提供Fense_Malloc、Fense_Free、Fense_Realloc及Fense_Calloc等内存管理函数,功能和调用形式与C语言中的malloc、free、realloc和calloc保持一致。

限于篇幅,这里仅对Fense_Malloc和Fense_Free的实现过程做一个简单描述,

/*内存分配函数*/

void*Fense_Malloc(size_tsize,char*file,unsignedlongline)

//检查Fense的运行时开关,如果Fense被关闭,则调用malloc

//分配并返回

//检查是否零分配,如有则提示警告信息后返回0(用户定制选项)

//分配内存,包括链表节点区域和前/后监测区域

//初始化链表节点,保存分配内存的信息,包括分配的大小、所在文件名和行号

//将此节点插入链表st_Head

//为本节点区域计算校验和

//用预设值初始化前/后监测区域

//用预设值填充用户内存区域(用户定制选项)

//返回用户内存区域的起始位置

/*内存释放函数*/

voidFense_Free(void*uptr,char*file,unsignedlongline)

//检查Fense的运行时开关,如果Fense初关闭,则调用free释译并返回

//检查所有Fense管理下的动态内存(用户定制选项)

//判断当前内存块是否在链表st_Head中,如果不在则提示

//警靠信息,退出(用户定制选项)

//检查当前内存块是否存在越界操作

//将当前内存块的相应的链表节点从st_Head中删除

//重新计算当前节点的前后相邻节点的校验和

//用预设值填充被释放的内存区(用户定制选项)

//调用free释放当前的内存块

用户态与核心态

386及以上的CPU实现了4个特权级模式(WINDOWS只用到了其中两个),其中特权级0(Ring0)是留给操作系统代码,设备驱动程序代码使用的,它们工作于系统核心态;

而特权极3(Ring3)则给普通的用户程序使用,它们工作在用户态。

运行于处理器核心态的代码不受任何的限制,可以自由地访问任何有效地址,进行直接端口访问。

而运行于用户态的代码则要受到处理器的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/OPermissionBitmap)中规定的可访问端口进行直接访问(此时处理器状态和控制标志寄存器EFLAGS中的IOPL通常为0,指明当前可以进行直接I/O的最低特权级别是Ring0)。

以上的讨论只限于保护模式操作系统,象DOS这种实模式操作系统则没有这些概念,其中的所有代码都可被看作运行在核心态。

既然运行在核心态有如此之多的优势,那么病毒当然没有理由不想得到Ring0。

处理器模式从Ring3向Ring0的切换发生在控制权转移时,有以下两种情况:

访问调用门的长转移指令CALL,访问中断门或陷阱门的INT指令。

具体的转移细节由于涉及复杂的保护检查和堆栈切换,不再赘述,请参阅相关资料。

现代的操作系统通常使用中断门来提供系统服务,通过执行一条陷入指令来完成模式切换,在INTELX86上这条指令是INT,如在WIN9X下是INT30(保护模式回调),在LINUX下是INT80,在WINNT/2000下是INT2E。

用户模式的服务程序(如系统DLL)通过执行一个INTXX来请求系统服务,然后处理器模式将切换到核心态,工作于核心态的相应的系统代码将服务于此次请求并将结果传给用户程序。

用户态又称目态,核心态又称管态 

在X86下,可以理解成ring3和ring0 

用户态权限低,无权调用一些核心态才能调用的指令

这是两种内存保护态,一个进程4G地址空间中的每一页均被标记出它是否是处于核心态,所有系统地址空间中的页是核心态,用户空间中的页则为用户态。

访问标记为核心页的的唯一途径是运行在核心态,而只有操作系统和设备驱动才能运行在核心态。

因此一个

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > IT计算机 > 计算机硬件及网络

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1