五十行代码教你写一个简单的内存池(二级指针的应用)

简介: 五十行代码教你写一个简单的内存池(二级指针的应用)

固定大小内存的内存池实现

该内存池是一个很简单的内存池,只能申请固定大小的内存,仅供学习

要点:

  • 构造隐式链表
  • 二级指针

存储结构

typedef struct mempool_s{
    int block_size;  // 每一块的大虚哎
    int free_count;  // 剩余有多少块是可以用的
    char *free_ptr;  // 可以分配的开始位置
    char *mem;       // 指向4k的起始位置
}mempool_t;

开放的API

  • int memp_init(mempool_t * m, int block_size)
    对内存池每一项都赋值,用malloc申请大块内存,让mem指向malloc
    这里面有特殊操作:构建隐式链表
  • void* memp_alloc(mempool_t* m)
    free_ptr链开始分配
  • void* memp_free(mempool_t* m, void *ptr)
    释放给定的内存块,并将它头插在隐式链表中

构建隐式链表

我们会用malloc分配一个4k大小的内存,mem指向这块起始地址,free_ptr指向可以分配的内存的起始地址

每次从内存池中分配一个32字节大小的内存块,于是4k内存就分分割为下图所示

现在就有一些问题

  • 如何管理些块块内存?
  • 分配时分配哪一个块?
  • 释放了的块怎么回收?

上述这些问题全部都可以用隐式链表来解决

构造隐式链表的思路:

每次我们申请的是32个字节,我们可以将前面8个字节拿来存储next指针,也就是下一个块的地址,这样我们的内存就变成了这样

这样的话我们就构建了一个隐式链表,每次分配就从头部取,每次挥手就将32B的块头插进这个链表

如何实现

利用二级指针可以很方便的实现隐式链表的构造。

二级指针是一个指向指针的指针

操作

假设有下述4K内存

char * free_ptr = malloc(1024);

每个块的大小为32用变量block_size表示

int block_size = 32;

一共有1024/32个块用free_count表示

int free_count = 1024 / block_size;

使用二级指针

int i = 0;
char *ptr = m->free_ptr;
for(i = 0; i < m->free_count; i++){
    *(char**)ptr = ptr + block_size; 
    ptr += block_size;
}
*(char**)ptr = NULL;

ptr一级指针被强转为了二级指针,ptr本来是指向一个字节(char)的指针,现在变为了指向8个字节的指针,

对于这一*(char**)ptr解引用操作,就能够操作以ptr原本指向的一字节开始的八个字节,就将这8个字节赋予为下一个块的地址ptr + block_size,这里不懂的话最后还会有一个二级指针操作队列尾部的例子。

这里我有必要解释一下一级指针与二级指针以char为例

  • char*一级指针,指向char类型的指针,char类型为一字节,也可以说是指向一个字节的指针
  • char**二级指针,指向char*类型的指针,由于64位下指针类型大小为8个字节所以,也可以说是指向8个字节的指针

经过上述操作,就隐式的构建了链表这样一来内存池内存块的管理就实现了。

  • 分配时从free_ptr中取出
  • 回收时从将内存块插入链表的头部

完整代码实现

#include <stdio.h>
#include <stdlib.h>
// 实现一个分配固定大小块的内存池
// 将4k的内存分割为大小相等的块  比如 32bytes/block  就有128个块
#define MEM_PAGE_SIZE 0X1000
typedef struct mempool_s{
    int block_size;  // 每一块
    int free_count;  // 剩余有多少块 可以用
    char *free_ptr;  // 可以分配的开始位置
    char *mem;       // 指向4k的起始位置
}mempool_t;

初始化内存池块

int memp_init(mempool_t * m, int block_size){  // 初始化4k内存
    if(!m) return -2;
    // 对内存池的每一项赋值
    m->block_size = block_size;
    m->free_count = MEM_PAGE_SIZE / block_size;  // 4k/32
    m->free_ptr = (char*)malloc(MEM_PAGE_SIZE);
    if(!m->free_ptr) return -1;
    m->mem = m->free_ptr;
    // 构建了隐式链表
    int i = 0;
    char *ptr = m->free_ptr;
    for(i = 0; i < m->free_count; i++){
        *(char**)ptr = ptr + block_size; 
        ptr += block_size;
    }
    *(char**)ptr = NULL;
    return 0;
}

分配内存块

// 以free_ptr链开始分配
void* memp_alloc(mempool_t* m){  // 已经确定了固定大小  不需要传入大小
    if(!m || m->free_count == 0) return NULL;
    void *ptr = m->free_ptr;
    m->free_ptr = *(char**)ptr;  // 后移指向下一个空闲处
    m->free_count--;
    return ptr;
}

回收内存块

void* memp_free(mempool_t* m, void *ptr){  // 
    // 将空闲块头插 进空闲列表
    *(char**)ptr = m->free_ptr;
    void * old_free = (void*)m->free_ptr;
    m->free_ptr = (char*)ptr;
    m->free_count++;
    return old_free;
}

测试

int main(){
    mempool_t m;
    int i = memp_init(&m, 32);
    printf("%d\n", i);
    void* p1 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p1);
    void* p2 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p2);
    void* p3 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p3);
    void* p4 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p4);
    memp_free(&m, p1);
    memp_free(&m, p3);
    void* p5 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p5);
    void* p6 = memp_alloc(&m);
    printf("memp_alloc : %p\n", p6);
}

打印

memp_alloc : 0x5624aa9cc260 # p1
memp_alloc : 0x5624aa9cc280 # p2
memp_alloc : 0x5624aa9cc2a0 # p3
memp_alloc : 0x5624aa9cc2c0 # p4
memp_alloc : 0x5624aa9cc2a0 # p5
memp_alloc : 0x5624aa9cc260 # p6

这个内存池存在的问题

可以明显的看见,我们分配的32个字节的内存其中将前面8个字节用来构造了隐式链表,所以实际能用的内存只有32-8,并且如果你分配的块的大小小于8,则上述程序会出现段错误

二级指针操作自定义类型

自定义类型二级指针的应用

假设有下列结构

typedef struct task_s {
    task_t *next;  // 必须是第一个字段
    handler_pt func;
    void *arg;
} task_t;

初始化

task_t *a = malloc(sizeof(task_t));

二级指针操作

*(task_t**)a = NULL;  // 等价于 a->next = NULL;

解释:a本来是一个task_t类型的指针,由于task_t24个字节,也可以说a是指向24个字节的指针,由于a被转为了二级指针,意思就是现在a是一个指向8个字节的指针,该8个字节就对应task_t中前8个字节也就是next指针,与是解引用就是相当于解了next指针的引用

相关文章
|
2月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
66 0
|
5天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
6天前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
22 2
|
17天前
|
C++
析构造函数就是为了释放内存,就是在局部指针消失前释放内存,拷贝构造函数就是以构造函数为模块,在堆里面新开一块,同一个变量在堆里面的地址
本文讨论了C++中构造函数和析构函数的作用,特别是它们在管理动态内存分配和释放中的重要性,以及如何正确地实现拷贝构造函数以避免内存泄漏。
30 2
|
3天前
|
运维 JavaScript Linux
容器内的Nodejs应用如何获取宿主机的基础信息-系统、内存、cpu、启动时间,以及一个df -h的坑
本文介绍了如何在Docker容器内的Node.js应用中获取宿主机的基础信息,包括系统信息、内存使用情况、磁盘空间和启动时间等。核心思路是将宿主机的根目录挂载到容器,但需注意权限和安全问题。文章还提到了使用`df -P`替代`df -h`以获得一致性输出,避免解析错误。
|
1月前
|
传感器 物联网 大数据
C 指针在物联网的应用
在物联网(IoT)中,C 语言及其指针功能广泛应用于嵌入式系统。C 指针在内存管理、设备驱动、数据结构处理、传感器通信等方面发挥关键作用,如动态分配内存、直接访问硬件寄存器、传递复杂数据结构等,有效提升了资源受限环境下的性能和灵活性。通过函数指针和省电模式管理,还能实现事件驱动编程和节能目标,使 C 语言成为 IoT 开发的重要工具。
60 12
|
1月前
|
存储 安全 C语言
C语言 二级指针应用场景
本文介绍了二级指针在 C 语言中的应用,
|
2月前
|
数据采集 Rust 安全
Rust在网络爬虫中的应用与实践:探索内存安全与并发处理的奥秘
【8月更文挑战第31天】网络爬虫是自动化程序,用于从互联网抓取数据。随着互联网的发展,构建高效、安全的爬虫成为热点。Rust语言凭借内存安全和高性能特点,在此领域展现出巨大潜力。本文探讨Rust如何通过所有权、借用及生命周期机制保障内存安全;利用`async/await`模型和`tokio`运行时处理并发请求;借助WebAssembly技术处理动态内容;并使用`reqwest`和`js-sys`库解析CSS和JavaScript,确保代码的安全性和可维护性。未来,Rust将在网络爬虫领域扮演更重要角色。
66 1
|
2月前
|
JavaScript 前端开发 Java
JavaScript内存泄露大揭秘!你的应用为何频频“爆内存”?点击解锁救星秘籍!
【8月更文挑战第23天】在Web前端开发中,JavaScript是构建动态网页的关键技术。然而,随着应用复杂度增加,内存管理变得至关重要。本文探讨了JavaScript中常见的内存泄露原因,包括意外的全局变量、不当使用的闭包、未清除的定时器、未清理的DOM元素引用及第三方库引发的内存泄露。通过了解这些问题并采取相应措施,开发者可以有效避免内存泄露,提高应用性能。
44 1