news 2026/4/16 4:28:03

手写一个简单的内存池:C语言动态内存管理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写一个简单的内存池:C语言动态内存管理实战

前言

在上篇文章中,我们提到了动态内存分配的四大金刚:malloc、calloc、realloc、free。然而,频繁调用这些函数会带来两个问题:

1. 性能开销:系统调用涉及用户态/内核态切换,频繁分配小块内存时效率低下
2. 内存碎片:反复分配释放会产生大量外部碎片,导致大块连续内存分配失败

内存池 是一种常见的解决方案:预先申请一大块内存,然后由我们自己管理其内部的小块分配与释放。今天,我们就手写一个简单实用的内存池,彻底理解其背后的原理。

一、内存池的核心设计思想

一个经典的内存池需要解决三个核心问题:

· 如何快速分配:维护空闲块链表,分配时直接取用
· 如何高效合并:释放时尝试合并相邻空闲块,减少碎片
· 如何管理元数据:每个内存块需要记录大小、是否空闲等信息

我们采用 边界标记法(Boundary Tag) 实现一个简单但完整的内存池。

二、数据结构设计

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

// 内存块头结构(紧挨着用户数据之前)
typedef struct block_header {
size_t size; // 当前块大小(包括头部)
int is_free; // 是否空闲
struct block_header* next; // 指向下一个空闲块(仅空闲块使用)
struct block_header* prev; // 指向上一个空闲块
} block_header_t;

// 内存池结构
typedef struct {
void* pool_start; // 池起始地址
size_t pool_size; // 池总大小
block_header_t* free_list; // 空闲链表头
} memory_pool_t;
```

关键设计点:

· 每个内存块前都有一个 block_header_t,存储元数据
· 空闲块通过 next/prev 连接成双向链表
· 用户得到的地址是 header + 1(跳过头部)

三、核心函数实现

1. 内存池创建

```c
memory_pool_t* mem_pool_create(size_t size) {
if (size < sizeof(block_header_t) + 8) {
size = sizeof(block_header_t) + 8; // 最小池大小
}

memory_pool_t* pool = (memory_pool_t*)malloc(sizeof(memory_pool_t));
if (!pool) return NULL;

pool->pool_start = malloc(size);
if (!pool->pool_start) {
free(pool);
return NULL;
}

pool->pool_size = size;

// 初始化唯一的空闲块
block_header_t* first_block = (block_header_t*)pool->pool_start;
first_block->size = size;
first_block->is_free = 1;
first_block->next = NULL;
first_block->prev = NULL;
pool->free_list = first_block;

return pool;
}
```

2. 分配函数(首次适应算法)

```c
void* mem_pool_alloc(memory_pool_t* pool, size_t req_size) {
if (!pool || req_size == 0) return NULL;

// 对齐到8字节边界(常见CPU要求)
size_t aligned_size = (req_size + 7) & ~7;
size_t total_size = aligned_size + sizeof(block_header_t);

block_header_t* current = pool->free_list;

while (current) {
if (current->is_free && current->size >= total_size) {
// 找到合适的空闲块
if (current->size >= total_size + sizeof(block_header_t) + 8) {
// 拆分:剩余空间足够成为一个新的空闲块
block_header_t* new_block = (block_header_t*)((char*)current + total_size);
new_block->size = current->size - total_size;
new_block->is_free = 1;
new_block->next = current->next;
new_block->prev = current;

if (current->next) {
current->next->prev = new_block;
}

current->size = total_size;
current->next = new_block;
}

current->is_free = 0;

// 从空闲链表中移除
if (current->prev) {
current->prev->next = current->next;
} else {
pool->free_list = current->next;
}
if (current->next) {
current->next->prev = current->prev;
}

// 返回用户数据区地址
return (void*)(current + 1);
}
current = current->next;
}

return NULL; // 内存不足
}
```

3. 释放与合并

```c
void mem_pool_free(memory_pool_t* pool, void* ptr) {
if (!pool || !ptr) return;

// 获取块头地址
block_header_t* block = (block_header_t*)ptr - 1;
block->is_free = 1;

// 尝试与前一个块合并(如果前一个块存在且空闲)
// 注意:这里简化了,完整实现需要遍历或维护相邻块指针
// 实际工程中会维护每个块的前后块指针(不仅仅是空闲块)

// 插入回空闲链表(按地址排序)
block_header_t* current = pool->free_list;
block_header_t* prev = NULL;

while (current && current < block) {
prev = current;
current = current->next;
}

block->next = current;
block->prev = prev;
if (prev) {
prev->next = block;
} else {
pool->free_list = block;
}
if (current) {
current->prev = block;
}

// 尝试与后一个空闲块合并
if (current && (char*)block + block->size == (char*)current) {
block->size += current->size;
block->next = current->next;
if (current->next) {
current->next->prev = block;
}
}

// 尝试与前一个空闲块合并
if (prev && (char*)prev + prev->size == (char*)block) {
prev->size += block->size;
prev->next = block->next;
if (block->next) {
block->next->prev = prev;
}
}
}
```

4. 销毁内存池

```c
void mem_pool_destroy(memory_pool_t* pool) {
if (!pool) return;
if (pool->pool_start) {
free(pool->pool_start);
}
free(pool);
}
```

四、测试代码

```c
int main() {
// 创建 4KB 的内存池
memory_pool_t* pool = mem_pool_create(4096);
printf("内存池创建成功,总大小: %zu 字节\n", pool->pool_size);

// 分配三次
int* a = (int*)mem_pool_alloc(pool, sizeof(int));
char* b = (char*)mem_pool_alloc(pool, 64);
double* c = (double*)mem_pool_alloc(pool, sizeof(double));

*a = 42;
*c = 3.14159;
printf("a=%d, c=%f\n", *a, *c);

// 释放中间块
mem_pool_free(pool, b);
printf("释放 b 后,空闲链表重新连接\n");

// 再次分配,应复用刚刚释放的内存
char* d = (char*)mem_pool_alloc(pool, 32);
printf("重新分配得到: %p\n", d);

// 清理
mem_pool_destroy(pool);
return 0;
}
```

五、优化与扩展方向

以上实现是一个教学版内存池,实际工程中可以从以下方面优化:

1. 多个固定大小池:预先创建不同尺寸的池(如 8B、16B、32B...),分配时取最接近的,彻底消除内部碎片
2. 线程安全:加互斥锁或使用线程局部存储(TLS)
3. 调试支持:在头部增加魔数、分配调用栈记录,便于检测内存越界和泄漏
4. 缓存对齐:将块大小对齐到 CPU 缓存行,避免伪共享

六、常见陷阱

· 越界访问:用户数据超出申请大小,会破坏下一个块的头部元数据
· 重复释放:释放同一块内存两次,链表会被破坏
· 释放非池内地址:需要增加校验机制

结语

手写内存池的过程,本质上是在重新思考操作系统的内存管理算法。通过这个练习,你不仅掌握了一个实用工具的实现,更能深刻理解内存布局、链表操作和碎片管理这些核心概念。

当然,生产环境建议使用成熟的内存池库(如 tcmalloc、jemalloc),但亲手写一遍的价值,无可替代。

下一篇预告:《深入理解C语言指针:从一级指针到函数指针》

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 4:22:48

Google-10000-English:自然语言处理的终极词频数据集

Google-10000-English&#xff1a;自然语言处理的终极词频数据集 【免费下载链接】google-10000-english This repo contains a list of the 10,000 most common English words in order of frequency, as determined by n-gram frequency analysis of the Googles Trillion Wo…

作者头像 李华
网站建设 2026/4/16 4:18:10

如何快速掌握TX-LCN分布式事务框架:从理论到实践的完整指南

如何快速掌握TX-LCN分布式事务框架&#xff1a;从理论到实践的完整指南 【免费下载链接】tx-lcn codingapi/tx-lcn: 一个基于 Java 的分布式事务框架&#xff0c;用于解决分布式系统中的事务问题。适合在分布式系统中需要处理事务的场景&#xff0c;可以实现高性能、高可用性的…

作者头像 李华
网站建设 2026/4/16 4:17:11

OpenAppFilter时间控制教程:为孩子制定健康上网时间表

OpenAppFilter时间控制教程&#xff1a;为孩子制定健康上网时间表 【免费下载链接】OpenAppFilter OAF(OpenAppFilter) is a parental control software based on OpenWrt. It supports popular applications across gaming, video streaming, instant messaging, such as TikT…

作者头像 李华