1.动态内存管理
1.1.C语言中相关知识
栈区 局部变量 函数形参 |
堆区 (自己申请,自己释放) 动态内存开辟 Malloc Free Realloc calloc |
静态区(数据段) 全局变量 静态变量 |
动态开辟空间,2中回收方式
- 主动free
- 程序结束
- 动态内存函数的介绍
- malloc和free
void* malloc(size_t size)
返回类型为开辟空间的地址,大小为开辟的字节数
Malloc向内存申请一块连续可用的空间
开辟失败时会返回空指针,所以返回值一定要检查。
使用后用free函数释放
Free只能释放动态开辟的空间
Malloc和free函数都声明在stdlib.h头文件中
- calloc
void* calloc(size_t num,size_t size);
num 元素的个数
size 每个元素的大小(字节)
初始化 每个元素为0
- realloc
void* realloc(void* ptr,size_t size)
ptr是要调整的内存地址
size是调整之后的新大小
返回值为调整之后的内存起始地址
Realloc有可能找不到合适的空间,来调整大小,这时就返回NULL
- 常见的动态内存错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟内存使用free释放
- 用free释放一块动态开辟内存的一部分
- 对同一块动态开辟的空间多次释放
- 动态开辟的内存空间忘记释放->内存泄漏
1.2.动态内存开辟
#include<stdio.h>
#include<stdlib.h>
//int main()
//{
// //假设开辟10个整形的空间
// //int arr[10];//栈区开辟
//
// //动态内存开辟
// //void* 类型不能直接解引用和++--
// //void* p = malloc(10 * sizeof(int));
// int* p = (int*)malloc(10 * sizeof(int));
// //使用这些空间的时候
// if (p == NULL)
// {
// printf("malloc error");
// //perror("main"); 保错,会返回 main: 错误信息
// return 0;
// }
// //使用
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// *(p + i) = i;
// }
//
// for (i = 0; i < 10; i++)
// {
// printf("%d ", p[i]);//p[i]=*(p+i)
// }
//
// //回收空间
// free(p);
// p = NULL;
//
// return 0;
//}
//int main()
//{
// //int* p = (int*)malloc(40);
// int* p = calloc(10, sizeof(int));
// if (p == NULL)
// {
// return 1;
// }
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// printf("%d\n", *(p + i));
// }
//
// return 0;
//}
//int main()
//{
// int* p = calloc(10, sizeof(int));
// if (p == NULL)
// {
// perror("main");
// return 1;
// }
// //使用
// int i = 0;
// for (i = 0; i < 10; i++)
// {
// *(p + i) = 5;
// }
//
// //这里需要p指定的空间更大,需要20个int空间
// //realloc调整空间
// int* ptr = (int*)realloc(p, 20 * sizeof(int));
// if (ptr != NULL)
// {
// p = ptr;
// }
//
// for (i = 0; i < 10; i++)
// {
// printf("%d\n", *(p + i));
// }
//
// return 0;
//}
int main()
{
int* p = (int*)realloc(NULL, 40);//这里功能类似与malloc,就是直接在堆区开辟40个字节
return 0;
}
1.3.常见的动态内存错误
#include<stdio.h>
#include<stdlib.h>
//1.对NULL指针的解引用操作
//int main()
//{
// int* p=(int*)malloc(10000000000000000);
// //对malloc函数的返回值,做判断
// int i=0;
// if(i=0;i<10;i++)
// {
// *(p+i)=i;
// }
//
// return 0;
//}
//2.对动态开辟空间的越界访问
//int main()
//{
// int* p=(int*)malloc(10*sizeof(int));
// if(p==NULL)
// {
// return 1;
// }
//
// int i=0;
// //越界访问
// for(i=0;i<40;i++)
// {
// *(p+i)=i;
// }
//
// free(p);
// p=NULL;
//
// return 0;
// }
//3.对非动态开辟内存使用free释放
//int main()
//{
// int arr[10]={0};//栈上
// int* p=arr;
//
// free(p);
// p=NULL;
//
// return 0;
//}
//4.用free释放一块动态开辟内存的一部分
//int main()
//{
// int* p=(int*)malloc(10*sizeof(int));
// if(p==NULL)
// {
// return 1;
// }
//
// int i=0;
// for(i=0;i<5;i++)
// {
// //p发生改变
// *p++=i;
// }
//
// //不再是释放所有新开辟的空间
// free(p);
// p=NULL;
//
// return 0;
//}
//5.对同一块动态开辟的空间多次释放
//int main()
//{
// int* p=(int*)malloc(100);
//
// free(p);
// p=NULL;
//
// free(p);
//
// return 0;
//}
//6.动态开辟的内存空间忘记释放
void test()
{
int* p=(int*)malloc(100);
if(p==NULL)
{
return;
}
//使用
}
int main()
{
test();
return 0;
}
1.4.经典笔试题
1.值传递问题
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
//void GetMemory(char* p)
//{
// p=(char*)malloc(100);
//}
//
//void test(void)
//{
// char* str=NULL;
// //值传递,所以GetMemory函数的形参p是str的一份临时拷贝
// //在GetMemory函数内部,动态申请空间的地址存放在p中,不会影响外边str
// //所以当GetMemory函数返回之后,str依然是空指针
// //所以strcpy会失败
//
// //当GetMemory函数返回之后,形参p销毁
// //使得动态开辟的100个字节存在内存泄露,无法释放
// GetMemory(str);
// strcpy(str,"hello world");
// printf(str);
//}
//
//int main()
//{
// test();
//
// return 0;
//}
//改:1
//char* GetMemory(char* p)
//{
// p=(char*)malloc(100);
// return p;
//}
//
//void test(void)
//{
// char* str=NULL;
//
// str=GetMemory(str);
// strcpy(str,"hello world");
// printf(str);
// //char* p="hello world" 传入printf函数的是p(即hello world中的首地址)
// //printf("hello world");
//
// free(str);
// str=NULL;
//}
//
//int main()
//{
// test();
//
// return 0;
//}
//改:2
void GetMemory(char** p)
{
*p=(char*)malloc(100);
}
void test(void)
{
char* str=NULL;
GetMemory(&str);
strcpy(str,"hello world");
printf(str);
free(str);
str=NULL;
}
int main()
{
test();
return 0;
}
2.返回栈空间地址问题
#include"stdio.h"
#include"stdlib.h"
//GetMemory函数内部创建的数组是在栈区创建的
//出了函数,p数组的空间就还给了操作系统
//返回地址没有实际意义
//如果通过返回地址,
char* GetMemory(void)
{
char p[]="hello world";
return p;
}
void test(void)
{
char* str=NULL;
//返回栈空间地址的问题
//栈上开辟,出函数之后销毁,返回指针无意义
str=GetMemory();
printf(str);
}
int main()
{
test();
return 0;
}
3.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char**p,int num)
{
*p=(char*)malloc(num);
}
void test(void)
{
char* str=NULL;
GetMemory(&str,100);
strcpy(str,"hello");
printf(str);
free(str);
str=NULL;
}
int main()
{
test();
return 0;
}
4.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test(void)
{
char* str=(char*)malloc(100);
strcpy(str,"hello");
free(str);
str=NULL;
if(str!=NULL)
{
//非法访问
strcpy(str,"world");
printf(str);
}
}
int main()
{
test();
return 0;
}
2.柔性数组
1.柔性数组C语言相关
- C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员
- 柔性数组的特点
·结构中的柔性数组成员前面必须至少有一个其他成员
·sizeof返回的这种结构大小不包括柔性数组的内存
·包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于 结 构的大小,以适应柔性数组的预期大小
- 优势:
方便内存释放
有利于访问速度
2.柔性数组实现
#include<stdio.h>
#include<stdlib.h>
//柔性数组
//写法1:
//struct S
//{
// int n;
// int arr[];//大小是未知的
//};
//写法2
struct S
{
int n;//4
int arr[0];//大小是未知的
};
int main()
{
//期望arr的大小是10个整形
struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
ps->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//使用
//释放
free(ps);
ps = NULL;
//struct S s = { 0 };
//printf("%d\n", sizeof(s));//4
return 0;
}
3.计算机操作系统内存管理
- 内存管理的概念
- 内存空间的分配与回收
- 内存空间的扩展,从逻辑上对内存进行扩充
- 逻辑地址到物理地址的转换(地址重定位)
绝对装入
编译时产生绝对地址
可重定位装入
装入时将逻辑地址转换为物理地址
动态运行时装入
运行时将逻辑地址转换为物理地址,需设置重定位寄存器
- 内存保护,保证各进程在各自存储空间运行,互不影响
- 覆盖与交换
- 覆盖技术
解决“程序大小超过物理内存总和”的问题
思想:将程序分成多个段
内存中分一个为“固定区”,和若干个“覆盖区”
必须由程序员声明覆盖结构,操作系统自动覆盖
缺点:对用户不透明
覆盖技术只用于早期操作系统,已成为历史
- 交换技术
思想:内存紧张时,可将某些进程换出外存,将外存中已具备运行条件的进程换入内存
暂时换出外存等待的进程状态为挂起状态。
- 连续分配管理方式
连续分配:为用户进程分配的必须是一个连续的内存空间。
- 单一连续分配
内存分为:系统区:存放操作系统相关程序
用户区:存放用户进程相关数据
内存中只能有一道用户程序。
优点:实现简单,无外部碎片,不一定需要内存保护
缺点:只能用于单用户、单任务的操作系统中;有内部碎片;存储器利用率极低
- 固定分区分配
将用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业
固定分区分配分为:
分区大小相等:缺乏灵活性,但适合用于用一台计算机控制多个相同对象的场合
分区大小不等:增加了灵活性,可以满足不同大小的进程需求。
分区说明表(一种数据结构),来实现各个分区的分配与回收。
每个表项对应一个分区,通常按分区大小排列。
每个表项包括对应分区的大小、起始地址、状态(是否已分配)
优点:实现简单,无外部碎片
缺点:
- 当用户程序太大时,可能所有分区都不满足要求
不得不采用覆盖技术
- 会产生内部碎片,内存利用率低
- 动态分区分配
又称可变分区分配,不会预先划分内存分区
在进程装入内存时,根据进程的大小动态地建立分区,使分区大小正好合适。系统分区的大小和数目是可变的。
-
- 记录内存使用情况:
空闲分区表
空闲分区链
-
- 当很多空闲分区都能满足需求时,应选择哪个分区进行分配?
按照动态分区分配算法
-
- 如何进行分区的分配与回收?
假设使用空闲分区表
空闲分区大小比申请空间大:更改此表项的分区大小和起始地址
空闲分区大小等于申请空间:删掉此表项
回收区的后面有一个相邻的空闲分区:
更改此空闲分区的分区大小和起始地址
回收区的前面有一个相邻的空闲分区:
更改此空闲分区的分区大小和起始地址
回收区前后都有相邻空闲分区:
三个分区进行合并,更改内存大小和首地址
回收区前后都没有相邻空闲分区:
增加表项
-
- 各表项的排序依据动态分区分配算法确定
内部碎片:分配给某进程的内存区域中,有些部分没用上
外部碎片:指内存中某些空闲分区太小而难以利用
可以通过紧凑技术解决外部碎片
- 动态分区分配算法
- 首次适应算法
思想:每次从低地址开始找,找到第一个能满足大小的空闲分区
实现:空闲分区以地址递增的次序排列,每次分配内存顺序查找空闲分区链。
- 最佳适应算法
思想:优先使用更小得空闲区
实现:空闲分区按容量递增次序链接,每次分配内存顺序查找空闲分区链。
缺点:每次都选最小得分区进行分配,会留下越来越多的很小的、难以利用的内存块。会产生很多外部碎片。
- 最坏适应算法
思想:优先使用最大的连续空闲区
实现:空闲分区按容量递减次序链接
缺点:之后有大进程,没有内存分区可用
- 邻近适应算法
思想:解决首次适应算法每次从头查找的开销,所以采用每次从上次查找结束的位置开始检索
实现:空闲分区以地址递增的顺序存储(可排成循环链表)。每次分配内存时从上次查找结束的位置开始查找空闲分区链
综合来看,首次适应算法的效果更好
4.数据结构中的动态存储管理
一、概述
1、占用块
占用块:已分配给用户使用的地址连续的内存区 可利用空间块:未曾分配的地址连续的内存区
2、动态存储分配过程的内存状态
二、可利用空间表及分配方法
可利用空间表也称为存储池。
1、可利用空间表的三种不同结构形式
系统运行期间所有用户请求分配的存储量大小相同。对此类系统,在系统开始运行时将它使用的内存区按所需大小分割成若干大小相同的块,然后用指针链接成一个可利用空间表。因为表中节点的大小相同,则分配时无需查找,只要将表中的第一个节点分配给用户即可;同样,当用户释放内存时,系统只要将用户释放的空闲块插入表头即可。
第二种情况,系统运行期间用户请求分配的存储量有若干种大小的规格。对此类系统,一般情况下建立若干个可利用空间表,同一链表中的节点大小相同。此时的分配和回收在很大程度上和第一种情况类似,只是当节点大小和请求分配的内存大小相同的链表为空时,需要查询节点较大的链表,并从中取出一个节点,将其中一部分内存分配给用户,而将剩余部分插入到相应大小的链表中。回收和第一种情况相同。然而,这种情况的系统还有一个特殊的问题要处理:即当节点与请求相符的链表和节点更大的链表均为空时,分配不能进行,而实际上内存空间并不一定不存在所需大小的连续空间,只是由于在系统运行过程中,频繁出现小块的分配和回收,导致大街店链表中的空闲块被分隔成小块后插入在小节点的链表中,此时若要系统继续运行,就必须重新组织内存,即执行“存储紧缩”的操作。
第三种情况,系统在运行期间分配给用户的内存块的大小不固定,可以随请求而变。因此,可利用空间表中的节点即空闲块的大小也是随意的。
2、可利用空间表的分配策略
在选择时需要考虑:用户的逻辑要求、请求分配量的大小分布、分配和释放的频率以及效率对系统的重要性等
在回收空闲块时,首先应检查地址与它相邻的内存是否是空闲块
首次拟合法:从表头指针开始查找可利用空间表,将找到的第一个大小不小于的空闲块的一部分分配给用户。在回收时,只要将释放的空闲块插入在链表的表头即可
最佳拟合法:将可利用空间表中一个不小于n且最近n的空闲块的一部分分配给用户。在回收时,必须将释放的空闲块插入到合适的位置上去
最差拟合法:将可利用空闲表中不小于n且是链表中最大的空闲块的一部分分配给用户。在回收时将释放的空闲块插入到链表的适当位置上去
三、边界标识法
1、思想
在每个内存区的头部和底部两个边界上分别设有标识,以表示该区域为占用块或空闲块,使得在回收用户释放的空闲块时易于判别在物理位置上与其相邻的内存区域是否为空闲块,以便将所有地址连续的空闲存储区组合成一个尽可能大的空闲块
2、分配算法
用首次拟合法进行分配时,只要从表头指针pav所指结点起,在可利用空间表中进行查找,找到第一个容量不小于请求分配的存储量(n)的空闲块时,即可进行分配
3、回收算法
为了使物理地址毗邻的空闲块结合成一个尽可能大的结点,则首先需要检查刚释放的占用块的左、右紧邻是否为空闲块。若释放块的左、右邻区均为占领块,则将此新的空闲块作为一个结点插入到可利用空闲表中即可;若只有左邻区是空闲块,则应与左邻区合并成一个结点;若只有右邻区是空闲块,则应与右邻区合并成一个结点;若左、右邻区都是空闲块,则应将3块合起来成为一个结点留在可利用空间表中
四、伙伴系统
1、定义
在用户提出申请时,分配一块大小“恰当”的内存区给用户,反之,在用户释放内存区时即回收。所不同的是:在伙伴系统中无论是占用块或是空闲块,其大小均为2的k次幂(k为某个正整数),伙伴系统优点是算法简单,速度快,缺点是由于只归并伙伴而容易产生碎片
2、分配算法
当用户提出大小为n的内存请求时,首先在可利用表上寻找结点大小与n相匹配的子表,若此子表为非空,则将子表中任意一个节点分配之即可;若此子表为空,则须从结点更大的非空子表中去查找,直至找到一个空闲块,则将其中一部分分配给用户,而将剩余剩余部分插入到相应的子表中
3、回收算法
在用户释放不再使用的占用块时,系统需将这新的空闲块插入到可利用可利用空闲表中去,在回收空闲块时,应首先判别其伙伴是否是空闲块,若否,则只要将释放的空闲块简单插入在相应的子表中即可;若是,则需在相应子表中找到其伙伴并删除之,然后再判别合并后的空闲块的伙伴是否为空闲块。依次重复,直到归并所得空闲块的伙伴不是空闲块时,再插入到相应的子表中去
五、无用单元收集
1、定义
指那些用户不再使用而系统没有回收的结构和变量,特点是在用户释放存储时进行回收,即系统是应用户的需求来进行存储分配和回收的
如果所释放的结点被再分配而继续访问指针q所指结点,则这种访问为“悬挂访问”
2、解决无用单元方法
(1)、使用访问计数器
(2)、收集无用单元
对所有占用结点加上标志,在无用单元收集之前所有结点的标志域均置为“0”,则加上标志就是将结点的标志域置为“1”
对整个可利用存储空间顺序扫描一遍,将所有标志域为“0”的结点链接成一个新的可利用空间表
六、存储紧缩
1、实现办法
设立一个指针,称为堆指针,始终指向堆的最低(或最高)地址,当用户申请N个单位的存储块时,堆指针向高地址(或低地址)移动N个存储单位,而移动之前的堆指针的值就是分配给用户的占用块的初始地址。由于系统的可利用空间始终是一个地址连续的存储块,因此回收时必须将所释放的空闲块合并到整个堆上去才能重新使用,这就是“存储紧缩”的任务
2、存储紧缩的操作
计算占用块的新地址
修改用户的初始变量表,以便在存储紧缩后用户程序能继续正常运行
检查每个占用块中存储的数据。若有指向其他存储块的指针,则需做相应修改
将所有占用块迁移到新地址去,实质是作传送数据的工作
5.边界标识法实现动态存储管理
#pragma once
//采用首次拟合法 思想:每次从低地址开始找,找到第一个能满足大小的空闲分区
// 实现:空闲分区以地址递增的次序排列,每次分配内存顺序查找空闲分区链。
//使用不带头的双向循环列表
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#ifndef BOUNDARY_H_ //如果标识未定义,则执行下一句
#define BOUNDARY_H_
#endif
#define FootLoc(p) (p+p->size-1) //计算块尾部地址(相当于开辟空闲空间的尾地址)
#define MEM_SIZE 1024 //字的个数
#define EPISION 10 //整块分配的临界阙值
//构建双向循环链表
//每个节点有两个边界 head和foot
typedef struct Word
{
union
{
struct Word* prev; //前驱 前一个的地址
struct Word* uplink; //指向当前内存块的地址 也是开辟的空闲空间的起始地址 foot中指向本节点头部
};
int tag; //0表示空闲,1表示占据 空间中每个地址都有对应的tag
int size; //空闲内存块的大小,包含头和尾
struct Word* next; //后继 后一个的地址
}Word,Head,Foot,*Space;//四个区,space代表空闲块
//先创建内存池,用于模拟内存的分配与回收
//*pav为可利用空间表的头指针
//申请内存池
Space CreateMem();
//内存分配
Space MemAlloc(Space* pav, int n);
//打印占据块的信息
void ShowUsed(Space* pav, int len);
//打印空闲块信息
void ShowSpace(Space pav);
void Destroy(Space s);
//释放某个占用块
void Free(Space* pav, Space p);
#include"Boundary.h"
//对内存块操作,必须要记得改尾部的空闲状态以及指向
//申请内存池
Space CreateMem()
{
Word* p = (Word*)malloc(MEM_SIZE * sizeof(Word));
assert(p!=NULL);
//双向循环链表初始化
p->prev = p; //指向前一个内存块地址
p->next = 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 (*pav == NULL)
{
return NULL;
}
//分配前先在空闲链表中找到合适的空闲块
Word* pTemp = *pav;
do
{
if (pTemp->size >= n)
{
break;
}
pTemp = pTemp->next;
} while (pTemp != *pav); //只算一圈,如果再次相等就是又回到原点
//没找到合适的内存块
if (pTemp->size < n)
{
printf("没找到合适的内存块");
return NULL;
}
//找到合适内存块
else
{
if (pTemp->size - n <= EPISION)//整块分配 剩余空闲空间小于整块分配的临界阙值
{
if (pTemp->next == *pav) //说明此时只有一个空闲块节点,此时整块分配以后链表为空
{
*pav = NULL;//*pav为表头指针,表为空,则它也置空
}
else
{
//此时将这个节点分配以后链表中还有其他空闲节点,所以要删除这个节点
pTemp->prev->next = pTemp->next;
pTemp->next->prev = pTemp->prev;
}
pTemp->tag = 1;
FootLoc(pTemp)->tag = 1;
return pTemp; //返回分配出的空间首地址
}
else
{
//只分出高地址的部分内存,将数据存在内存块后部
//相当于将一个内存块分为两个,一个为空闲,一个被占据
pTemp->size -= n;//空闲内存块的大小为原来的减去n
FootLoc(pTemp)->tag = 0; //空闲块尾地址为空闲状态
FootLoc(pTemp)->uplink = pTemp; //本节点头部为pTem
Word* pTail = FootLoc(pTemp) + 1;//被占据的内存块的头的地址
pTail->tag = 1;//标识为占据状态
pTail->size = n;//此内存块大小为 n
FootLoc(pTail)->tag = 1;//此内存块尾部也为占据状态
FootLoc(pTail)->uplink = pTail;//本头节点为pTail
return pTail; //返回分配出的空间首地址
}
}
return NULL;
}
//打印占据块的信息
void ShowUsed(Space* pav, int len)
{
printf("打印占据块的信息\n");
for (int i = 0; i < len; i++)
{
if (pav[i] != NULL)
{ //此块的后一个地址
printf("起始地址:%d,内存块大小:%d,结束地址:%d\n", pav[i], pav[i]->size, FootLoc(pav[i]) + 1);
}
}
}
//打印空闲块信息
void ShowSpace(Space pav)
{
if (pav == NULL)
{
return;
}
Space pTemp = pav;
printf("空闲块信息\n");
do
{
printf("起始地址:%d,内存块大小:%d,结束地址:%d\n", pTemp, pTemp->size, FootLoc(pTemp));
} while (pTemp != pav);
}
//释放某个占用块
void Free(Space* pav, Space p)
{
if (*pav == NULL) //链表为空,此时没有空闲块
{
p->tag = 0; //
p->next = p;
p->prev = p;
FootLoc(p)->tag = 0;
FootLoc(p)->uplink = p;
*pav = p; //p空闲块已置空,加入链表
return;
}
Space left;
Space right;
//左右均不为空(物理存储),直接将p所指的内存插再pav前
if ((p - 1)->tag != 0 && (FootLoc(p) + 1)->tag != 0)
{
p->tag = 0;
FootLoc(p)->tag = 0;
FootLoc(p)->uplink = p;
p->next = *pav;
(*pav)->prev = p;
p->prev = (*pav)->prev;
*pav = p;
}
else if ((p - 1)->tag == 0 && (FootLoc(p) + 1)->tag != 0)
{
//左空右不空,直接将p接到左边内存块后面
left = (p - 1)->uplink;//p-1为左边内存块的尾地址,left现在为左内存块的首地址
left->size += p->size;
FootLoc(p)->tag = 0;//将并进去的p的尾部标记为空闲
FootLoc(p)->uplink = left;
}
else if ((p - 1)->tag != 0 && (FootLoc(p) + 1)->tag == 0)
{
//左不为空右为空
left = (*pav)->prev;//头指针的前一个
right = FootLoc(p) + 1;//物理右侧内存块的地址
//将p插到pav前面
p->tag = 0;
left->next = p;
p->prev = left;
p->next = (*pav);
(*pav)->prev = p;
//将物理右侧内存块从链表中删除
right->next->prev = right->prev;
right->prev->next = right->next;
//合并
p->size += right->size;
FootLoc(right)->uplink = p;
FootLoc(p)->tag = 0;
*pav = p;
}
else
{
//((p - 1)->tag == 0 && (FootLoc(p) + 1)->tag == 0)
//左右都为空
left = (p - 1)->uplink;//物理左
right = FootLoc(p) + 1;//右边内存块首地址 物理右
//先将右内存块从表中删除
right->next->prev = right->prev;
right->prev->next = right->next;
//合并三个内存块
left->size += p->size + right->size;
FootLoc(right)->uplink = left;//右内存块的尾指向本内存块的头,此时为left
*pav = left;//left一直在链表中,合并之后依旧还在
}
}
void Destroy(Space s)
{
}
#include"Boundary.h"
int main()
{
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);//整块分配
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[2]);
UsedMem[2] = 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[4]);
UsedMem[4] = NULL;
ShowSpace(pav);
ShowUsed(UsedMem, sizeof(UsedMem) / sizeof(*UsedMem));
printf("-------------------------------------------------------------------------------------------------\n");
return 0;
}
6.伙伴系统
#pragma once
#define M 16
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<math.h>
#include<stdbool.h>
//空闲内存块
//节点
//双向链表
//可利用空间表
typedef struct Word
{
struct Word* prev;//前一个节点的地址
int tag; //0为空闲,1为占用
int kval;//k值,内存空间的大小2^k
struct Word* next;//后一个节点的地址
}Word,* Space;
//顺序表
typedef struct HeadNode
{
int nodesize;//每个内存空间的大小 第0组 块的大小都为2^0
Word* first;//内存空间的指针
Word* start;//记录内存池的首地址
}FreeList[M+1];
Space CreateMem();//创建内存池
void InitSqlist(FreeList* avail, Space p); //初始化顺序表
static void Delete(FreeList* avail, Space p);//从表中删除对应内存单元
Word* AllocBuddy(FreeList* avail, int n);//分配内存单元
static Word* RorL(FreeList* avail, Space p);//判断要回收的内存块是左块还是右块
static void Insert(FreeList* avail, Space p);//根据内存单元变化,更改顺序表的内容
bool IsEmpty(FreeList* avail, Space p);//判断顺序表是否为空
static void Free_mask(FreeList* avail, Space* p);//进行一次回收合并
void Free(FreeList* avail, Space p); //回收内存
#include"buddy.h"
//创建内存池
//Word* start;
Space CreateMem()
{
//pow(x,y)计算x的y次方
//物理连续的大小固定段
Word* pmem = (Word*)malloc((int)pow(2, M) * sizeof(Word));//创建内存池
assert(pmem != NULL);
pmem->prev = pmem;
pmem->next = pmem;
pmem->tag = 0;
pmem->kval = M;
//start = pmem;//记录内存池开始的地址
return pmem;
}
//初始化顺序表
void InitSqlist(FreeList* avail, Space p)
{
avail[0]->start = p;//记录内存池开始的地址
for (int i = 0; i <= M; i++)
{
avail[i]->first = NULL;//顺序表组存储内存空间都置为空指针
avail[i]->nodesize = 0;//内存空间大小为0
}
int k = p->kval; //2的幂次
avail[k]->first = p;//第K组 所存地址为p
avail[k]->nodesize = (int)pow(2, k);//第K组 每个内存块(节点)的内存大小为2的K次幂
}
//表中删除对应内存单元p
static void Delete(FreeList* avail, Space p)
{
Word* front = p->prev;
Word* rear = p->next;
if (p == rear)//占用/合并后,链表中只有p一个节点,对应顺序表内无其他内存单元,表此格置空
{
avail[p->kval]->first = NULL;
}
else
{
//占用/合并后,链表内有其他内存单元,从链表中删除p
front->prev = rear;
rear->prev = front;
//顺序表所存地址改为p的下一个
avail[p->kval]->first = rear;
//将p从链表拿出,它是被占用块
p->next = p;
p->prev = p;
}
}
//分配内存单元,将剩余空间分裂
Word* AllocBuddy(FreeList* avail, int n)
{
Word* p = NULL;
//顺序表中内存块大小随序号递增,2^0,2^1,2^2……
for (int j = 0; j <= M; ++j)//顺序表一共有M+1个格
{
//if (j > M)
//{
// return NULL;//?
//}
//顺序表第J组中 每个内存块大小满足 内存匹配
if (avail[j]->nodesize >= n && avail[j]->first != NULL)
{
p = avail[j]->first;
Delete(avail, p);//将被占用块从表中删除(两个表都得变)
int i = 0;
//只有一个内存块空闲,链表中只有p一个节点,移出后,链表为空,顺序表记录地址为空
//可能出现此内存块很大的情况
//将此内存块分为2份,4份……
if (p->kval > 0 && avail[j]->first == NULL)
{
//找到更小的内存块去分配
//遍历,找到最小的j-i,使2的j-i次方大于等于n
while ((int)pow(2, j - i) >= n)
{
Word* pi = p + (int)pow(2, j - i); //将内存块分为2^i份之后,后面的内存块的首地址
//构建新的链表
pi->next = pi;
pi->prev = pi;
//pi节点为空闲
pi->tag = 0;
//pi节点空间大小为p的一半的几次方
//将p的空间撕裂
pi->kval = j - i;
//对应顺序表第j-i组
avail[j - i]->first = pi;
avail[j - i]->nodesize = (int)pow(2, pi->kval);
i++;
}
}
p->next = p;
p->prev = p;
p->tag = 1;
p->kval = j - (--i);//将大空间分裂,找到更小的内存块去分配
break;
}
}
return p;
}
//判断要回收的内存块是左块还是右块
static Word* RorL(FreeList* avail, Space p)
{
int tmp = p - avail[0]->start;//计算p的相对位置 相对 内存池开始的地址 的位置
//在内存池中,先存内存大的,再存内存小的
//p的下一组内存块大小为(int)pow(2,p->kval+1)
//取余如果为0,就说明这一组,p是第一个,p的地址低
int flg = tmp % (int)pow(2, p->kval + 1);
if (flg != 0)//为右块,地址高
{
return p - (int)pow(2, p->kval); //返回前一个内存块的地址,即返回伙伴(和此内存块大小相等的内存块)的地址
}
else//为左块,地址低
{
return p + (int)pow(2, p->kval); //返回后一个内存块的地址,即返回伙伴(和此内存块大小相等的内存块)的地址
}
}
//根据内存单元变化,更改顺序表的内容
static void Insert(FreeList* avail, Space p)
{
if (avail[p->kval]->first == NULL)//对应在顺序表的组为空
{
//直接插入
avail[p->kval]->first = p;
p->tag = 0;
}
else
{
//不为空,插入在对应组指向的内存块的前面
Word* q1 = avail[p->kval]->first;
p->prev = q1->prev;
q1->prev->next = p;
q1->prev = p;
p->next = q1;
p->tag = 0;
}
}
//判断顺序表是否为空
bool IsEmpty(FreeList* avail, Space p)
{
for (int i = 0; i <= M; i++)
{
if (avail[i]->first != NULL)
{
return false;
}
}
return true;
}
//进行一次回收合并
static void Free_mask(FreeList* avail, Space* p)//此时p为二级指针 其余时是一级指针
{
Space pi = RorL(avail, *p);//pi为p的伙伴的地址
if (pi->tag == 1)//伙伴不空闲
{
Insert(avail, *p);//直接根据*p该变表状态,不考虑伙伴
}
else if (pi->tag == 0 && pi->kval != (*p)->kval)//伙伴不完整,为分割后的伙伴
{
Insert(avail, *p);//直接根据*p该变表状态,不考虑伙伴
}
else//伙伴空闲
{
//合并
Delete(avail, pi);//从对应表删除
(*p)->tag = 0;
if (*p > pi)
{
*p = pi;//让p每次都为左块
}
(*p)->kval++; //p和pi合并,K值+1
}
}
//回收内存
void Free(FreeList* avail, Space p)
{
//顺序表不为空
if (!IsEmpty(avail, p))
{
p->tag = 0;
Space q = p;
int k = p->kval;
while (p->tag == 0 && p->kval == k)//内存块空闲且完整,进行合并
{
Free_mask(avail, &p);
k++;//向大块内存合并
}
if (p->kval == M - 1)//如果合并后的大小为内存池的一半时且不能继续合并,那么表中空闲空间只有此内存块
{
//相当于内存池变为原来的一半
q->kval--;//?
avail[q->kval]->first = NULL;
q = q - (int)pow(2, q->kval);//最大内存块地址,即内存池首地址
avail[M]->first = q;//顺序表最后一组存内存池首地址
//链表
q->next = q;
q->prev = q;
q->kval = M;
q->tag = 0;
}
}
else//顺序表为空,直接插入
{
avail[p->kval]->first = p;
p->tag = 0;
}
}
#include"buddy.h"
int main()
{
return 0;
}