风骚裙摆--最后的内存池

来源:互联网 发布:算法导论 第三版 目录 编辑:程序博客网 时间:2024/06/11 04:54

上一篇文章我介绍过了对象池,后来想了一下,发现就这么用模板以及一些raii技法,工厂模式实现一个对象管理接口似乎有点局限性,首先它很难做到单例模式,因为一个程序总不能来来去去就创建那么一种对象吧,但单例的情况下,这个对象池的实例只能被构造一次,也就是说,只能填充一次模板实体,好咯,既然有局限性,那我就会想设法去改善它咯,所以我又设计并开发了一个内存池。。。

所谓内存池,就完全可以利用单例模式,抛开对象具体类型的束缚,一切以类型的字节大小为度量单位,进行内存的分配,管理以及最后的回收,内存池析构时所有控制的内存的完全释放,内存池的编写我也在很多地方仔细考虑了内存泄露问题,经过测试应该没有内存泄露问题,内存池的全部代码最新版本放在我的github https://github.com/linuxb/BBMemoryPool上,也十分欢迎大家来进行commit啦,或者debug来找出隐藏的内存泄露,本人不胜感激。

好,现在说一下我的内存池怎么设计的吧,有必要看看UML设计图:
这里写图片描述

设计思想跟前面的对象池基本类似,主要是内存池主类CMemoryPool类以及一个聚合于它的内存池管理类CMemoryManager,前者通过后者的一个指针对管理类的方法进行调用,管理类其实主要就是一个avl树,对于avl树的操作,我专门写了一个类对它的操作进行了封装,就是CManagerTree类啦,CMemoryManager 继承自这个基类,所以它也可以进行avl树的操作,其他的聚合类型主要是三个结构体,分别有两个链表一颗avl树,其实就是维持平衡状态的二叉树,详细的数据结构我上一张数据结构设计图:

这里写图片描述

它们之间的依赖关系比较复杂,开始考虑用哈希表类完成操作的,毕竟哈希表也是通过哈希映射直接由获得key作为数组索引,但考虑到,一般的哈希表又要实现哈希映射,而且bucket的长度在这种跨度较大的条件下对内存空间的消耗是在太大,比如我分配4字节的对象后马上分配640字节,这个哈希表的bucket真是无法想象了,所以反正实现的是搜索功能,不妨就用搜索常用的二叉搜索树吧,但频繁的插入节点很明显会使二叉树变得非常不平衡,这样时间复杂度仍然还是O(n),根本发挥不出bst作为搜索的优势,所以干脆就使用不断自身维持平衡的avl树,每一次增加节点就完成一次树形的调整重构,具体的avl树的操作大家可以直接看我的源代码,在这里就不多说啦。

avl树的每一个节点都对应一个链表,这是为了解决像哈希表那样出现的哈希碰撞的问题,两个一样的内存分配需求可以说是很常见的,所以使用一个链表进行同样内存的存储,每一次回收内存,只需要将内存的指针告诉manager就可以,析构后的内存区已经清除掉原来的全部数据,而且内存的大小都是固定的(4字节对齐),完全可以继续给下一个同样大小的对象指针或者引用引用,获取回收内存首先在avl树中快速查找(这下时间复杂度为O(logn),优化太多了),找到对应的节点就直接在链表中进行插入节点,然后指针往内存区的对应区块一指,就完成回收过程了,内存区的分配跟对象池的设计思想是差不多的,都是通过链表来管理内存块,调整自增系数,不连续的链表更能充分利用我们的内存空间,而且使用指针的计算来获取当前块的使用情况,即内存的分配总是从低地址向高地址分配,并以_pAlloateInit 指针来表示当前使用的指针位置,即说明了当前块的下一次分配内存到底要从哪里开始,链表使用空间分配器,每一个节点管理一个内存块,看一下相应的代码:

struct LinkNode{    void* _pMemory;    size_t _mUsedByte;    LinkNode* _next = NULL;    size_t _capacity;    size_t _itemSize;    void* _pAlloateInit = NULL;    LinkNode(size_t size)    {        if (size > MAX_NODE)        {            throw std::invalid_argument("size is too large,no more memory");        }        /*Allocate*/        _pMemory = TAllocator::Allocate(size);        if (_pMemory == NULL) throw std::bad_alloc();        _mUsedByte = 0;        _capacity = size;        _pAlloateInit = _pMemory;    }    ~LinkNode()    {        TAllocator::deAllocate(_pMemory, _capacity);    }};

代码应该算是很简单的,跟对象池的设计思想是差不多的,顺便看下二叉树节点的设计,其实就基本包含上面说的它的功能:

struct TreeNode{    size_t _capacity;    TreeMemListNode* RHeader = NULL;    //回收链表头指针    TreeMemListNode* RTail = NULL;  //尾指针    size_t listNum = 0;    /*回溯节点指针*/    TreeNode* _pThr = NULL;    /*当前树高度*/    int _height;    /*children*/    TreeNode* _LChild = NULL;    TreeNode* _RChild = NULL;    /*析构*/    ~TreeNode()    {        if (RHeader != NULL)        {            TreeMemListNode* pcurr = RHeader;            TreeMemListNode* ppre;            while (pcurr != NULL)            {                ppre = pcurr;                pcurr = pcurr->_pNext;                delete ppre;            }        }    }};

空间配置器可以有效的控制指针的生存销毁,控制对象的析构,代码中多次利用对象的析构函数进行内存的释放,比如上面的就是每一个节点析构时销毁链表,其实这个时候没有必要释放块内存,因为销毁内存池时它自己的析构调用时会触发控件配置器的deAllocate函数进行全部内存的回收,所以在avl树以及链表中只需要将节点的指针的内存释放就可以了,不过最后单例的内存释放还是需要用到RAII技法,通过一个静态对象的析构来进行单例的销毁:

class GCWorker    {    public:        /*析构回收单例*/        ~GCWorker()        {            if (CMemoryPool::mPool != NULL)            {                delete CMemoryPool::mPool;            }        }    };    /*程序结束时析构函数被调用*/    static GCWorker mGcWorker;

该段代码在单例类的内部,是一个私有的内部类,在类销毁时,成员会被回收,mGcWorker的析构函数会被调用,这样我们的单例就玩完咯,还是要注意在析构函数完成对所有内存的回收,内存泄露是一个比较严重的问题。

在链接时使用模板函数会遇到不能解析外部符号的错误,其实这个错误就是说我们在不同编译单元的模板函数是不可以相同相互调用的,因为每一个模板函数的声明定义默认都是inline的,即只在跟头文件的模板引用性声明同一个编译单元,所以单元之间是不可以相互调用的啦,本来不同世界的两个人还是很难相爱的,它的模板引用声明对于被调用时不可见的,然而却在别人的导出符号表中,然后编译器会认为在别的类内部的函数被引用,这就叫做让别人爱上你,你却不肯让别人看到你的内心,真是悲哀。对于单例,不仅在类的内部的进行引用性的声明,还要在全局进行定义式声明,毕竟是静态的,没有一个初始化的意思,链接器是不会放过你的,ok,关于C++的内存优化系列差不多也快结束了,这估计是最后一个比较普适的内存优化接口了,还是欢迎大家进行commit,最后附上程序分配一个4字节对象并进行回收的运行效果图(测试用),本文章到此结束啦~~~~

这里写图片描述

0 0