详解malloc,calloc,realloc原理及其模拟实现

malloc原理

malloc它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足 用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传 给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片 段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检 查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
查询链表的方法:

break指针

Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。

First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块。首次适配有更好的运行效率。
Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。最佳适配具有较高的内存使用率。

int brk(void *addr);
void *sbrk(intptr_t increment);

brk将break指针直接设置为某个地址;
而sbrk将break从当前位置移动increment所指定的增量,如果将increment设置为0,则可以获得当前break的地址。

malloc实现:

void* malloc(unsigned size); 在堆内存中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度。

#include <sys/types.h>
#include <unistd.h>typedef struct s_block *t_block;struct s_block {size_t size;          // 数据区大小 t_block next;         // 指向下个块的指针 int free;             // 是否是空闲块 int padding;          // 填充4字节,保证meta块长度为8的倍数 char data[1]          // 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta 
};//首次适配
t_block find_block(t_block *last, size_t size) {t_block b = first_block;while(b && !(b->free && b->size >= size)) {*last = b;b = b->next;}return b;
}//如果现有block都不能满足size的要求,
//则需要在链表最后开辟一个新的block。
//这里关键是如何只使用sbrk创建一个struct#define BLOCK_SIZE 24 //由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 t_block extend_heap(t_block last, size_t s) {t_block b;b = sbrk(0);if(sbrk(BLOCK_SIZE + s) == (void *)-1)return NULL;b->size = s;b->next = NULL;if(last)last->next = b;b->free = 0;return b;
}//First fit有一个比较致命的缺点,
//就是可能会让很小的size占据很大的一块block,
//此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,void split_block(t_block b, size_t s) {t_block newb;newb = b->data + s;newb->size = b->size - s - BLOCK_SIZE ;newb->next = b->next;newb->free = 1;b->size = s;b->next = newb;
}//由于我们希望malloc分配的数据区是按8字节对齐,
//所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:
size_t align8(size_t s) {if(s & 0x7 == 0)return s;return ((s >> 3) + 1) << 3;
}void *first_block=NULL;void *malloc(size_t size) {t_block b, last;size_t s;/* 对齐地址 */s = align8(size);if(first_block) {/* 查找合适的block */last = first_block;b = find_block(&last, s);if(b) {<pre name="code" class="cpp">         /* 如果可以,则分裂 */if ((b->size - s) >= ( BLOCK_SIZE + 8))split_block(b, s);b->free = 0;} else {/* 没有合适的block,开辟一个新的 */b = extend_heap(last, s);if(!b)return NULL;}} else {b = extend_heap(NULL, s);if(!b)return NULL;first_block = b;}return b->data;
}

calloc实现:

void* calloc(size_t numElements, size_t sizeOfElement);
与malloc相似,参数sizeOfElement为单位元素长度(例如:sizeof(int)),numElements为元素个数,即在内存中申请numElements * sizeOfElement字节大小的连续内存空间。并且会把内存初始化为0。

calloc(num, size) 基本上等于 void *p = malloc(num * size); memset(p, 0, num * size); 但理论上 calloc 的实现可避免 num * size 溢出,当溢出时返回 NULL 代表失败,而 malloc(num * size) 可能会分配了一个尺寸溢出后的内存。

由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。

void *calloc(size_t number, size_t size) {size_t *news;size_t s8, i;news = malloc(number * size);if(news) {s8 = align8(number * size) >> 3;for(i = 0; i < s8; i++)news[i] = 0;}return news;
}

realloc实现:

void* realloc(void* ptr, unsigned newsize);

使用realloc函数为ptr重新分配大小为size的一块内存空间。下面是这个函数的工作流程:

  1. 对ptr进行判断,如果ptr为NULL,则函数相当于malloc(new_size),试着分配一块大小为new_size的内存,如果成功将地址返回,否则返回NULL。如果ptr不为NULL,则进入2。
  2. 查看ptr是不是在堆中,如果不是的话会抛出realloc invalid pointer异常。如果ptr在堆中,则查看new_size大小,如果new_size大小为0,则相当于free(ptr),将ptr指向的内存空间释放掉,返回NULL。如果new_size小于原大小,则ptr中的数据可能会丢失,只有new_size大小的数据会保存;如果size等于原大小,等于什么都没有做;如果size大于原大小,则查看ptr指向的位置还有没有足够的连续内存空间,如果有的话,分配更多的空间,返回的地址和ptr相同,如果没有的话,在更大的空间内查找,如果找到size大小的空间,将旧的内容拷贝到新的内存中,把旧的内存释放掉,则返回新地址,否则返回NULL。
//为了实现realloc,我们首先要实现一个内存复制方法。
//如同calloc一样,为了效率,我们以8字节为单位进行复制
void copy_block(t_block src, t_block dst) {size_t *sdata, *ddata;size_t i;sdata = src->ptr;ddata = dst->ptr;for(i = 0; (i * 8) < src->size && (i * 8) < dst->size; i++)ddata[i] = sdata[i];
}
void *realloc(void *p, size_t size)
{size_t s;t_block b, newb;void *newp;if (!p)/* 根据标准库文档,当p传入NULL时,相当于调用malloc */return malloc(size);if(valid_addr(p)){s = align8(size);b = get_block(p);if(b->size >= s){if(b->size - s >= (BLOCK_SIZE + 8))split_block(b,s);} else{/* 看是否可进行合并 */if(b->next && b->next->free&& (b->size + BLOCK_SIZE + b->next->size) >= s){fusion(b);if(b->size - s >= (BLOCK_SIZE + 8))split_block(b, s);}else {/* 新malloc */newp = malloc (s);if (!newp)return NULL;newb = get_block(newp);copy_block(b, new);free(p);return(newp);}}return (p);}return NULL;
}

free实现:

  1. 如何验证所传入的地址是有效地址,即确实是通过malloc方式分配的数据区首地址
    地址应该在之前malloc所分配的区域内,即在first_block和当前break指针范围内;
    这个地址确实是之前通过我们自己的malloc分配的。
  2. 如何解决碎片问题
//首先我们在结构体中增加magic pointer(同时要修改BLOCK_SIZE)
typedef struct s_block *t_block;struct s_block {size_t size;          // 数据区大小 t_block next;         // 指向下个块的指针 int free;             // 是否是空闲块 int padding;          // 填充4字节,保证meta块长度为8的倍数 char data[1]          // 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入metavoid *ptr;            // Magic pointer,指向data  
};
#define BLOCK_SIZE 24 //我们定义检查地址合法性的函数:
t_block get_block(void *p) 
{char *tmp;tmp = p;return (p = tmp -= BLOCK_SIZE);
}int valid_addr(void *p) 
{if(first_block) {if(p > first_block && p < sbrk(0)) {return p == (get_block(p))->ptr;}}return 0;
}

将block和相邻block合并。为了满足这个实现,需要将s_block改为双向链表。修改后的block结构如下:

 typedef struct s_block *t_block;
struct s_block {size_t size; /* 数据区大小 */t_block prev; /* 指向上个块的指针 */t_block next; /* 指向下个块的指针 */int free; /* 是否是空闲块 */int padding; /* 填充4字节,保证meta块长度为8的倍数 */void *ptr; /* Magic pointer,指向data */char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
#define BLOCK_SIZE 28

合并方法如下:

t_block fusion(t_block b) {if (b->next && b->next->free) {b->size += BLOCK_SIZE + b->next->size;b->next = b->next->next;if(b->next)b->next->prev = b;}return b;
}
void free(void *p) 
{t_block b;if(valid_addr(p)) {b = get_block(p);b->free = 1;if(b->prev && b->prev->free)b = fusion(b->prev);if(b->next)fusion(b);else {if(b->prev)b->prev->prev = NULL;elsefirst_block = NULL;brk(b);}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383139.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c++动态内存管理题目

malloc/free和new/delete的区别 malloc/free和new/delete的共同点是&#xff1a;都是从堆上申请空间&#xff0c;并且需要用户手动释放。不同的地方是&#xff1a; malloc和free是函数&#xff0c;new和delete是操作符malloc申请的空间不会初始化&#xff0c;new可以初始化ma…

私人博客定制----封装数据库接口

封装MySQLAPI 我们先把初始化句柄和断开句柄进行一个封装 static MYSQL* MySQLInit(){ //1.初始化一个Mysql句柄建立连接 MYSQL* connect_fd mysql_init(NULL); //2.和数据库建立连接 if (mysql_real_connect(connect_fd, "127.0.0.1", "root&quo…

私人博客定制---服务器接口封装

实现服务器接口 我们用一个http服务器作为底层&#xff0c;但是c中并没有先成的http服务器&#xff0c;所以我在GitHub上找到一个牛人写的http服务器&#xff0c;拿来直接用&#xff0c;节省本项目开发的时间 这是服务器的链接地址 上面有详细的使用方法&#xff0c;本文就不再…

私人博客定制

项目背景 可行性方面 需求分析&#xff1a; 详细设计&#xff1a; 数据库设计 博客管理API的设计 标签相关API 服务器端的实现 对数据库操作进行封装 对服务器操作进行封装 客户端实现 具体操作 使用markdown 具体实现 测试 项目效果展示 维护 完整代码 项目…

初识c++中的函数模板

函数模板 函数模板概念 函数模板:编译器生成代码的一个规则。函数模板代表了一个函数家族&#xff0c;该函数模板与类型无关&#xff0c;在使用时被参数化&#xff0c;根据实参类型产生函数的特定类型版本。 函数模板格式 //要让这个函数与类型无关 //Add函数模板 template…

深入理解c++中的函数模板

非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当成常量来使…

c++中的IO流

c语言中的IO操作 标准类型的输入输出: 输入------>数据来源是通过键盘进行输入输出------>程序中的数据输出到控制台 c语言中: scanf:输入 printf:输出 两个函数的相同点 1 —格式串 2 —不定参数 两个函数的缺陷 1 —用户要提供数据的格式—用户要记忆大量的格式串—…

201301 JAVA2~3级---走格子

请编写一个函数&#xff08;允许增加子函数&#xff09;&#xff0c;计算n x m的棋盘格子&#xff08;n为横向的格子数&#xff0c;m为竖向的格子数&#xff09;沿着各自边缘线从左上角走到右下角&#xff0c;总共有多少种走法&#xff0c;要求不能走回头路&#xff0c;即&…

复习Linux基本操作----常见指令

Linux基本操作 ls命令 ls(list):相当于windows上的文件资源管理器 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 常用选项&#xff1a; -a 列出目…

复习Linux基础操作---权限操作

shell命令以及运行原理 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;“ &#xff0c;但我们一般用户&#xff0c;不能直接使用kernel。而是通过kernel的“外壳”程序&#xff0c;也就是所谓的shell&#xff0c;来与kernel沟…

【剑指offer】_01 (二维数组中的查找)

题目描述 在一个二维数组中&#xff08;每个一维数组的长度相同&#xff09;&#xff0c;每一行都按照从左到右递增的顺序排序&#xff0c;每一列都按照从上到下递增的顺序排序。请完成一个函数&#xff0c;输入这样的一个二维数组和一个整数&#xff0c;判断数组中是否含有该…

【剑指offer】_02替换空格

题目描述 请实现一个函数&#xff0c;将一个字符串中的每个空格替换成“%20”。例如&#xff0c;当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 解题思路 首先我们先算出整个字符串的长度&#xff0c;还有总共多少个空格。因为空格只占一个字节&#…

【剑指offer】_04 重建二叉树

题目描述 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}&#xff0c;则重建二叉树并返回。 解题思路 用前序的顺序…

再谈c++中的多态

何为多态 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同的状态。 多态的实现 在继承的体系下 基类中必须有虚函数(被virtual关键字修饰的成员函数)&#xff0c;在派生类中必须…

再谈c++中的继承

继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象程序设计的层次结构&#xff0c;体现了…

红黑树概念及其相关操作的实现

红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但它并不像AVL树一样&#xff0c;每个结点绑定一个平衡因子。但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过 对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c…

模拟实现STL中map和set容器

红黑树的迭代器 //红黑树的迭代器 template<class T> struct RBTreeIterator {typedef RBTreeNode<T>Node;typedef RBTreeIterator<T> Self; public:RBTreeIterator(Node* pNode nullptr):_pNode(pNode){}//具有指针操作T& operator*(){return _pNode-…

【剑指offer】_05 连续子数组最大和

题目描述 HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢&#…

排序上---(排序概念,常见排序算法,直接插入,希尔排序,直接选择排序,堆排序)

排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&…

排序下---(冒泡排序,快速排序,快速排序优化,快速排序非递归,归并排序,计数排序)

排序上 排序上 交换类排序 基本思想&#xff1a;所谓交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置&#xff0c;交换排序的特点是&#xff1a;将键值较大的记录向序列的尾部移动&#xff0c;键值较小的记录向序列的前部移动。 冒泡…