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


    一、为什么需要内存池

    按照标准库的写法,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 
    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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

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

    malloc a mem
    0x19c53785f20:
    malloc a mem
    0x19c53785f30:
    free a mem
    malloc a mem
    0x19c53785f20:
    free a mem
    free a mem
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

  • 相关阅读:
    防火墙部署模式 -- 镜像流量(旁路模式)
    手机+卫星的科技狂想
    vue 在什么情况下在数据发生改变的时候不会触发视图更新
    【无标题】
    JNDI注入的理解、JDK给出的修复
    OpenJudge NOI 1.13 49:计算对数
    java计算机毕业设计养生药膳推荐系统源程序+mysql+系统+lw文档+远程调试
    拷贝方式之DMA
    Redis 5 种基本数据类型详解
    vue 里使用 antv g6 实现脑图左右布局、文本超出隐藏处理、自定义边、自定义节点、自定义事件功能
  • 原文地址:https://blog.csdn.net/jiexianxiaogege/article/details/133780740