简单内存池的实现

简单内存池的实现

在实际项目中,很多地方都存在IO。

而IO是很耗时间的,比方说:new、malloc。

对于现代的计算机来说,动辄就是GHz量级的主频,似乎感觉不到影响。但是在大型的项目里面,反复的new和delete,不仅会耗大量的时间,还会造成内存碎片。

 

new是一个很麻烦的东西。

有new的地方,就是你可能栽坑的地方。

new出的内存,没有被正确释放掉,会造成内存泄漏。微不足道的内存泄漏,在经过长时间积累,也会让你的程序崩溃。

学会内存管理,是C++程序员必须具备的能力。

 

接下来,我在这里实现一个简单的内存池。

这个内存池并不能与STL的内存池媲美。STL的内存池的实现更复杂,当然,它完成的功能也更强大。但是其实质是相同的。这个简单的内存池会使用到模版。

需要的知识点:

 

    1. C++基本语法:reinterpret_cast<expr>(para)
    2. 数据结构:链表
    3. C++泛型编程:模板
    4. 操作系统:临界区、锁
    5. placement new (这个需要自己去看 C++ Primer)

 

好了,现在开始:

 

  • 实践基础要点之一:reinterpret_cast<expr>(para)

    看reinterpret_cast<expr>(para)的字面意思:重新解释。好深奥的词,怎么解释?《C++ Primer》一书中,对此的解释也甚少。

我之前也是不懂的,但是后来看多了,有点感觉。我的理解是:用新的expr结构,重新解释。

在下面的代码中,我们将看到 

1
pHead = *( reinterpret_cast <T**>(pHead))

事实上,pHead 是T* 类型的。用reinterpret_castT* 结构的pHead,强制转换成T**结构,然后对T**结构进行*操作符。怎么样,很暴力吧。之后结合代码再解释。

 

  • 实践基础要点之二:数据结构:链表

    对于所有的程序员来说,熟悉基本的数据结构必须是必备的技能。常听大牛们讲:基础决定了你能爬多高。想想也是,万丈高楼平地起嘛。我大二学习数据结构的时候,完全不知道老师在讲台上一个人唾沫横飞有什么意思,完全听不懂他在讲什么。到了考试临近的时候,拼命啃书,慢慢才对数据结构讲的是什么有些眉目。好了,回到正题。

所谓链表,就是有一个头结点H,该节点指向它的下一个节点NodeA,而NodeA又指向自己的下一个节点NodeB……一直到最后的节点指向NULL。这样,就连成了串,俗称链表。

举例说吧:

1
2
3
4
5
6
7
8
9
typedef  struct  Node
{
     int  _content;
     Node* next;
}
  
Node* pHead;  // define a head of a LinkList
Node* pNode;  // define another node
pHead->next = pNode;  // the head point to another node.

上面的代码只是简单的示例,省略了初始化部分,实际操作时候切不可这样写。

 

  • 实践基础要点之三:C++泛型编程:模板

    以模板的基础的应用的典型例子就是STL了,因为如此,我们才能方便的使用vector、list、map等容器。STL已经成为了C++重要的库之一了,在工程上运用得很多。这种无视类型的定义,增强了代码可复用性。在网络编程中,模版的作用必不可少噢。

模板在《C++ Primer》一书中,做了非常详细的讲解。模板函数,模板类。不了解的请仔细阅读该书的泛型编程部分。

 

  • 实践基础要点之四:操作系统:临界区、锁

    现代计算机的硬件已经很牛逼了,多核,多线程。为了充分发挥处理器的作用,在实际工程中,多线程技术已经相当成熟。但是,在多线程操作的时候,线程A对某共享数据进行写操作,而同时线程B也要多相同的共享数据进行写操作,如果没有控制好,共享数据就成了脏数据。这时候该怎么办。解决办法很多种,锁,互斥变量,信号量,临界区。在Windows下,系统提供了一套api,来处理这些。我们需要用到

1
2
3
4
InitializeCriticalSection(CRITICAL_SECTION*);    // 初始化临界区
DeleteCriticalSection(CRITICAL_SECTION*);       // 释放临界区
EnterCriticalSection(CRITICAL_SECTION*);       // 进入临界区
LeaveCriticalSection(CRITICAL_SECTION*);       // 离开临界区

其中: CRITICAL_SECTION是Windows所定义的临界区结构。我们不用去深究,只要明白它的作用就OK。

整个处理共享区域的流程,应该像这样:

共享数据Data在初始化时候,就声明并初始化一个临界区结构,在线程A对共享数据Data操作时候,让Data进入临界区域,然后操作。这样,线程B就无法对Data进行写操作了。操作完成,让Data离开临界区域。线程B这个时候才可以对Data进行写操作。在Data析构的时候,释放临界区域。

 

介绍完这些知识点,下面是应该上代码的时候了。在命名上,我们把临界区取个别名:锁。

  • 定义临界区(因为要使用到Windows的API函数,记得包含Windows.h)

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
class  CBaseLock
{
public :
     CBaseLock()
     {
         InitializeCriticalSection(&m_sect);
     }
 
     ~CBaseLock()
     {
         DeleteCriticalSection(&m_sect);
     }
 
     void  Lock()
     {
         EnterCriticalSection(&m_sect);
     }
 
     void  Unlock()
     {
         LeaveCriticalSection(&m_sect);
     }
 
private :
     CRITICAL_SECTION m_sect;
};
  • 定义锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template < class  T>
class  CLockImpl
{
public :
     CLockImpl(T* l) : m_lock(l)
     {
         m_lock->Lock();
     }
 
     ~CLockImpl()
     {
         m_lock->Unlock();
     }
 
private :
     T* m_lock;
};
  • 定义内存池

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
template < class  T,  class  LockT = CBaseLock>
class  MyPool
{
public :
     MyPool()
     {
 
         AllocatedCount = 0;
 
         ElemSize =  sizeof (T) >  sizeof (T*) ?  sizeof (T) :  sizeof (T*);
 
         pHead = NULL;
     }
 
     ~MyPool()
     {
         while (pHead)
         {
             T* ret = pHead;
             pHead = *( reinterpret_cast <T**>(pHead));
             :: free (ret);
         }
     }
 
     int  GetCount() const
     {
         return  numAllocated;
     }
 
     T *alloc()
     {
         CLockImpl<LockMode> lock(&m_lock);
 
         numAllocated++;
 
         if (pHead == NULL)
             return  new ( malloc (ElemSize))T;
 
         T* ret = freeListHead;
         pHead = *( reinterpret_cast <T**>(pHead));
 
         return  new (ret)T;
     }
 
     template < class  T1>
     T *alloc( const  T1& p)
     {
         CLockImpl<LockMode> lock(&m_lock);
 
         numAllocated++;
 
         if (pHead == NULL)
             return  new ( malloc (ElemSize))T(p);
 
         T* ret = pHead;
         pHead = *( reinterpret_cast <T**>(pHead));
 
         return  new (ret)T(p);
     }
 
     template < class  T1,  class  T2>
 
     template < class  T1,  class  T2,  class  T3>
 
     …… //这些情况我就不写了
     void  dealloc(T* elem)
     {
         elem->~T();
 
         if ( true )
         {
             CLockImpl<LockMode> lock(&m_lock);
             memset (elem, 0xfe, elementSize);
             --AllocatedCount;
             *( reinterpret_cast <T**>(elem)) = pHead;
             pHead = elem;
         }
     }
 
private :
     int  AllocatedCount;  // 已经分配的内存节点个数
     size_t  ElemSize;   // 内存节点的大小
     T* pHead;  // 空闲内存的链表表头
     LockMode m_lock;
}

 

以上代码,基本完成了一个简单的内存池。

下面,对内存池中的一些重点语句进行解释。

  • 构造函数

内存池初始化出来,是什么都没有的。因此,指向空闲内存区域的指针应指向NULL。而已分配的内存节点个数,当然必须是0。最最富有深意的,就应该是这句:

1
ElemSize =  sizeof (T) >  sizeof (T*) ?  sizeof (T) :  sizeof (T*);

    其含义是,ElemSize的大小,应该是木板参数T与T*二者中较大的一个。(为什么要这么设计?往下看。)

  • public 接口 alloc()

对于内存池中的元素对象,若需要多个参数来初始化,就需要传入多个参数。这样,alloc()接口显然要满足多种情况,就需要把alloc()定义为模板函数,提供对应构造形参。而形参在内存池中,用模板参数指定,这样,调用alloc(),传入实参,就可以初始化元素,并为之分配内存了。

  • 仔细看alloc()的实现,发现pHead指向空的时候,需要new一个ElemSize大小的空间。而pHead不为空的时候,就把pHead指向的空间返回。

1
pHead = *( reinterpret_cast <T**>(pHead));

    pHead指向T*类型,然后把pHead用reinterpret_cast<T**>重新解释,这个时候,pHead指向的地址的空间(逻辑)结构就改变了。假设pHead指向ElemA类型,       sizeof(ElemA) = 5,用reinterpret_cast<ElemA**>重新解释,pHead指向就是指向指针的指针了。然后*reinterpret_cast<ElemA**>就是向重新解释的地址区域取     值(取的是什么?继续往下看)。

  • 析构

    析构的时候,有这么一句:

    1
    *( reinterpret_cast <T**>(elem)) = pHead;
  • 按照之前的解释,是把elem的地址用T**重新解释,然后取值,把pHead指向的地址放到该处。而pHead指向的地址,一定是比T和T*中,较大的一个的。32位操作系统下,T*是4字节,这说明pHead指向的地址,至少是4字节,至少足够容纳一个T**的类型。也就是说,我把elem这块内存的内容删除了,然后把这块地址分成若干4字节(32位系统下)的连续块,把pHead指向的第一个空闲地址块的地址放倒这块抵制块的第一个4字节中保存,然后pHead = elem; pHead指向elem的地址,则第一个空闲地址快就是刚刚释放了的elem的地址,elem地址中的第一个4字节保存了下一个空闲地址块的地址……如此链成链表。

  • 然后又回到alloc上,alloc先检查有没有空闲地址,即pHead是否指向NULL,如果指向NULL,则new一块ElemSize大小的内存,在该内存中,用placement new技术,在申请的内存中,构造需要的元素对象。若pHead不指向NULL,则说明已经有申请好的内存(可能是某个对象析构之后,留下来,没有返还给系统的),取到pHead指向的第一块空闲内存块的地址,然后然pHead指向这块空闲地址质指向的空闲地址块(即 pHead = *(reinterpret_cast<T**>(pHead));这句的含义),然后构造元素对象。这样减少了想系统new的次数,节省了系统寻找合适大小内存块的时间,提高了效率。

加锁的原因,是因为防止多个线程,同时对某一对象内存池进行操作。

这样把简单的内存池解释了一遍。内存池的运行机制,大致是这样的。有些内存池会比这个内存池复杂得多。但是,理解了其中分配的要点,什么内存池都能玩得游刃有余。

 

  • 总结:

    内存池的存在,减少了系统IO次数,缩减了系统查找合适大小内存块的时间。提高了程序的运行效率,并有效减少了内存碎片的产生。

 

=====>THE END<=====

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内存池是一种常见的内存管理技术,它可以在程序启动时预先分配一定数量的内存空间,并将其划分为多个固定大小的块,然后在程序运行过程中动态地将这些块分配给需要使用内存的对象,从而减少内存碎片和内存分配的时间开销。下面是一个简单内存池实现示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define BLOCK_SIZE 1024 #define BLOCK_NUM 10 typedef struct _memory_block { void *start; void *end; struct _memory_block *next; } memory_block; typedef struct _memory_pool { size_t block_size; memory_block *free_list; memory_block *used_list; } memory_pool; memory_pool *memory_pool_create(size_t block_size) { memory_pool *pool = (memory_pool *) malloc(sizeof(memory_pool)); pool->block_size = block_size; pool->free_list = NULL; pool->used_list = NULL; for (int i = 0; i < BLOCK_NUM; i++) { memory_block *block = (memory_block *) malloc(sizeof(memory_block)); block->start = malloc(block_size); block->end = (char *) block->start + block_size; block->next = pool->free_list; pool->free_list = block; } return pool; } void *memory_pool_alloc(memory_pool *pool, size_t size) { memory_block *block = pool->free_list; while (block) { if ((char *) block->end - (char *) block->start >= size) { void *ptr = block->start; block->start = (char *) block->start + size; if (block->start == block->end) { pool->free_list = block->next; block->next = pool->used_list; pool->used_list = block; } return ptr; } block = block->next; } return NULL; } void memory_pool_free(memory_pool *pool) { memory_block *block = pool->used_list; while (block) { memory_block *next = block->next; free(block->start); free(block); block = next; } block = pool->free_list; while (block) { memory_block *next = block->next; free(block->start); free(block); block = next; } free(pool); } int main() { memory_pool *pool = memory_pool_create(BLOCK_SIZE); char *str1 = (char *) memory_pool_alloc(pool, 10); char *str2 = (char *) memory_pool_alloc(pool, 20); char *str3 = (char *) memory_pool_alloc(pool, 30); strcpy(str1, "hello"); strcpy(str2, "world"); strcpy(str3, "memory pool"); printf("%s %s %s\n", str1, str2, str3); memory_pool_free(pool); return 0; } ``` 该示例中,首先定义了两个结构体:memory_block表示内存块,包括起始地址、结束地址和下一个内存块的指针;memory_pool表示内存池,包括块大小、空闲链表和已用链表。 然后,定义了三个函数:memory_pool_create用于创建内存池,先分配一定数量的内存块,并将其加入空闲链表;memory_pool_alloc用于从内存池中分配一块指定大小的内存空间,遍历空闲链表,找到第一个大小足够的内存块,并将其划分为新的内存空间;memory_pool_free用于释放内存池中的所有内存块,将已用链表和空闲链表中的内存块全部释放。 最后,在main函数中创建一个内存池,并使用memory_pool_alloc从内存池中分配三个字符串空间,将其赋值并打印出来,最后使用memory_pool_free释放内存池中的所有内存块。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值