news 2026/7/18 16:54:27

【C/C++】手写内存池(四):内存池的重置、销毁与完整生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C/C++】手写内存池(四):内存池的重置、销毁与完整生命周期

前面已经介绍了小块内存的顺序分配、内存对齐、节点扩容以及大块内存的单独管理。一个完整的内存池还需要解决两个问题:

一批数据使用结束后,如何快速重新使用内存池? 程序结束时,如何完整释放内存池中的所有资源?

对应的操作分别是:

mp_reset_pool():重置内存池,保留已经申请的内存节点 mp_destroy_pool():销毁内存池,彻底释放所有内存

本文将结合mp_calloc()mp_reset_pool()mp_destroy_pool(),完成整个内存池生命周期的分析。

一、重置内存池和销毁内存池有什么区别

内存池中的小块内存采用顺序分配方式:

内存起始位置 ↓ +----------+----------+----------+----------------+ | 已分配A | 已分配B | 已分配C | 剩余空间 | +----------+----------+----------+----------------+ ↑ ↑ last end

每次申请内存时,last都会向后移动:

node->last = memory + size;

小块内存通常不支持逐个释放。如果一批任务全部完成,可以直接将所有节点的last恢复到初始位置。

这就是重置内存池:

重置前: +----------+----------+----------------+ | 已使用内存 | 剩余空间 | +----------+----------+----------------+ ↑ ↑ last end 重置后: +--------------------------------------+ | 全部可用空间 | +--------------------------------------+ ↑ ↑ last end

重置操作不会释放小块内存节点,而是将它们恢复为可重新分配的状态。

销毁内存池则完全不同:

重置内存池: 保留内存节点,只清除使用状态,可以继续分配。 销毁内存池: 释放所有大块内存、扩展节点和内存池本身, 销毁后不能继续使用。

因此,二者适用于不同场景:

同一个内存池需要重复使用: 调用 mp_reset_pool() 内存池以后不再使用: 调用 mp_destroy_pool()

例如,服务器在处理每一个请求时都需要申请许多临时对象,可以为每个请求复用同一个内存池:

开始处理请求1 ↓ 从内存池申请临时数据 ↓ 请求1处理完成 ↓ 重置内存池 ↓ 开始处理请求2

与逐个调用free()相比,重置内存池只需要恢复几个指针,效率更高。


二、mp_calloc 如何申请并清零内存

普通的mp_alloc()只负责申请内存,不保证返回空间中的数据为零。

void *data = mp_alloc(pool, 128);

这块内存可能以前被使用过,因此其中可能仍然保留旧数据。

如果需要得到一块已经清零的内存,可以实现mp_calloc()

/** * 从内存池申请一块内存,并将其初始化为0 * * pool:内存池对象 * size:需要申请的字节数 * * 返回值: * 成功时返回内存地址,失败时返回NULL */ void *mp_calloc(struct mp_pool_s *pool, size_t size) { // 先通过普通分配函数申请内存 void *ptr = mp_alloc(pool, size); if (ptr != NULL) { // 将申请到的size字节全部设置为0 memset(ptr, 0, size); } return ptr; }

它的执行过程为:

调用 mp_alloc() ↓ 获得一块可用内存 ↓ 调用 memset() 清零 ↓ 返回初始化后的内存

使用方式如下:

// 申请128字节,并将所有内容初始化为0 unsigned char *buffer = mp_calloc(pool, 128); if (buffer == NULL) { printf("内存申请失败\n"); return; }

也可以用于创建并初始化结构体:

typedef struct user_s { int id; int age; char name[32]; } user_t; // 申请一个user_t对象,并将所有成员清零 user_t *user = mp_calloc(pool, sizeof(user_t)); if (user != NULL) { user->id = 1001; user->age = 24; }

此时,name数组中的内容默认全部为0,不需要再单独初始化。

mp_alloc()mp_calloc()的区别可以总结为:

mp_alloc: 只申请内存,原有内容不确定,速度相对更快。 mp_calloc: 申请内存后调用memset清零,适合需要初始化的数据。

需要注意,mp_calloc()中的calloc只是表示“申请并清零”,内存最终仍然由内存池统一管理,不能直接对小块内存调用free()


三、mp_reset_pool 如何重置整个内存池

重置内存池需要完成以下工作:

释放所有大块内存 ↓ 清空大块内存记录入口 ↓ 恢复每个小块节点的last指针 ↓ 清空节点分配失败次数 ↓ 让current重新指向第一个节点

实现代码如下:

/** * 重置内存池 * * 重置后: * 1. 所有小块内存重新变为可分配状态; * 2. 所有大块内存都会被释放; * 3. 已经创建的小块内存节点会被保留; * 4. 内存池可以继续使用。 */ void mp_reset_pool(struct mp_pool_s *pool) { if (pool == NULL) { return; } struct mp_large_s *large; struct mp_node_s *node; /* * 第一步:释放所有仍然存在的大块内存。 * * large管理节点本身位于小块内存池中, * 因此这里只释放large->alloc指向的实际内存。 */ for (large = pool->large; large != NULL; large = large->next) { if (large->alloc != NULL) { free(large->alloc); large->alloc = NULL; } } /* * 重置小块区域后,原来的large管理记录也会失效, * 因此将大块内存链表入口清空。 */ pool->large = NULL; /* * 第二步:恢复所有小块内存节点的分配位置。 * * 每个节点开头保存mp_node_s管理结构, * 真正的可分配空间位于管理结构之后。 */ for (node = pool->head; node != NULL; node = node->next) { node->last = (unsigned char *)node + sizeof(struct mp_node_s); // 清空当前节点的分配失败次数 node->failed = 0; } /* * 第三步:重新从第一个节点开始分配。 */ pool->current = pool->head; }

假设内存池中有三个节点:

pool->head | v node1 → node2 → node3 → NULL

重置之前,每个节点可能已经使用了一部分空间:

node1:last接近end node2:last位于中间 node3:last刚刚开始移动

执行:

mp_reset_pool(pool);

以后,三个节点都会恢复:

node1:last恢复到管理结构之后 node2:last恢复到管理结构之后 node3:last恢复到管理结构之后

节点本身并没有被释放,因此下一次分配可以直接重复使用这些内存,不需要再次调用posix_memalign()创建节点。

这也是重置操作效率较高的原因。

重置后原来的指针还能使用吗

不能。

例如:

char *name = mp_alloc(pool, 64); strcpy(name, "memory pool"); mp_reset_pool(pool); // 错误:name指向的空间已经重新归内存池管理 printf("%s\n", name);

虽然name保存的地址没有立刻改变,但该地址已经可以被下一次内存申请重新使用。

例如:

char *name = mp_alloc(pool, 64); mp_reset_pool(pool); char *message = mp_alloc(pool, 64);

此时message很可能与name指向同一位置:

name = 0x1000 message = 0x1000

因此,调用mp_reset_pool()后,之前从该内存池中获得的所有指针都应该视为失效。


四、mp_destroy_pool 如何彻底释放资源

销毁内存池时,需要释放三类资源:

1. 通过malloc或posix_memalign单独申请的大块内存; 2. 内存池扩容时创建的额外小块节点; 3. 创建内存池时申请的首块内存。

内存池结构可以表示为:

pool ├── 首个节点head │ ├── node2 │ ├── node3 │ └── large链表 ├── large1 → 大块内存1 └── large2 → 大块内存2

销毁函数如下:

/** * 销毁内存池并释放全部资源 * * 调用后,pool以及从pool中申请的所有地址都会失效。 */ void mp_destroy_pool(struct mp_pool_s *pool) { if (pool == NULL) { return; } struct mp_large_s *large; struct mp_node_s *node; struct mp_node_s *next; /* * 第一步:释放所有尚未手动释放的大块内存。 */ for (large = pool->large; large != NULL; large = large->next) { if (large->alloc != NULL) { free(large->alloc); large->alloc = NULL; } } /* * 第二步:释放扩容时创建的额外节点。 * * 第一个节点head与pool位于同一次内存申请中, * 因此不能在这里单独释放head。 */ node = pool->head->next; while (node != NULL) { next = node->next; free(node); node = next; } /* * 第三步:释放内存池本身。 * * pool和第一个节点head属于同一块内存, * free(pool)会同时释放二者。 */ free(pool); }

为什么从下面的位置开始释放节点:

node = pool->head->next;

而不是:

node = pool->head;

原因是第一个节点与内存池对象是在同一次内存申请中创建的:

posix_memalign( (void **)&pool, MP_ALIGNMENT, size + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s) );

内存布局为:

+------------------+------------------+----------------+ | mp_pool_s | 第一个mp_node_s | 可分配空间 | +------------------+------------------+----------------+ ↑ pool

因此,第一个节点不能单独调用:

free(pool->head);

否则会释放一个并非独立申请的地址,造成未定义行为。

后续扩展节点则是单独申请的:

posix_memalign( (void **)&memory, MP_ALIGNMENT, block_size );

它们可以逐个调用free()

正确销毁顺序是:

释放所有大块内存 ↓ 释放node2、node3等额外节点 ↓ 释放pool及其内部的第一个节点

在部分代码中,销毁函数可能被命名为:

mp_destory_pool()

其中destory是拼写错误,更规范的命名应该是:

mp_destroy_pool()

实际使用时,函数声明、函数定义和调用处保持一致即可。


五、完整使用流程与注意事项

下面通过一个完整示例,演示内存池的创建、分配、重置、复用和销毁过程。

#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { /* * 创建一个4096字节的内存池。 */ struct mp_pool_s *pool = mp_create_pool(4096); if (pool == NULL) { printf("内存池创建失败\n"); return -1; } /* * 第一阶段:申请小块内存。 */ char *name = mp_alloc(pool, 64); if (name != NULL) { strcpy(name, "memory pool"); printf("name = %s\n", name); } /* * 申请并清零一个整数数组。 */ int *numbers = mp_calloc( pool, sizeof(int) * 10 ); if (numbers != NULL) { for (int i = 0; i < 10; ++i) { printf("%d ", numbers[i]); } printf("\n"); } /* * 申请一块大内存。 * 如果大小超过pool->max, * 内部会调用malloc单独申请。 */ void *large_buffer = mp_alloc(pool, 8192); if (large_buffer != NULL) { printf( "large buffer = %p\n", large_buffer ); } /* * 大块内存支持单独释放。 */ mp_free(pool, large_buffer); /* * 第一批数据使用完毕,重置内存池。 * * 重置后,name和numbers都不能继续使用。 */ mp_reset_pool(pool); /* * 第二阶段:复用已经创建的内存节点。 */ char *message = mp_calloc(pool, 128); if (message != NULL) { strcpy(message, "pool reused successfully"); printf("message = %s\n", message); } /* * 内存池不再使用,彻底释放所有资源。 */ mp_destroy_pool(pool); /* * 销毁后不能继续访问pool、message等地址。 */ pool = NULL; message = NULL; return 0; }

完整生命周期可以概括为:

创建内存池 mp_create_pool() ↓ 申请小块或大块内存 mp_alloc() / mp_calloc() ↓ 大块内存可以单独释放 mp_free() ↓ 一批数据使用完成 mp_reset_pool() ↓ 重复使用已有内存节点 ↓ 程序不再需要内存池 mp_destroy_pool()

使用过程中需要注意以下问题:

1. mp_reset_pool不会释放小块节点,而是恢复分配位置; 2. 重置后,之前申请的所有小块内存地址都不能继续使用; 3. 重置时,尚未手动释放的大块内存也会被释放; 4. mp_free只用于释放large链表管理的大块内存; 5. 第一个节点与pool属于同一次内存申请,不能单独free; 6. 额外扩展节点需要在销毁时逐个释放; 7. 内存池销毁后,所有从池中获得的地址都会失效; 8. 当前实现没有同步保护,不支持多线程同时分配。

在创建内存池时,还应确保第一个节点的成员被完整初始化:

pool->head->next = NULL; pool->head->failed = 0;

重置内存池时,也建议恢复:

pool->current = pool->head; node->failed = 0;

避免分配器继续使用重置前的查找状态。

至此,一个基础内存池的主要内容已经完整,包括:

固定大小内存块与空闲链表; 小块内存的顺序分配与地址对齐; 大块内存的记录与单独释放; 内存池的清零、重置和彻底销毁。

0voice · GitHub

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

XUnity Auto Translator:Unity游戏实时文本翻译与汉化实战指南

1. 项目概述&#xff1a;为什么你需要一个专业的游戏翻译工具 如果你是一个喜欢玩各种独立游戏、视觉小说&#xff0c;或者经常接触一些尚未推出官方中文版的海外游戏的玩家&#xff0c;那么“啃生肉”的痛苦你一定深有体会。面对满屏的英文、日文或者其他语言&#xff0c;即使…

作者头像 李华
网站建设 2026/7/18 16:48:48

AI 网关语义路由:共享底座多 LoRA 微调的架构思考与调参指南

如果你的团队已经在 AI 网关上跑了一段时间的流量&#xff0c;大概率会遇到这样几个让人头疼的问题&#xff1a;中文意图分类的准确率远低于英文测试集&#xff1b;PII 脱敏时个人信息偶尔被截断&#xff1b;越狱检测的召回率在真实流量里远不如离线评估&#xff1b;以及随着检…

作者头像 李华
网站建设 2026/7/18 16:46:34

2026年7月全国精选5款钟表眼镜零售SAAS小程序商城制作软件:0门槛做小程序

2026年7月全国精选5款钟表眼镜零售SAAS小程序商城制作软件&#xff1a;0门槛做小程序 站在经营眼镜店、配镜中心或钟表门店的老板的角度&#xff0c;选择小程序工具不能只看模板和最低价格。钟表、眼镜零售经营中&#xff0c;验光配镜需要预约和档案、镜框镜片组合多、钟表序列…

作者头像 李华
网站建设 2026/7/18 16:44:18

Adobe Photoshop 基础从入门到精通

学习 Adobe Photoshop 从入门到精通&#xff0c;可以遵循一个由浅入深、理论与实践相结合的路径。以下为你梳理了一个系统性的学习框架。&#x1f3af; 第一阶段&#xff1a;夯实基础&#xff0c;理解核心这个阶段的目标是熟悉软件界面&#xff0c;掌握核心概念&#xff0c;而不…

作者头像 李华
网站建设 2026/7/18 16:42:19

AI辅助编剧技术解析:从逻辑校验到复杂系统建模

最近刷到一部2026年即将上线的悬疑美剧预告&#xff0c;剧情设定相当炸裂&#xff1a;男主因谋杀罪入狱五年&#xff0c;结果"死者"突然完好无损地现身&#xff0c;整个司法系统瞬间崩塌。这种高概念设定背后&#xff0c;其实藏着影视行业正在发生的技术革命——AI辅…

作者头像 李华