C++手写内存池之内存池的重置、销毁与完整生命周期

 更新时间:2026年07月18日 09:45:10   作者:(Charon)  
本文结合代码讲解内存池的清零分配、重置和销毁流程,并分析重置内存池与彻底释放内存池之间的区别,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧

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

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

对应的操作分别是:

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

到此这篇关于C++手写内存池之内存池的重置、销毁与完整生命周期的文章就介绍到这了,更多相关C++ 内存池生命周期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 用c++实现x的y次幂的代码

    用c++实现x的y次幂的代码

    以下实例是对使用c++实现x的y次幂的解决方法进行了介绍。需要的朋友参考下
    2013-05-05
  • C++字符串反转的几种方法

    C++字符串反转的几种方法

    通过不同的方法,实现对所输入字符串的反转,具有一定的参考价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-06-06
  • 总结c++性能优化策略

    总结c++性能优化策略

    在本篇文章中小编给大家总结了关于C++的性能优化策略的相关知识点,对此有兴趣的朋友可以参考学习下。
    2018-03-03
  • 浅谈带缓冲I/O 和不带缓冲I/O的区别与联系

    浅谈带缓冲I/O 和不带缓冲I/O的区别与联系

    下面小编就为大家带来一篇浅谈带缓冲I/O 和不带缓冲I/O的区别与联系。小编觉得挺不错的现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • C语言接口与实现方法实例详解

    C语言接口与实现方法实例详解

    这篇文章主要介绍了C语言接口与实现方法,包括接口的概念、实现方法及抽象数据类型等,并配合实例予以说明,需要的朋友可以参考下
    2014-09-09
  • VS2019 Nuget找不到包的问题处理

    VS2019 Nuget找不到包的问题处理

    这篇文章主要介绍了VS2019 Nuget找不到包的问题处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • C++使用FFmpeg实现YUV数据编码转视频文件

    C++使用FFmpeg实现YUV数据编码转视频文件

    这篇文章主要介绍了C++如何使用FFmpeg实现把一个YUV原始视频数据(时间序列图像)经过h264编码为视频码流,然后在使用mp4封装格式封装,感兴趣的可以了解一下
    2023-06-06
  • C语言数组栈实现模板

    C语言数组栈实现模板

    这篇文章主要为大家详细介绍了C语言数组栈实现模板,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-12-12
  • C++11标准库bind函数应用教程

    C++11标准库bind函数应用教程

    bind函数定义在头文件functional中,可以将bind函数看做成一个通用的函数适配器,他接收一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表。本文将带大家详细了解一下bind函数的应用详解
    2021-12-12
  • C语言创建和操作单链表数据结构的实例教程

    C语言创建和操作单链表数据结构的实例教程

    这篇文章主要介绍了C语言创建和操作单链表数据结构的实例教程,讲解使用C语言实现链表结构时指针的使用,需要的朋友可以参考下
    2016-04-04

最新评论