手撸一个内存分配器

科技   2024-09-17 11:32   浙江  

C++ 以其强大的标准库及其对内存管理的控制而闻名。为您提供细粒度控制的工具之一是std::allocator。在很多的时候,开发人员依赖默认分配器,但在某些情况下,自定义分配器可以显著提高性能或满足特定需求,或者通过实现自定义分配器来加深对这块的理解。

今天,来做一个手动挡,手撸一个STL中常见的内存分配器Allocator,文章内容将从一个简单的实力开始,然后逐步进行完善,最后与vector集成。

前序

可能有人会问,标准里面不是已经实现了分配器么,为什么还要自己手撸一个?

俗话说,存在即合理,既然标准开了这个口子,允许使用自定义的内存分配器,那就有其一定的道理,一般情况下,使用自定义分配器的原因无非是以下几个原因:

性能优化:通过根据特定模式定制内存分配,可以减少碎片并改善缓存局部性。内存池:分配器可以管理内存池,以便快速分配和释放调试和分析:自定义分配器可以帮助跟踪内存使用情况并检测泄漏或损坏专用硬件:分配器可以设计为与非标准内存交互,例如GPU内存或共享内存区域。

emm,还有一个原因,就是可以锻炼自己的编码能力,加深对标准的理解。好了,有些事想做的话,一个理由就可以,不想做的话,有千万个理由,手撸系列类似~

好了,开始吧!

实现自定义分配器

在本文中,我将着手一个分配器的实现,因此,内容将涉及到以下几个方面:

定义类:通过定义其成员类型和方法来实现分配器处理内存分配和释放:根据需要定制allocate和``deallocate`方法来管理内存与数据结构集成:在本文中,将其用在我们常见的std::vector中正确性测试:确保我们实现的分配器在使用结果上与预期一致

在开始实现我们的自定义分配器之前,需要先了解下标准中用到了哪些方法,比如allocate、deallocate,construct以及destroy,了解这些内容对我们来说非常重要,因为在使用中,我们往往提供一个类,剩下的则交给container来使用了,而container在使用中则用到allocate等操作:

allocate:目的:用以分配内存调用方:标准库中的容器,当插入元素或者需要为元素重新分配内存时候声明:T* allocate(std::size_t n)deallocate:目的:用以释放又allocate函数分配的内存调用方:标准库中的容器,在销毁容器或者释放元素的时候声明:void deallocate(T* p, std::size_t n)construct:目的:用以为分配的内存调用容器中存储元素的构造函数调用方:在为元素创建内存的时候声明:template<typename T, typename... Args> void construct( T*  t, Args&&... args)destroy目的:用以释放对象调用方:释放对方的时候用于释放其所占用的资源声明:template void destroy(T* t)

定义分配类

在实现一个面向对象功能的时候,我们要做的第一步就是实现一个基础类,把框架先搭起来,然后根据具体的功能要求,再进行添加。

#include <memory>
#include <iostream>
#include <vector>

template<typename T>
classSimpleAllocator{
public:
using value_type = T;
SimpleAllocator()=default;
template<typename U>
    constexpr SimpleAllocator(const SimpleAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
if(n > std::allocator_traits<SimpleAllocator>::max_size(*this)){
throw std::bad_alloc();
}
returnstatic_cast<T*>(::operatornew(n *sizeof(T)));
}

void deallocate(T* p, std::size_tnoexcept {
::operator delete(p);
}
};

在上面这个基础类中:

value_type是数据类型,即我们这个分配器所要分配内存的类型,类似于std::vectorstd::string中的std::string构造函数和拷贝构造函数,其中,拷贝构造函数是一个模板成员函数,用于在不同的数据数据类型之间进行分配allocate和deallocate,分配调用了operator new 和 operator delete操作符,用于管理内存

特殊成员函数

接下来,我们要新增特殊的成员函数,比如construct和destroy,用以构造和释放对象:

template<typename T>
classSimpleAllocator{
public:
using value_type = T;

SimpleAllocator()=default;

template<typename U>
    constexpr SimpleAllocator(const SimpleAllocator<U>&) noexcept {}

T* allocate(std::size_t n) {
if(n > std::allocator_traits<SimpleAllocator>::max_size(*this)){
throw std::bad_alloc();
}
returnstatic_cast<T*>(::operatornew(n *sizeof(T)));
}

void deallocate(T* p, std::size_tnoexcept {
::operator delete(p);
}

template<typename U, typename... Args>
    void construct(U* p, Args&&... args) {
new(p)U(std::forward<Args>(args)...);
}

template<typename U>
    void destroy(U* p) noexcept {
        p->~U();
}
};

其中,construct用以在分配内存后,通过operator new调用元素的构造函数,而destroy则用以调用元素的析构函数。

比较函数

因为允许容器进行比较是否相等,因此分配器也需要支持比较函数:

template<typename T>
classSimpleAllocator{
public:
using value_type = T;

SimpleAllocator()=default;

template<typename U>
    constexpr SimpleAllocator(const SimpleAllocator<U>&) noexcept {}

T* allocate(std::size_t n) {
if(n > std::allocator_traits<SimpleAllocator>::max_size(*this)){
throw std::bad_alloc();
}
returnstatic_cast<T*>(::operatornew(n *sizeof(T)));
}

void deallocate(T* p, std::size_tnoexcept {
::operator delete(p);
}

template<typename U, typename... Args>
    void construct(U* p, Args&&... args) {
new(p)U(std::forward<Args>(args)...);
}

template<typename U>
    void destroy(U* p) noexcept {
        p->~U();
}

friendbooloperator==(constSimpleAllocator&,constSimpleAllocator&){returntrue;}
friendbooloperator!=(constSimpleAllocator&,constSimpleAllocator&){returnfalse;}
};

在上述实现中,新增了两个函数分别是operator=operator!=,其被声明为friend,用以比较两个分配器是否相同。

集成

前面把分配器的基本功能实现了,现在,我们就借助一个例子,将前面实现的分配器和现有的容器集成起来,看看运行是否正常:

int main() {
SimpleAllocator<int> alloc;
    std::vector<int,SimpleAllocator<int>>vec(alloc);
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
for(int i : vec){
        std::cout << i <<" ";
}
    std::cout << std::endl;
return0;
}

emm,不出所料的话,输出为:

Vector contents: 1 2 3 

结语

本文中,我们首先从宏观角度讲解了下标准库中的内存分配器,为了加深对这块的了解 ,我们从上到下,从简到全,实现了一个简单的内存分配器,并结合STL中的std::vector进行了使用。需要注意的是,本文只是一个基本简单的不能再简单的分配器,如果想在业务上进行使用,可能需要做更多的开发和扩展,虽然自定义分配器在某些场景上性能会较标准库高,但它们也伴随着一定的风险和注意事项:

复杂性:实现自定义分配器会增加代码库的复杂性,使其更难维护和调试兼容性:确保自定义分配器遵守标准库的要求,以避免与不同容器的兼容性问题内存泄漏:分配器实现不当可能会导致内存泄漏或损坏,因此彻底的测试和验证至关重要性能:虽然自定义分配器可以提高性能,但如果没有针对特定用例正确设计,它们也会降低性能,因此,分析和基准测试对于验证其优势至关重要

推荐阅读  点击标题可跳转

1、巧活用折叠表达式

2、嵌入式 C 语言,那些“花里胡哨”的语法特性。

3、C 语言老将从中作梗,Rust for Linux 项目内讧升级!核心维护者愤然离职:不受尊重、热情被消耗光

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章