数据结构动态存储管理.docx
- 文档编号:26811016
- 上传时间:2023-06-22
- 格式:DOCX
- 页数:30
- 大小:84.44KB
数据结构动态存储管理.docx
《数据结构动态存储管理.docx》由会员分享,可在线阅读,更多相关《数据结构动态存储管理.docx(30页珍藏版)》请在冰豆网上搜索。
数据结构动态存储管理
动态存储管理
(对应教材第八章p193—p213)
8·1概述
1、动态存储管理的基本问题
指系统随机的根据用户程序申请来“分配”内存和用户退出运行后,“回收”用户原来占有空间的内存管理。
早期的计算机存储管理由程序员完成。
2、“占用块”和“空闲块”的概念
分配给用户使用的、地址连续的内存区称为“占用块”;未曾分配的(或用户退出的)、地址连续的内存区称为“可利用空闲块”或“空闲块”。
内存分配时从低地址开始,低地址为占用块,高地址为空闲块。
3、系统响应用户请求分配内存的策略
运行一段时间后,当有新用户请求分配内存时,系统如何动作?
策略1:
继续从高地址空闲块分配,直至无法进行分配,再回收所有空闲块,重新组织内存。
策略2:
用户一旦运行结束,系统将其“占用块”释放成“空闲块”,并将其插入到“可利用空间表”中。
内存分配图;
目录表;
起始地址内存快大小使用情况
1000015000空闲
310008000空闲
5900041000空闲
链表
8·2可利用空间表及分配方法
1、可利用空间表的3种结点结构
(1)结点大小相同:
用户所需内存量大小相同,可以把内存分为大小相同的若干块,将各块链接起来,需要时从头上摘取,用完后插入到头上,这实际是链栈。
(2)结点有若干规格:
用户所需内存量不同,但只允许在几种规格间选取。
如大小为2、4、8字节,可以构造3个链表。
用加标记办法区分占用块或空闲块(tag=0,1)。
分配时按类型分配,所需类型没有时,向较大块的类型中去找,取出所需,剩余插入合适链表中。
(3)结点大小不等:
内存块大小不固定,只有一个链表。
开始整个内存是一个空闲块。
以后每个空闲块或占用块均用4个字段标记:
tag,size,space,link。
2、利用空间表的3种分配策略
最先适配法(首先适配法):
最佳适配法:
结点从小到大排序
最差适配法:
结点从大到小排序
3、3种分配策略的比较
最先适配法适合事先不掌握请求分配和释放信息的情况,分配时查询,释放时插入表头。
最佳适配法分配与回收都需要查询。
分配时容易产生存储量小而无法利用的内存小碎片。
这种分配策略适合请求分配内存大小较广的系统。
最差适配法要求结点从大到小排序,适合请求分配内存大小范围较窄的系统。
分配时不需查询,回收时查询,以便插入到适当位置。
8·3边界标识法
可利用空间表是双向循环链表结构。
可使用首次拟合法,也可使用最佳拟合法。
其特点是内存区域大,底部边界上设有标记。
8·3·1可以用空间表的结构
llink
ltag
size
rlink
space
uplink
tag
8·3·2分配算法
从表头指针pav所指结点开始,在可利用空间表中查找第一个不小于n的空闲块m即可分配。
规则如下:
(1)若m-n<=e(e是适当常量),则将m全部分给用户。
为避免修改指针,约定将结点中的高地址部分分配给用户。
(2)每次分配后,令pav指向刚进行分配的结点的后继结点。
分配算法见教材。
8·3·3回收算法
用户释放占用块,系统要立即回收。
为使相邻的空闲块成为较大的结点,要查看左右邻是否为空闲块。
共分4种情况:
(1)空闲块的左右邻均为“占用块”:
只需将空闲块插入可利用空间表中即可。
(2)空闲块的左邻为“占用块”,右邻为空闲块:
将空闲块和右邻空闲块合并,修改循环链表前驱后继指针、空闲块的size、以及uplink等。
(3)空闲块的右邻为“占用块”,左邻为空闲块:
将空闲块和右邻空闲块合并,修改空闲块的size、以及uplink等。
(4)空闲块的左右邻均为空闲块:
将三块空闲块合并,修改循环链表前驱后继指针、空闲块的size、以及uplink等。
教材P201—P204给出了示意性算法。
8·4伙伴系统
与边界标识符类似,不同之处是占用块和空闲块的大小均为2的幂。
8·4·1可以用空间表的结构
若用户内存为2m(0—2m-1),开始是一块空闲块。
运行一段时间后,可能分成若干块,其可能的大小为20,21,222m-1。
相同大小的空闲块组成双向循环链表。
示意图见教材P204。
8·4·2分配算法
取大于等于所申请空间的最小的2的幂次,若正好有这么大的空闲块,则从该循环链表头摘下即可。
否则,应对大于该2的幂次的最小空闲块,依次进行分裂,取下所需空间,将剩余部分插入各个空闲块的链表中。
8·4·3回收算法
两个由同一大空闲块分裂的小块互称“伙伴”,只有互为伙伴的空闲块才能合并。
起始地址为P,大小为2k的内存块,其伙伴起始地址为:
如果PMOD2k+1=0则buddy(p,k)=p+2k
否则PMOD2k+1=2k则buddy(p,k)=p+2;
举例:
8·5无用单元收集
以上几节是应用户要求来“分配”内存和“回收”内存。
1、“无用单元”----用户不再使用,而系统没有回收的结构和变量。
如:
p=malloc(size);
……
p=null;
或
p=malloc(size);
q=p;
free(p);
2、含共享子表的广义表
3、三种标志算法
8·6存储紧缩
1、前几节的特点:
“空闲块”组成可利用空间表。
2、堆:
地址连续的存储区。
设堆指针,指向堆的最低(或最高)地址。
3、分配算法:
4、回收算法:
第八章动态存储管理
一、内容提要
1.1. 动态存储管理指的是在用户需要时给分配内存,而在用户结束使用时,系统要收回用户所占空间。
2.2. 可利用空间表的三种结构形式:
结点固定大小;分几种规格;任意大小。
3.3. 可利用空间表的两种组织形式:
目录表,链表。
4.4. 可利用空间表的分配方式:
首次拟合法,最佳拟合法,最差拟合法。
5.5. 可利用空间表的分配和回收的两种基本实现方法:
边界标识法,伙伴系统。
6.6. 无用单元回收和紧缩存储的概念。
二、学习重点
1、概念:
可利用空间表及分配方式,紧缩存储,伙伴系统,等。
2、边界表示法的分配及回收算法。
3、伙伴系统的分配及回收算法。
三、例题解析
设有大小为512字节的存储,有6个用户申请大小分别为23,45,52,100,11和19
字节的存储空间,然后再顺序释放大小为45,52和11的占用空间。
假设以伙伴系统实现动态存储管理。
1.1. 画出可利用空间表的初始化状态。
2.2. 画出为6个用户分配的存储空间后可利用空间表的状态,以及每个用户得到的存储块的起始地址。
3.3. 画出在3个占用块回收后可利用空间表的状态。
【解答】
1.1. 因为512=29,所以初始化为:
20
^
21
^
22
^
23
^
24
^
25
^
26
^
27
^
28
^
29
0
9
2,
(1)23<=25=32,所以要分配25字节一块占用块首址0。
因无25空闲块,故原先29块分裂为28、、27、26、25各一块,首址依次为256,128,64,32。
(0—31给了申请23的用户)。
(2)45〈=26=64,故将26空闲块(64—127)给了该用户,其首址为64。
(3)因52〈=26=64,这时已无26大小空闲块,故将27块分裂,一块26大小给了用户(128—129),起始地址128,其伙伴起始地址192(192—255)挂到26空闲块链表中。
(4)100〈=27=128,因无27空闲块,28块产生分裂,其中首址256(256—383)的一半分给用户,其伙伴(首址384)挂到27空闲块链表中。
(5)11〈=16=24,因无24空闲块,25产生分裂,一半(首址32(32—47))分给用户,其伙伴(首址48)挂到24大小的空闲块链块表。
(6)19〈=32=25。
因无25空闲块,26块分裂,一半(首址192(192—223))分给用户,其伙伴(首址224)挂到25空闲块链表中。
总之,6个用户占用块首址依次为0,64,128,256,32和192,这时可利用空间表的状态为:
20
^
21
^
22
^
23
^
24
25
26
^
27
28
^
29
^
│0│4│
│0│5│
│0│7│
3、回收
(1)
(1) 45〈=26=64,其首址为64,因64MOD26+1=26,所以其伙伴地址为64-64=0(不空)其伙伴占用,故仅将此结点挂到26空闲块链表中。
(2)
(2) 52<=26=64,其首址为128,因128MOD26+1=0,所以,伙伴地址=128+64=192(占用),故仅将此块挂到26空闲块链表中。
(3)(3) 11〈=24=16,首址32
因32MOD24+1=0,伙伴地址32+24=48(空闲),故合并成25(块)。
因32MOD25+1=2,新伙伴地址为32-32=0(不空),故将25一块挂于
25的空闲块链表中。
总之,三块释放后空闲表状态如下:
20
^
21
^
22
^
23
^
24
^
25
26
27
28
^
29
^
│0│7│
首地址384
│0│6│
首地址64
│0│6│
首地址128
│0│5│
首地址224
│0│5│
首地址32
动态存储管理
为什么需要动态存储管理
程序中需要用变量(各种简单类型变量、数组变量等)保存被处理的数据和各种状态信息,变量在使用之前必须安排好存储:
放在哪里、占据多少存储单元,等等,这个工作被称作存储分配。
用机器语言写程序时,所有存储分配问题都需要人处理,这个工作琐碎繁杂、很容易出错。
在用高级语言写程序时,人通常不需要考虑存储分配的细节,主要工作由编译程序在加工程序时自动完成。
这也是用高级语言编程序效率较高的一个重要原因。
C程序里的变量分为几种。
外部变量、局部静态变量的存储问题在编译时确定,其存储空间的实际分配在程序开始执行前完成。
程序执行中访问这些变量,就是直接访问与之对应的固定位置。
对于局部自动变量,在执行进入变量定义所在的复合语句时为它们分配存储。
应该看到,这种变量的大小也是静态确定的。
例如,局部自动数组的元素个数必须用静态可求值的表达式描述。
这样,一个函数在调用时所需的存储量(用于安放该函数里定义的所有自动变量)在编译时就完全确定了。
函数定义里描述了所需要的自动变量和参数,定义了数组的规模,这些就决定了该函数在执行时实际需要的存储空间大小。
以静态方式安排存储的好处主要是实现比较方便,效率高,程序执行中需要做的事情比较简单。
但这种做法也形成了对写程序方式的一种限制,使某些问题在这个框架里不好解决。
举个简单的例子:
假设现在要写一个处理一组学生成绩数据的程序,被处理数据需要存储,因此应该定义一个数组。
由于每次使用程序时要处理的成绩的项数可能不同,我们可能希望在程序启动后输入一个表示成绩项数的整数(或通过命令行参数提供一个整数,问题完全一样)。
对于这个程序,应该怎样建立其内部的数据表示呢?
问题在于写程序时怎样描述数组元素的个数。
一种理想方式是采用下面的程序框架:
intn;
...
scanf("%d",&n);
doublescores[n];
.../*读入成绩数据,然后进行处理*/
但是这一做法行不通。
这里存在两个问题:
首先是变量定义不能出现在语句之后。
这个问题好解决,可以引进一个复合语句,把scores的定义放在复合语句里。
第二个问题更本质,在上面程序段里,描述数组scores大小的表达式是一个变量,它无法静态求出值。
也就是说,这个数组大小不能静态确定,C语言不允许以这种方式定义数组。
这个问题用至今讨论过的机制都无法很好解决。
目前可能的解决方案有(一些可能性):
1.分析实际问题,定义适当大小的数组,无论每次实际需要处理多少数据都用这个数组。
前面的许多程序采用了这种做法。
如果前期分析正确,这样做一般是可行的。
但如果某一次实际需要处理的数据很多,程序里定义数组不够大,这个程序就不能用了(当然,除非使用程序的人有源程序,而且知道如果修改程序,如何编译等等。
在现实生活中,这种情况是例外)。
2.定义一个很大的数组,例如在所用的系统里能定义的最大数组。
这样做的缺点是可能浪费大量空间(存储器是计算机系统里最重要的一种资源)。
如果在一个复杂系统里,有这种情况的数组不止一个,那就没办法了。
如果都定义得很大,系统可能根本无法容纳它们。
而在实际计算中,并不是每个数组都真需要那么大的空间。
上面只是一个说明情况的例子。
一般情况是:
许多运行中的存储需求在写程序时无法确定。
通过定义变量的方式不能很好地解决这类问题。
为此就需要一种机制,使我们能利用它写出一类程序,其中可以根据运行时的实际存储需求分配适当大小的存储区,以便存放到在运行中才能确定大小的数据组。
C语言为此提供了动态存储管理系统。
说是“动态”,因为其分配工作完全是在动态运行中确定的,与程序变量的性质完全不同。
程序里可以根据需要,向动态存储管理系统申请任意大小的存储块。
现在有了动态存储分配,可以要求系统分配一块存储,但是怎么能在程序里掌握和使用这种存储块呢?
对于普通的变量,程序里通过变量名去使用它们。
动态分配的存储块无法命名(命名是编程序时的手段,不是程序运行中可以使用的机制),因此需要另辟蹊径。
一般的语言里都通过指针实现这种访问,用指针指向动态分配得到的存储块(把存储块的地址存入指针),而后通过对指针的间接操作,就可以去使用存储块了。
引用动态分配的存储块是指针的最主要用途之一。
与动态分配对应的是动态释放。
如果以前动态分配得到的存储块不再需要了,就应该考虑把它们交回去。
动态分配和释放的工作都由动态存储管理系统完成,这是支持程序运行的基础系统(称为程序运行系统)的一部分。
这个系统管理一片存储区,如果需要存储块,就可以调用动态分配操作申请一块存储;如果以前申请的某块存储不需要了,可以调用释放操作将它交还管理系统。
动态存储管理系统管理的这片存储区通常称为堆(heap)。
7.6.2C语言的动态存储管理机制
C语言的动态存储管理由一组标准库函数实现,其原型在标准文件
与动态存储分配有关的函数共有四个:
1)存储分配函数malloc()。
函数原型是:
void*malloc(size_tn);
这里的size_t是标准库里定义的一个类型,它是一个无符号整型。
这个整型能够满足所有对存储块大小描述的需要,具体相当于哪个整型由具体的C系统确定。
malloc的返回值为(void*)类型(这是通用指针的一个重要用途),它分配一片能存放大小为n的数据的存储块,返回对应的指针值;如果不能满足申请(找不到能满足要求的存储块)就返回NULL。
在使用时,应该把malloc的返回值转换到特定指针类型,赋给一个指针。
例:
利用动态存储管理机制,前面提出的问题可以采用如下方式解决:
intn;
double*scores;
...
scanf("%d",&n);
scores=(double*)malloc(n*sizeof(double));
if(scores==NULL){
..../*出问题时的处理,根据实际情况考虑*/
}
..scores[i]...*(scores+j).../*读入数据进行处理*/
调用malloc时,应该利用sizeof计算存储块的大小,不要直接写整数,以避免不必要的错误。
此外,每次动态分配都必须检查成功与否,并考虑两种情况的处理。
注意,虽然这里的存储块是通过动态分配得到的,但是它的大小也是确定的,同样不允许越界使用。
例如上面程序段分配的块里能存n个双精度数据,随后的使用就必须在这个范围内进行。
越界使用动态分配的存储块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统垮台。
2)带计数和清0的动态存储分配函数calloc。
函数原型是:
void*calloc(size_tn,size_tsize);
参数size意指数据元素的大小,n指要存放的元素个数。
calloc将分配一块存储,其大小足以存放n个大小各为size的元素,分配之后还把存储块里全部清0(初始化为0值)。
如果不能满足要求就返回NULL。
例:
前面程序片段里的存储分配也可以用下面语句实现:
scores=(double*)calloc(n,sizeof(double));
注意,malloc对于所分配区域不做任何事情,calloc对整个区域进行初始化,这是两个函数的主要不同点。
另外就是两个函数的参数不同,calloc主要是为了分配“数组”。
我们可以根据情况选用。
3)动态存储释放函数free。
原型是:
voidfree(void*p);
函数free释放指针p所指的存储块。
指针p的值(存储块地址)必须是以前通过动态存储分配函数分配得到的。
如果当时p的值是空指针,free就什么也不做。
注意,调用free(p)不会改变p的值(在函数里不可能改变值参数p),但被p指向的存储块的内容却可能变了(可能由于存储管理的需要)。
释放后不允许再通过p去访问已释放的块,否则也可能引起灾难性后果。
为了保证动态存储区的有效使用,在知道某个动态分配的存储块不再用时,就应及时将它释放,这应该成为习惯。
释放动态存储块只能通过调用free完成。
下面是一个示例:
intfun(...){
int*p;
...,..
p=(int*)malloc(...);
...
free(p);
return...;
}
这里的free(p)在fun退出前释放了在函数里分配的存储块。
如果没有最后的这个free(p),函数里分配的这个存储块就可能丢掉。
因为fun的退出也是p的存在期结束,此后p保存的信息(动态存储块地址)就找不到,这个块就可能丢掉了[1]。
丢失动态分配块的情况称为动态存储的“流失”。
对于需要长时间执行的程序,存储流失就可能成为严重问题,可能造成程序执行一段后被迫停止。
因此,实际系统不能容忍这种情况的发生。
4)分配调整函数realloc。
函数原型是:
void*realloc(void*p,size_tn);
这个函数用于更改以前的存储分配。
在调用realloc时,指针变量p的值必须是以前通过动态存储分配得到的指针,参数n表示现在需要的存储块大小。
realloc在无法满足新要求时返回NULL,同时也保持p所指的存储块的内容不变。
如果能够满足要求,realloc就返回一片能存放大小为n的数据的存储块,并保证该块的内容与原块一致:
如果新块较小,其中将存放着原块里大小为n的范围内的那些数据;如果新块更大,原有数据存在新块的前面一部分里,新增的部分不自动初始化。
如果分配成功,原存储块的内容就可能改变了,因此不允许再通过p去使用它。
假如要把一个现有的双精度块改为能存放m个双精度数,可以用下面程序段处理:
q=(double*)realloc(p,m*sizeof(double));
if(q==NULL){
....../*分配不成功,p仍指向原块,处理这种情况*/
}
else{
p=q;
....../*分配成功,通过p可以去用新的存储块*/
}
上面的q是另一个双精度指针。
这里没有把realloc的返回值赋给直接p,是为了避免分配失败时丢掉原存储块。
如果直接赋值,指针p原来的值就会丢掉。
如果当时的分配没有成功,p将被赋空指针值,原来那个块可能就再也找不到了(除非在这次调整前已经让另一个指针指向了它)。
请注意:
通过动态分配得到的块是一个整体,只能作为一个整体去管理(无论是释放还是改变大小)。
在调用free(p)或者realloc(p,...)时,p当时的值必须是以前通过调用存储分配函数得到的,绝不能对指在动态分配块里其他位置的指针调用这两个函数(更不能对并不指向动态分配块的指针使用它们),那样做的后果不堪设想。
7.6.3两个程序实例
例:
修改筛法程序,令它由命令行参数得到所需的整数范围。
如果没有命令行参数,就要求用户输入一个确定范围的整数值。
先考虑main的设计。
为了使程序更加清晰,我们可以考虑把筛法计算写成一个函数。
这里还有一个小问题:
如果用户通过命令行参数给出工作范围,程序就需要从命令行参数字符串计算出对应的整数。
为此我们定义如下函数:
ints2int(chars[]);
再利用原来的getnumber函数,这个程序的main可以定义为:
enum{LARGEST=32767};
intmain(intargc,char**argv)
{
inti,j,n,*ns;
if(argc==2)n=s2int(argv[1]);
elsegetnumber("Largestnumbertotest:
",2,LARGEST,5,&n);
if(n<2||n>LARGEST){
printf("Largestnumbermustinrange[2,%d]",LARGEST);
return1;
}
if((ns=(int*)malloc(sizeof(int)*(n+1)))==NULL){
printf("Noenoughmemory!
\n");
return2;
}
sieve(n,ns);
for(j=1,i=2;i<=n;++i)
if(ns[i]==1){
printf("%7d%c",i,(j%8==7?
'\n':
''));
++j;
}
putchar('\n');
free(ns);
return0;
}
主函数被清晰地分为三部分:
准备工作,主要处理部分,输出与结束。
如果程序得到的范围不合要求,它就打印错误信息并立即结束。
正常情况下完成筛法计算并产生输出。
使用动态存储管理的要点
1)必须检查分配的成功与否。
人们常用的写法是:
if((p=(...*)malloc(...))==NULL){
...../*对分配未成功情况的处理*/
}
2)系统对动态分配块的使用不做任何检查。
编程序的人需要保证使用的正确性,绝不可以超出实际存储块的范围进行访
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 数据结构 动态 存储 管理