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

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

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

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

要点:

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

存储结构

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指针的引用

相关文章
|
16天前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
49 0
|
1天前
|
缓存 算法 Java
聚焦Java应用程序的内存管理和调优技巧
在现代软件开发中,性能优化对提升用户体验和系统稳定性至关重要。本文聚焦Java应用程序的内存管理和调优技巧。从理解Java内存模型入手,深入探讨堆内存的管理与优化,揭示如何避免内存泄漏,利用工具检测问题,并介绍高效字符串处理及数据结构选择的方法。同时,解析垃圾回收机制及其调优策略,包括不同回收器的选择与配置。此外,还介绍了调整堆大小、运用对象池和缓存技术等高级技巧。通过这些方法,开发者能有效提升应用性能和稳定性。
9 1
|
4天前
|
算法 Java
双指针在数组遍历中的应用
文章深入探讨了双指针技术在数组遍历中的应用,通过实战例子详细解释了快慢指针和首尾指针的不同用法,并提供了解决LeetCode相关问题的Java代码实现。
|
4天前
|
存储 搜索推荐 C语言
C语言中的指针函数:深入探索与应用
C语言中的指针函数:深入探索与应用
|
13天前
|
存储 安全 Go
Go 中的指针:了解内存引用
Go 中的指针:了解内存引用
|
8天前
|
编解码 Android开发 UED
【性能狂飙!】揭秘Android应用极速变身秘籍:内存瘦身+用户体验升级,打造丝滑流畅新境界!
【8月更文挑战第12天】构建高效Android应用需全方位优化,尤其重视内存管理和用户体验。通过弱引用降低内存占用,懒加载资源减少启动负担。运用Kotlin协程确保UI流畅不阻塞,响应式设计适配多屏需求。这些策略共同提升了应用性能与用户满意度。
20 1
|
29天前
|
缓存 安全 算法
Java内存模型深度解析与实践应用
本文深入探讨Java内存模型(JMM)的核心原理,揭示其在并发编程中的关键作用。通过分析内存屏障、happens-before原则及线程间的通信机制,阐释了JMM如何确保跨线程操作的有序性和可见性。同时,结合实例代码,展示了在高并发场景下如何有效利用JMM进行优化,避免常见的并发问题,如数据竞争和内存泄漏。文章还讨论了JVM的垃圾回收机制,以及它对应用程序性能的影响,提供了针对性的调优建议。最后,总结了JMM的最佳实践,旨在帮助开发人员构建更高效、稳定的Java应用。
|
1月前
|
监控 Java 数据库连接
Java面试题:如何诊断和解决Java应用的内存泄漏问题?
Java面试题:如何诊断和解决Java应用的内存泄漏问题?
24 2
|
27天前
|
存储 缓存 安全
深入理解Java内存模型(JMM)及其在并发编程中的应用
本文旨在探索Java内存模型(JMM)的奥秘,并揭示其在并发编程中的关键作用。我们将通过数据支撑,案例分析,以及对比研究的方法,深入剖析JMM的核心概念、原理和机制。文章将提供丰富的实例,包括同步块、volatile关键字的使用,以及线程间的通信机制,来具体展示JMM如何管理并发环境下的数据一致性和可见性问题。我们还将讨论JMM在现代多核处理器架构下面临的挑战,以及如何在编写高效且线程安全的代码时避免常见的并发陷阱。最后,文章将提出一些最佳实践,帮助开发者充分利用JMM的特性,以提升应用程序的性能和可靠性。
|
30天前
|
算法 Java Serverless
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决