C++内存管理:其四、使用链表实现简易版内存池

本文介绍了内存池的概念,为何需要它来解决频繁malloc和free的问题,以及链表如何实现内存池的基本思想,包括内存的分配、回收和操作符重载。作者通过代码示例展示了内存池的工作原理,以及可能遇到的一些问题,如内存对齐导致的地址偏移。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、为什么需要内存池?

按照标准库的写法,new一个对象的时候,会malloc一块内存;delete的时候会free这块内存。频繁的malloc与free存在两个问题:
(1)耗时,这两个都是操作系统层级的函数,会相对耗时。
(2)malloc的内存中存在上下两个cookie,用来标记这块内存的起止地址,而且内存要向上取8的倍数。如果大量malloc,那么内存内存浪费还是比较惊人的。
这个问题,内存池可以很好地解决。

二、什么是内存池?

既然频繁的malloc不是一件好事情,那么我们可以一次malloc申请一大块内存。当需要new一个对象的时候,可以在已经申请好的内存里面执行构造函数。在我们delete这个对象之后,这个内存上还可以用来构造其它对象。
这就是内存池设计的基本思想:一次性申请大块内存,用于多次new对象,避免频繁的malloc与delete。

三、链表实现内存池的基本思想

你如果在中文搜索引擎上面搜索“内存池的实现”,出现最多的方法就是链表实现内存池。有一说一,这个名字取得不好,非常容易让人产生误解。比如:
(1)用链表实现内存池,链表本来就是不连续的内存,那就说明要malloc多次,那么这个内存池有什么意义???
答案:内存池就是一次(扩容情况下多次)malloc的内存,是连续的内存。只不过将连续的内存使用链表管理起来了。

(2)既然有了一个连续的内存,为什么要将其用链表分割呢?
答案:方便我们即使获取被释放的内存,将其纳入可用内存。举个例子,假如我们的内存池可以容纳若干个对象,我们依次按照地址顺序构造了三个对象,这个时候如果释放了第一个对象,可以把这块地址通过指针管理起来。一句话:内存池在构造的时候是连续地址,但是在多次获取和释放以后,就会变成局部离散的地址!

内存池的基本设计思想:
(1)一次申请大块内存,将其按照对象所需的大小切片。各个内存片之间通过指针串起来。内存池构造好的时候,这个链表的各个结构体内存地址是顺序的。
(2)如果需要使用内存,将链表头指向下一个节点,原来的链表头返回。这个链表头中有足够的空间供给所需对象的构造。
(3)如果需要释放内存,则将这块内存原封不动的插到链表头,到这里就可以看出来,多次申请和释放内存后,链表就不是有序的了。
(4)最关键一步:使用这个内存池的类,要重载operator new和operator delete函数,使用内存池版本。

看代码:

#include <iostream>
using namespace std;

template<int ObjectSize, int maxSize = 20>
class MemPool
{
    struct Node{     //实际用于构造对象的内存
        Node * next = nullptr;
        char data[ObjectSize];   //足够大的空间容纳对象
    };
private:
    Node * freeNodeHead{nullptr};
public:
    MemPool(){
        cout<<sizeof(Node)<<endl;
        freeNodeHead = new Node[maxSize];
        for(int i=0;i<maxSize-1;i++){
            freeNodeHead[i].next=&freeNodeHead[i+1];
        }
    }

    ~MemPool(){
        delete [] freeNodeHead;
    }

    void * malloc(){
        if(freeNodeHead == nullptr){
            freeNodeHead = new Node[maxSize];
            for(int i=0;i<maxSize-1;i++){
                freeNodeHead[i].next=&freeNodeHead[i+1];
            }
        }
        cout<< "malloc a mem" <<endl;
        Node * ret = freeNodeHead;
        freeNodeHead = freeNodeHead->next;
        ret->next = nullptr;
        return ret;
    };

    void free(void * p){
        cout<<"free a mem"<<endl;
        Node * newNode = (Node * )p;
        newNode->next = freeNodeHead;
        freeNodeHead = newNode;
    }
};

class Test {
    int No{1};

public:

    void print() {
        cout << this << ": "<<endl;
    }

    void* operator new(size_t size);
    void operator delete(void* p);
};

MemPool<sizeof(Test), 10> mp;

void* Test::operator new(size_t size) {
    return mp.malloc();
}

void Test::operator delete(void* p) {
    mp.free(p);
}

int main()
{
    Test * p1 = new Test;
    p1->print();

    Test * p2 = new Test;
    p2->print();

    delete p1;

    Test * p3 = new Test;
    p3->print();

    delete p2;

    delete p3;

}

猜测一下,如果p1和p3的地址一模一样,就说明我们的内存池成功了。

malloc a mem
0x19c53785f20:
malloc a mem
0x19c53785f30:
free a mem
malloc a mem
0x19c53785f20:
free a mem
free a mem

一模一样,成功了。
但是好像有点问题,地址偏移量什么是16个字节?因为内存对齐机制。虽然ObjectSize+指针大小小于16,但是会对齐为16个字节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值