1. 程式人生 > >資料結構之動態儲存管理(C語言)

資料結構之動態儲存管理(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即可,相比而言更加方便和簡單。

相關推薦

資料結構動態儲存管理(C語言)

一、 概述 1. 佔用塊 佔用塊:已分配給使用者使用的地址連續的記憶體區 可利用空間塊:未曾分配的地址連續的記憶體區 2. 動態儲存分配過程的記憶體狀態 系統執行一段時間後,有些程式的記憶體被釋放,造成了上圖(b)中的狀態。假如此時又有新

資料結構動態記憶體管理機制

通過前面的學習,介紹很多具體的資料結構的儲存以及遍歷的方式,過程中只是很表面地介紹了資料的儲存,而沒有涉及到更底層的有關的儲存空間的分配與回收,從本節開始將做更深入地介紹。 在使用早期的計算機上編寫程式時,有關資料儲存在什麼位置等這樣的問題都是需要程式設計師自己來給資料分配記憶體。而現在的高階語言,大大的

資料結構隨筆—動態儲存管理

動態儲存管理的基本問題是如何應使用者請求分配記憶體,如何回收那些使用者不在使用而釋放的記憶體。 經過一段時間的執行,有些使用者執行結束,它所佔用的記憶體區變成空閒塊,只就使整個記憶體區呈現出佔用快和空閒塊交錯的狀態,假如此時又有新的使用者進入系統請求分配記憶體,這時通常有兩

數據結構動態順序表(C實現)

int 隊列 destroy element 類型 for str ttr def 線性表有2種,分為順序表和鏈表。 順序表: 采用順序存儲方式,在一組地址連續的存儲空間上存儲數據元素的線性表(長度固定) 鏈表: 有3種,單鏈表、雙向鏈表、循環鏈表(長度不固定)

山東大學軟體學院 《資料結構、演算法與應用 c++語言描述》實驗指導書

實驗要求 採用良好的程式設計風格;關鍵操作要有註釋。 程式能夠執行,顯示執行結果。 二、      開發工具        

資料結構——稀疏矩陣運算器(C語言

資料結構——稀疏矩陣運算器(C語言) /*****************稀疏矩陣運算器 ****************/ #include<stdio.h> #include<stdlib.h> #define OK 1 #define TRUE

資料結構-佇列-順序表實現-C語言

佇列定義 對於一個存取的n個內容,最先進入的最先出去(First In,First Out:FIFO),即稱為佇列. 比如,食堂排隊,最先去的,最先得到飯菜; 關鍵步驟:入隊出隊 程式碼實現 //迴圈佇列 順序表實現 #include <stdio

資料結構動態順序表

動態順序表 動態順序表是跟靜態順序表大體相似,有些地方是不同的,動態順序表是在動態變化中,當我們的所需的記憶體不夠時,它會自動開闢一個我們需要的空間,來供我們使用。 動態順序表與靜態順序表的不同在於初始化/銷燬/所有插入,其他和靜態順序表完全一樣。 定義一個結構體 先將我們需

資料結構順序儲存結構線性表

順序儲存結構:元素按順序連續儲存。 一般它的定義儲存格式為: //定義資料格式 typedef struct { ElemType data[MAXSIZE]; //陣列儲存元素 int length; //線性表的長度 }Sq

資料結構——順序表操作(C語言實現)

//順序表list #include"stdio.h" #define maxsize 15 typedef struct{ int a[maxsize]; int size; }list; //建立 void create(lis

資料結構---迷宮問題題解(C語言

資料結構—迷宮問題題解(C語言) #include<stdio.h> #include<stdlib.h> #define FALSE 0 #define TRUE 1 #define OK 1 #define M 20

關於資料結構單鏈表的C++實現

1、連結串列List的基本單元是節點Node,因此想要操作方便,就必須為每一步打好基礎,Node的基本結構如下: class Node { public: int data; Node *next; Node(int da=0,Node *p=NULL) { t

資料結構-------------線索二叉樹(c語言)

一、線索二叉樹 如果二叉樹的節點包含資料域和兩個指標域( lchild 和 rchild ),當節點沒有下一個節點時,將指標域賦值為空(NULL),但有時會造成很大的浪費,所以可以將空指標域利用起來,存放其他節點的地址,這樣就便於索引,像二叉樹遍歷,查詢之類就會變得相對容易

資料結構—二叉樹(C語言實現)

以下所有內容來自網易雲課堂——資料結構(小甲魚版) 對於樹來說,一旦可以指明他的分支數,那麼就可以用連結串列來實現了 二叉樹是應用廣泛的樹,因為現實世界大部分模型都只包含0,1這兩種情況,非常適合用二叉樹 如下: typedef struct BiNode {

資料結構學習筆記-串(C語言實現)

串由零個或多個字元組成,說白了就是字串。串的儲存方式相對於線性表來講有些不同,他分為以下幾種:順序儲存、堆分配儲存、鏈式儲存。順序儲存通常在陣列中的頭元素存放字串長度。堆分配儲存通常會動態分配空間。鏈式儲存分為兩種,一種是每個節點存放一個字元(比較浪費空間),另一種則是每個節

資料結構:迴圈佇列(C語言實現)

生活中有很多佇列的影子,比如打飯排隊,買火車票排隊問題等,可以說與時間相關的問題,一般都會涉及到佇列問題;從生活中,可以抽象出佇列的概念,佇列就是一個能夠實現“先進先出”的儲存結構。佇列分為鏈式佇列和靜態佇列;靜態佇列一般用陣列來實現,但此時的佇列必須是迴圈佇列,否則

資料結構-二叉樹(binaryTree)-C語言實現

用C語言實現二叉樹基本功能 介面部分 “binaryTree.h” /*二叉樹介面*/ #ifndef _BINARY_TREE_H_ #define _BINARY_TREE_H_ #include <stdbool.h> #def

資料結構---二叉樹C實現

學過資料結構的都知道樹,那麼什麼是樹? 樹(tree)是包含n(n>0)個結點的有窮集,其中: (1)每個元素稱為結點(node); (2)有一個特定的結點被稱為根結點或樹根(root)。 (3)除根結點之外的其餘資料元素被分為m(m≥0)個互不相交的集合

資料結構、演算法與應用C++語言描述(第二版) 第一章部分練習參考答案

1、 void swap(int& x,int& y) {//交換x,y int temp=x; x=y; y=temp; } 2、 template<class T,unsigned N> size_t count(const T (

資料結構—樹的實現(C語言

1、樹的概念    樹形結構是節點之間以及分支關係定義的層次結構。作為一種重要的非線性結構,樹形結構中一個節點最多隻有一個前驅節點,但是可以有多個後繼節點。2、樹的儲存結構    在計算機中,樹有多種的儲存方式,下面介紹一種動態的“左子/右兄”二叉連結串列表示方法。#incl