数据结构之动态存储管理(C语言)

一、 概述
1. 占用块
占用块:已分配给用户使用的地址连续的内存区
可利用空间块:未曾分配的地址连续的内存区
2. 动态存储分配过程的内存状态

这里写图片描述

系统运行一段时间后,有些程序的内存被释放,造成了上图(b)中的状态。假如此时又有新的程序请求分配内存,系统将如何做呢?

通常有两种做法:一种策略是继续从高地址的空闲块中进行分配,而不考虑已经分配给用户的内存区是否已空闲,知道分配无法进行(即剩余的空闲块不能满足分配的请求)时,系统才回收所有用户不再使用的空闲块,并且重新组织内存,将所有空闲的内存区连接在一起成为一个大的空闲块。另一种策略是用户一旦运行结束,便将它所占内存区释放成为空闲区,同时,当用户请求新的内存分配时,系统需要检查整个内存区中的所有空闲块,并从中找出一个“合适”的空闲哭分配之。对于第二种策略,需要建立一张记录所有空闲区的“可利用空间表”,此表可以是“目录表”,也可以是链表。

这里写图片描述

二、 可利用空间表及分配方法
可利用空间表也称为存储池。
1. 可利用空间表的三种不同结构形式
(1)系统运行期间所有用户请求分配的存储量大小相同。对此类系统,在系统开始运行时将它使用的内存区按所需大小分割成若干大小相同的块,然后用指针链接成一个可利用空间表。因为表中节点的大小相同,则分配时无需查找,只要将表中的第一个节点分配给用户即可;同样,当用户释放内存时,系统只要将用户释放的空闲块插入表头即可。
(2)第二种情况,系统运行期间用户请求分配的存储量有若干种大小的规格。对此类系统,一般情况下建立若干个可利用空间表,同一链表中的节点大小相同。此时的分配和回收在很大程度上和第一种情况类似,只是当节点大小和请求分配的内存大小相同的链表为空时,需要查询节点较大的链表,并从中取出一个节点,将其中一部分内存分配给用户,而将剩余部分插入到相应大小的链表中。回收和第一种情况相同。然而,这种情况的系统还有一个特殊的问题要处理:即当节点与请求相符的链表和节点更大的链表均为空时,分配不能进行,而实际上内存空间并不一定不存在所需大小的连续空间,只是由于在系统运行过程中,频繁出现小块的分配和回收,导致大街店链表中的空闲块被分隔成小块后插入在小节点的链表中,此时若要系统继续运行,就必须重新组织内存,即执行“存储紧缩”的操作。
(3)第三种情况,系统在运行期间分配给用户的内存块的大小不固定,可以随请求而变。因此,可利用空间表中的节点即空闲块的大小也是随意的。

这里写图片描述

这种情况下节点的结构与之前的有点不一样,需要有一个size域来记录该空闲块的大小。
由于可利用空间块中节点大小不同,则在分配时就有一个如何分配的问题。假设某用户需大小为n的内存,而可利用空间表中仅有一块大小为m》n的空闲块,则直接将这个空闲块分配给申请的用户,同时将剩余大小为m-n的部分作为一个节点落在链表中即可。若可利用空间表中有若干个不小于n的空闲块时,那该如何分配呢?下面给出三种不同的分配策略。
a) 首次拟合法
从表头指针开始查找可利用空间表,将找到的第一个大小不小于n的空闲块的一部分分配给用户。
b) 最佳拟合法
将可利用空间表中不小于n且最接近n的空闲块的一部分分配给用户。在用最佳拟合法进行分配时,为了避免每次分配都要扫视整个链表,通常预先设定可利用空间表的结构按空间块的大小由小至大有序。因此分配时,只需找到第一块大于n的空闲块即可进行分配;但在回收时,必须将释放的空闲块插入到合适的位置上去。
c) 最差拟合法
将可利用空间表中不小于n且是链表中最大的空闲块的一部分分配给用户。同样,为了节约时间,此时的可利用空间表的结构应按空闲块的大小自大至小有序。这样每次分配的时候无需查找,只需判断链表的第一个节点大小是否大于所需要的要求,如果满足则直接分配。当然,回收时也要将空闲块插入可利用空间表中合理的位置。

三种分配策略各有所长。一般来说,最佳拟合法适用于请求分配的内存大小范围较广的系统。最差你和法适用于请求分配的内存范围较窄的系统。而首次拟合法的分配时随机的。
但是从分配和回收来上看,首次拟合法在分配时需要查找可利用空间表,而回收时只需直接将节点插入表头即可;而最佳拟合法无论是分配和回收均需查找链表,最费时间;而最差拟合法,分配时不需要查找,但回收时需要。

另外存在的一个问题时,相邻空闲块的节点合并问题。所以在回收空闲块时,首先应检查地址与它相邻的内存是否是空闲块。
三、边界标识法(C语言实现)
1.思想
将系统中所有空闲块链接在一个双重循环链表结构的可利用空间表中,然后根据上面提到的三种分配策略中的一种进行分配内存(这里是实现首次拟合法)。系统的特点在于,在每个内存区的头部和底部两个边界上设有标识,以标识该区域为占用块或空闲块,使得在回收用户释放的空闲块时易于判别在物理位置上与其紧邻内存区域是否为空闲块,以便将所有地址连续的空闲块组合成一个尽可能大的空闲块。
2.可利用空间表的结构
可利用空间表节点结构:

这里写图片描述

每个节点有两个边界,一个是head,一个是foot;其中head中有四个成员变量,分别是llink、tag、size和rlink;llink的含义是指向前驱节点;tag是用于标识该内存块的状态,其中tag为1表示该内存块已被占用,tag为0表示该内存块为空闲;size用于指示当前内存块的大小(size这个大小包括了head和foot所占的空间);rlink的含义是指向后继节点。
foot中有两个成员,一个是uplink,用于指向当前内存块的起始地址,这个成员在内存回收时起了很大的作用,tag用于表示内存块的状态,和head中的tag一样。

在这里要特别说明的一点,其实foot的本质应该如下图:

这里写图片描述

只不过是size和rlink域没有使用而已。之所以说明这一点,就是为了下面说明我们结构体的定义中将uplink和llink放在一个共用体中的原因。
typedef struct Word
{
    union
    {
        struct Word *llink;     //前驱
        struct Word *uplink;    //指向头部地址    
    };
    int tag;        //tag为0表示空闲,tag为1表示占用   
    int size;       //内存块大小
    struct Word *rlink; //后继

}Word, Head, Foot, *Space;
在说明将uplink和llink放在一个共用体中之前,我们要明确的一点是,foot和head是两个不同的字,当使用每个内存区的head时,我们要使用的是llink域;而要使用foot时,我们要使用的是uplink域;即同一个结构中,根据不同情况使用不同的成员,正好符合共用体的使用。
3.代码实现(C语言)
在实现代码之前,先进行几点说明:
(1)这里实现的是首次拟合法;
(2)采用的是不带头结点的双向循环链表,所以当链表中无节点时,表头指针为空;

在Boundary.h中实现结构体定义,以及内存操作函数的声明:
/*
内存分配策略:
           插入      回收    特点
首次拟合法:O(n)      O(1)   链表无序,
最佳拟合法:O(n)      O(n)   链表有序,导致空闲内存两极化
最差拟合法:O(1)      O(1)   链表有序,导致内存平均化
*/

#ifndef BOUNDARY_H_
#define BOUNDARY_H_

#define FootLoc(p) (p + p->size - 1)    //计算块尾部
#define MEM_SIZE  1024          //字的个数  
#define EPISION   10            //整块分配的临界阈值

typedef struct Word //双向循环链表
{
    union
    {
        struct Word *llink;    //前驱
        struct Word *uplink;   //指向当前内存块的首地址
    };

    int tag;    //0表示空闲,1表示占用
    int size;   //空闲内存块大小,包含头和尾
    struct Word *rlink; //后继
}Word, Head, Foot, *Space;

//先创建内存池,用于模拟内存的分配和回收
Space CreateMem();
Space AllocBoundTag(Space *pav, int n);

void ShowUsed(Space *pav, int len);
void ShowSpace(Space pav);
void Destroy(Space s);
void Free(Space *pav, Space p);
#endif
在Boundary.c中实现以上操作:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "Boundary.h"

typedef struct Word
{
    union
    {
        struct Word *llink;     //前驱
        struct Word *uplink;    //指向头部地址    
    };
    int tag;        //tag为0表示空闲,tag为1表示占用   
    int size;       //内存块大小
    struct Word *rlink; //后继

}Word, Head, Foot, *Space;

#define FootLoc(p) (p + p->size - 1)    //计算块尾部
#define MEM_SIZE  1024          //字的个数  
#define EPISION   10            //整块分配的临界阈值

//申请内存池
Space CreateMem()
{
    Word *p = (Word*)malloc(MEM_SIZE * sizeof(Word));
    assert(NULL != p);

    p->llink = p;  //双向循环链表初始化
    p->rlink = p;
    p->size = MEM_SIZE;
    p->tag = 0;

    FootLoc(p)->uplink = p;
    FootLoc(p)->tag = 0;

    return p;
}

//内存分配
Space MemAlloc(Space *pav, int n)
{
    if (NULL == *pav)
    {
        return NULL;
    }
    //分配前先在空闲链表中找合适的空闲块
    Word *pTemp = *pav; 
    do
    {
        if (pTemp->size >= n)
        {
            break;
        }
        pTemp = pTemp->rlink;
    } while (pTemp != *pav);
    if (pTemp->size < n)    //没有找到合适的空闲块
    {
        return NULL;
    }
    else                //找到合适的空闲块
    {
        if (pTemp->size - n <= EPISION) //整块分配
        {
            if (pTemp->rlink == *pav)   //说明此时只有一个空闲块节点,此时整块分配以后链表为空
            {
                *pav = NULL;
            }
            else
            {   //如果将此节点分配以后链表中还有其他空闲节点,则删除此已分配的空闲节点
                pTemp->llink->rlink = pTemp->rlink;
                pTemp->rlink->llink = pTemp->llink;
            }
            pTemp->tag = 1;
            FootLoc(pTemp)->tag = 1;
            return pTemp;   //将分配出去的内存首地址返回
        }
        else    //只分出高地址的部分内存
        {
            pTemp->size -= n;
            FootLoc(pTemp)->tag = 0;
            FootLoc(pTemp)->uplink = pTemp;
            Word *pTail = FootLoc(pTemp) + 1;
            pTail->tag = 1;
            pTail->size = n;
            FootLoc(pTail)->tag = 1;
            FootLoc(pTail)->uplink = pTail;
            return pTail;
        }
    }
    return NULL;
}

//打印占用块信息
void ShowUsed(Space *UsedMem, int iLen)
{
    printf("占用块信息:\n");
    for (int i = 0; i < iLen; ++i)
    {
        if (UsedMem[i] != NULL)
        {
            printf("起始地址:%d, 内存块大小:%d, 结束地址:%d\n",
                UsedMem[i], UsedMem[i]->size, FootLoc(UsedMem[i]) + 1);
        }
    }
}

//打印空闲块信息
void ShowSpace(Space pav)
{
    if (NULL == pav)
    {
        return;
    }
    Space pTemp = pav;
    printf("空闲块信息:\n");
    do
    {
        printf("起始地址:%d, 内存块大小:%d, 结束地址:%d\n",
            pTemp, pTemp->size, FootLoc(pTemp));
        pTemp = pTemp->rlink;
    } while (pTemp != pav);
}

//释放某个占用块
void Free(Space *pav, Space p)
{
    if (NULL == *pav)   //如果链表为空,即此时没有空闲块
    {
        p->tag = 0;
        p->llink = p;
        p->rlink = p;
        FootLoc(p)->tag = 0;
        FootLoc(p)->uplink = p;

        *pav = p;
        return;
    }

    Space left;
    Space right;
    //在这里才可以体会到在内存块尾部设立uplink和tag的作用,不然没办法找到物理上的前面内存块的状态,无法进行合并
    if ((p - 1)->tag != 0 && (FootLoc(p) + 1)->tag != 0) 
    { //左右均不为空, 直接将p所指的内存插在pav之前
        left = (*pav)->llink;
        p->tag = 0;
        FootLoc(p)->tag = 0;
        p->llink = left;
        p->rlink = (*pav);
        FootLoc(p)->uplink = p;
        left->rlink = p;
        (*pav)->llink = p;
    }
    else if ((p - 1)->tag == 0 && (FootLoc(p) + 1)->tag != 0)
    {   //左空右不空,直接将p接在左边内存块的后面
        left = (p-1)->uplink;
        left->size += p->size;
        FootLoc(p)->uplink = left;
        FootLoc(p)->tag = 0;
    }
    else if ((p - 1)->tag != 0 && (FootLoc(p) + 1)->tag == 0)
    {   //右空左不空
        left = (*pav)->llink;
        right = FootLoc(p) + 1;
        //将节点p查到pav的前面
        p->tag = 0;
        left->rlink = p;
        p->llink = left;
        p->rlink = (*pav);
        (*pav)->llink = p;
        //然后删除右边的可合并节点
        right->llink->rlink = right->rlink;
        right->rlink->llink = right->llink;
        //合并节点
        p->size += right->size;

        FootLoc(p)->tag = 0;
        FootLoc(p)->uplink = p;
        *pav = p;
    }
    else if ((p - 1)->tag == 0 && (FootLoc(p) + 1)->tag == 0)
    {   //左右均空
        left = (p - 1)->uplink; //物理左
        right = FootLoc(p) + 1; //物理右
        //先删除物理右
        right->llink->rlink = right->rlink;
        right->rlink->llink = right->llink;
        left->size += p->size + right->size;
        FootLoc(right)->uplink = left;
        *pav = left;
    }
}
int main(void)
{
    Word *pav = CreateMem();
    Word *UsedMem[10] = { NULL };        //用于存已分配的节点的首地址
    UsedMem[0] = MemAlloc(&pav, 200);
    UsedMem[1] = MemAlloc(&pav, 300);
    UsedMem[2] = MemAlloc(&pav, 400);
    UsedMem[3] = MemAlloc(&pav, 100);
    UsedMem[4] = MemAlloc(&pav, 20);    //此时还剩下24个字,而整块分配的临界值为10,所以应该将24个字全部分配
    //////  1   /////////
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    printf("==============================================\n"); //测试右空左不空
    Free(&pav, UsedMem[0]);
    UsedMem[0] = NULL;
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    printf("==============================================\n"); //测试左右均不为空
    Free(&pav, UsedMem[1]);
    UsedMem[1] = NULL;
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    printf("==============================================\n"); //测试左空右不空
    Free(&pav, UsedMem[3]);
    UsedMem[3] = NULL;
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    printf("==============================================\n"); //测试左空右不空
    Free(&pav, UsedMem[2]);
    UsedMem[2] = NULL;
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    printf("==============================================\n"); //测试左空右不空
    Free(&pav, UsedMem[4]);
    UsedMem[4] = NULL;
    ShowSpace(pav);
    ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
    return 0;
}
运行结果如下:

这里写图片描述

最后再对main函数中的内存分配情况用图加以说明,当执行到main函数中标签1处为止,内存的分布如下图:

这里写图片描述

每次分配时,总是先从高地址进行分配,举个简单的例子进行说明,假设现在有两个空闲块节点,一个节点大小为100,一个节点大小为400,这两个节点在链表中的关系如下图:

这里写图片描述

现要求分配内存大小为200,则此时需要从节点大小为400的内存块中进行分配。现在分别从节点大小为400的低地址开始分配200和从高地址为尾部分配200两种情况进行说明:
a)从节点大小为400的低地址开始分配200

这里写图片描述

b)从节点大小为400的高地址分配200

这里写图片描述

从上面两图可以看出,如果从低地址开始分配,则需要修改内存块的head,而如果从高地址开始分配,则只需修改内存块的foot即可,相比而言更加方便和简单。
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值