文章目录
C++内存池技术分析
为了防止应用程序频繁的使用分配和释放内存,很多程序都会使用内存池技术来预先分配内存,使用内存的时候再从内存池中取出内存。
内存池的好处是优化内存分配,使得内存碎片减少,不过使用起来肯定会增加复杂程度。在C++中有两种办法来提供内存池管理方案:
new
和delete
。std::allocator
。
通过这两种方式,我们可以手动管理内存。
1. new 和 delete
1.1 内存分配的原理
在C++中new和delete不是一个函数,而是一个操作表达式,我们使用的时候如下:
class CData
{
public:
CData(int d) : Data(d) {}
private:
int Data;
};
void Mem()
{
CData* data = new CData(100);
}
反汇编的结果如下:
CData* data = new CData(100);
00E619A7 push 4
00E619A9 call operator new (0E61361h)
00E619AE add esp,4
00E619B1 mov dword ptr [ebp-0ECh],eax
00E619B7 mov dword ptr [ebp-4],0
00E619BE cmp dword ptr [ebp-0ECh],0
00E619C5 je Mem+89h (0E619E9h)
00E619C7 push 4
00E619C9 mov ecx,dword ptr [ebp-0ECh]
00E619CF call CData::__autoclassinit2 (0E61005h)
00E619D4 push 64h
00E619D6 mov ecx,dword ptr [ebp-0ECh]
00E619DC call CData::CData (0E6129Eh)
这里有两个操作:
- 调用
call operator new (0E61361h)
,分配内存函数。 - 调用
call CData::CData (0E6129Eh)
构造函数。
对于operator new (0E61361h)
这个函数,解析接口为operator new(unsigned int)
,这个也就是C++的内存分配函数,这个函数只负责申请函数。
对于申请内存的函数家族,其实存在这一些:
// 这4个版本可能抛出异常
void *operator new(size_t); // 分配一个对象
void *operator new[](size_t); // 分配一个数组
void *operator delete(void*) noexcept; // 释放一个对象
void *operator delete[](void*) noexcept; // 释放一个数组
// 这些版本承若不会抛出异常
void *operator new(size_t, nothorw_t &); // 分配一个对象
void *operator new[](size_t, nothorw_t &); // 分配一个数组
void *operator delete(void*, nothorw_t &) noexcept; // 释放一个对象
void *operator delete[](void*, nothorw_t &) noexcept; // 释放一个数组
1.2 new 和 delete重载
在C++中,我们知道,只要是函数就可以重载,那么new和delete的函数是否也可以重载呢?其实是可以的。我们先看看new和delete函数怎么调用。
void Mem()
{
CData* data = NULL;
data = (CData*)::operator new(sizeof(CData));
::operator delete(data);
}
反汇编的结果如下:
CData* data = NULL;
002318D8 mov dword ptr [data],0
data = (CData*)::operator new(sizeof(CData));
002318DF push 4
002318E1 call operator new (0231361h)
002318E6 add esp,4
002318E9 mov dword ptr [data],eax
::operator delete(data);
002318EC mov eax,dword ptr [data]
002318EF push eax
002318F0 call operator delete (0231159h)
002318F5 add esp,4
可以看到,这里只是分配了内存,但是并没有构造对象。
我们尝试重载new和delete,得到如下:
void* operator new(size_t size)
{
std::cout << "my new" << std::endl;
return malloc(size);
}
void operator delete(void* p)
{
std::cout << "my delete" << std::endl;
::free(p);
}
void Mem()
{
CData* data = NULL;
data = new CData(10);
delete data;
}
此时反汇编结构如下:
CData* data = NULL;
00DE2627 mov dword ptr [data],0
data = new CData(10);
00DE262E push 4
00DE2630 call operator new (0DE135Ch)
00DE2635 add esp,4
00DE2638 mov dword ptr [ebp-0ECh],eax
00DE263E mov dword ptr [ebp-4],0
00DE2645 cmp dword ptr [ebp-0ECh],0
00DE264C je Mem+83h (0DE2663h)
00DE264E push 0Ah
00DE2650 mov ecx,dword ptr [ebp-0ECh]
00DE2656 call CData::CData (0DE12A3h)
00DE265B mov dword ptr [ebp-100h],eax
00DE2661 jmp Mem+8Dh (0DE266Dh)
00DE2663 mov dword ptr [ebp-100h],0
00DE266D mov eax,dword ptr [ebp-100h]
00DE2673 mov dword ptr [ebp-0E0h],eax
00DE2679 mov dword ptr [ebp-4],0FFFFFFFFh
00DE2680 mov ecx,dword ptr [ebp-0E0h]
00DE2686 mov dword ptr [data],ecx
这里依旧调用了构造函数,因为我们这里使用的是new CData(10)
表达式,表达式默认都会调用构造函数。
1.3 定位new
我们一定知道C++的一个规则,那就是:
void* operator new(size_t, void*);
这个函数不能重载。但是为什么呢?这个函数是干什么用的呢?
在C++中,专门提供可一个初始化内存的操作,叫做定位new表达式,语法如下:
new (place_address) type
new (place_address) type(initializer-list)
new (place_address) type[]
new (place_address) type[]{initializer-list}
我们看下定位new表达式的用法:
void Mem()
{
CData* data = NULL;
data = (CData*)operator new(sizeof(CData));
new(data) CData(100);
delete data;
}
这个new(data) CData(100);
操作就是在调用定位new表达式。看下反汇编的代码:
new(data) CData(100);
0041266C mov eax,dword ptr [data]
0041266F push eax
00412670 push 4
00412672 call operator new (0411442h)
00412677 add esp,8
0041267A mov dword ptr [ebp-0D4h],eax
00412680 push 64h
00412682 mov ecx,dword ptr [ebp-0D4h]
00412688 call CData::CData (04112A3h)
定位new多了两个操作:
call operator new (0411442h)
调用call CData::CData (04112A3h)
构造函数调用。
对于operator new (0411442h)
我们可以看下调用的结果为operator new(unsigned int,void *)
,这个就是C++无法充值的函数
void* operator new(size_t, void*);
也就是说无法重载的函数void* operator new(size_t, void*);
其实就是定位new表达式需要调用的函数。
1.4 应用
如果我们在C++程序中存在内存泄露,那么怎么办呢?想法是接管new和delete,跟踪一下那些new没有被释放,并且记录new的信息,可以如下实现:
class CData
{
public:
CData(int d) : Data(d) {}
private:
int Data;
};
void* operator new(size_t size, const char *file, int line)
{
void* p = nullptr;
p = malloc(size);
std::cout << "my new(" << file << "," << p << "," << line << ")" << std::endl;
return p;
}
void operator delete(void* p)
{
std::cout << "my delete(" << p << ")" << std::endl;
::free(p);
}
#define DEBUG_NEW new(__FILE__, __LINE__)
#define new DEBUG_NEW
void Mem()
{
CData* data = NULL;
data = new CData(100);
delete data;
}
执行结果:
my new(c:\users\xzh\source\repos\cplusplus\cplusplus\cplusplus.cpp,015CE340,45)
my delete(015CE340)
1.5 内存池
根据上面描述的知识点我们很容易就实现一个内存池模型,这里不再描述内存池怎么实现,下面看一下一个C++标准库的一个内存池allocator。
2. allocator
这是一个模板类,可以将 内存的分配和释放与对象的构造和析构分离开来,例如如下:
std::allocator<std::string> Allocator;
auto p = Allocator.allocate(n);
2.1 allocator 基本操作
操作 | 解释 |
---|---|
allocator<T> a |
定义了一个名为a的allocator 对象,它可以为类型T的对象分配内存 |
a.allocator(n) |
分配一段原始的、未构造的内存,这段内存能保存n个类型为T的对象 |
a.deallocate(p,n) |
释放T*指针p地址开始的内存,这块内存保存了n个类型为T的对象,p必须是一个先前由allocate 返回的指针,且n必须是p创建时所要求的大小,且在调用该函数之前必须销毁在这片内存上创建的对象。 |
a.construct(p,args) |
p必须是一个类型为T* 的指针,指向一片原始内存,arg将被传递给类型为T的构造函数,用来在p指向的原始内存上构建对象。 |
a.destory(p) |
p为T*类型的指针,用于对p指向的对象执行析构函数. |
2.2 实例
void Mem()
{
std::allocator<int> alloc;
std::allocator<int>::value_type* p = alloc.allocate(5);
alloc.construct(p, 10);
alloc.construct(p + 1, 20);
alloc.construct(p + 2, 30);
alloc.construct(p + 3, 40);
alloc.construct(p + 4, 50);
for (int i = 0; i < 5; ++i)
{
std::cout << p[i] << std::endl;
}
alloc.destroy(p);
alloc.destroy(p);
alloc.destroy(p + 2);
alloc.destroy(p + 3);
alloc.destroy(p + 4);
}
2.3 allocator的实现
知道上面的技术,我们就很容易掌握allocator的实现了,主要原理是分配内存和构造对象分离。
2.3.1 allocate内存分配
这个用来分配内存,实现如下:
_NODISCARD _DECLSPEC_ALLOCATOR _Ty * allocate(_CRT_GUARDOVERFLOW const size_t _Count)
{ // allocate array of _Count elements
return (static_cast<_Ty *>(_Allocate<_New_alignof<_Ty>>(_Get_size_of_n<sizeof(_Ty)>(_Count))));
}
template<size_t _Align,
class _Traits = _Default_allocate_traits,
enable_if_t<(_Align > __STDCPP_DEFAULT_NEW_ALIGNMENT__), int> = 0> inline
_DECLSPEC_ALLOCATOR void *_Allocate(const size_t _Bytes)
{ // allocate _Bytes when _HAS_ALIGNED_NEW && _Align > __STDCPP_DEFAULT_NEW_ALIGNMENT__
if (_Bytes == 0)
{
return (nullptr);
}
size_t _Passed_align = _Align;
return (_Traits::_Allocate_aligned(_Bytes, _Passed_align));
}
_DECLSPEC_ALLOCATOR static void * _Allocate_aligned(const size_t _Bytes, const size_t _Align)
{
return (::operator new(_Bytes, align_val_t{_Align}));
}
这里直接调用::operator new
分配内存,所以这个内存并没有初始化。
2.3.2 construct内存构造
template<class _Objty,
class... _Types>
_CXX17_DEPRECATE_OLD_ALLOCATOR_MEMBERS void construct(_Objty * const _Ptr, _Types&&... _Args)
{ // construct _Objty(_Types...) at _Ptr
::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
_Objty(_STD forward<_Types>(_Args)...);
}
这里使用定位new表达式来构造对象(调用构造函数来构造),参数使用完美转发实现。
2.3.3 destroy内存析构
template<class _Uty>
_CXX17_DEPRECATE_OLD_ALLOCATOR_MEMBERS void destroy(_Uty * const _Ptr)
{ // destroy object at _Ptr
_Ptr->~_Uty();
}
这里直接调用析构函数来析构内存块。
3. 总结
内存池实现的主要技术是:
- 预先分配内存。
- 内存分配和构造与析构分离。
- 单独提供构造的接口。
- 单独提供析构内存块的接口。
来源:CSDN
作者:xiangbaohui
链接:https://blog.csdn.net/xiangbaohui/article/details/103682913