第1篇:C++ 堆内存管理器-allocator

首先什么是Allocator?Allocator有什么用?

分配器是负责封装堆内存管理的对象,它们在整个标准库中使用,特别是STL容器使用它们来管理r容器内部的所有内存分配,大部份情况下,程序员不用理会,标准容器使用默认的分配器称为std :: allocator,例如当你声明一个简单的vector对象时,C++编译器默认已经使用了内置的std::allocator,在标准库的vector模板当中,第二个模板参数_Alloc就是std::allocator,实际上,std::allocator也是一个类模板

int main(){
    std::vector a;
}
第1篇:C++ 堆内存管理器-allocator_第1张图片
2020-01-12 10-25-43屏幕截图.png

先简要回顾一下历史。Alexander Stepanov(STL之父)提出了分配器的想法,其动机是使容器完全独立于底层内存模型。他打算让分配器完全封装内存模型,但标准委员会发现这很危险,因为这种方法会导致不可接受的效率下降。因此,分配器的当前用途是让程序员控制容器内的内存分配,而不是采用底层硬件的地址模型

本篇从零实现自己的内存分配器,来理解std::allocator的内部运行机制,如果你认为这是重造轮子的话,送你一个字“”,自定义allocator有很多现实的原因。

  • 有些嵌入式平台没有提供默认的malloc/free等底层内存管理函数,你需要继承std::allocator,并封装自定义版本的malloc/free等更底层的堆内存管理函数。
  • 使用C++实现自己的数据结构,有时我们需要扩展(继承)std::allocator。
  • 大部分用C++写的游戏程序都有自己重新实现的allocator。

每个容器实例中都有一个Allocator实例。它向分配器请求存储来存储元素。分配器应具备的基本成员函数如下:

  • T*allocate(size_t n);分配足够的存储空间来存储T的n个实例,并返回指向它的指针
  • void deallocate(T* p, size_t n) 释放分配的内存
  • void construct(T* p, Args ... args);使用p指向的args参数构造一个对象,该接口在C++20中已被移除
  • void destroy(T* p);调用p指向的对象的析构函数,该接口在C++20中已被移除

标准库中的allocator还有一些可选的成员,视不同C++编译器版本而异。

  • 指向类型T的指针,以及指向类型为T的常量指针
  • 类型T的引用,以及类型T的常量引用
  • 自身类型T的别名value_type
  • 能表达allocation模型中最大对象大小的一个无符号整数类型,并且表示在allocation模型中两个指针区别(difference_type)的有符号整数
  • size_type max_size()返回有关类型T的分配内存的最大值。该接口在C++20中已被移除

MyAllocator实现

我们下面从0实现一个符合定义的allocator,首先我们要在调用层代码,在声明一个vector实例,显式传入我们自定义的MyAllocator,并使用他来分配内存

int main(void){
    std::vector> v;
}

下面是我们对MyAllocator的定义,并且定义了标准库规定的所有类型别名,下面的类型别名在标准库的std::allocator_traints中都提供了默认值,因此这里列出所有类型别名仅仅为了完整说明一个allocator的实现过程。

template
class MyAllocator{
public:
      using value_type T;
      using pointer=T*;
      using const_pointer=const T*;
      using void_pointer=void*;
      using const_void_pointer=const void*;
      using size_type size_t;
      using difference_type=std::ptrdiff_t
      
      MyAllocator()=default;
      ~MyAllocator()=default;
};

allocator的堆内存管理接口

我们跟着下来需要实现的是allocate成员函数和dealloc成员函数,整个allocator的堆内存管理都围绕这两个接口展开。我们需要对C++的allocator的堆内存接口调用顺序有个清晰的认识,如下图所示。

第1篇:C++ 堆内存管理器-allocator_第2张图片
allocator的堆内存管理接口

我们在MyAllocator实现的allocate和deallocate函数是必须的,

template
class MyAllocator{
public:
      .....
      pointer allocate(size_type numObjs){
            return static_cast(operator new(sizeof(T)*numObjs));
      }
      
      pointer allocate(size_type numObjs,const_void_pointer hit){
              return allocate(numObjs);
      }
     void deallocate(pointer p,size_type numObjs){
            operator delete(p);
     }
}

我们这里也定义了另一个allocate版本的成员函数,接受一个numObjs参数,并接受一个已分配堆内存的指针,他是一个只想最近分配的元素的指针。可以使用他来改进已分配内存的释放,只是为了提供缓存性能,在我们的示例中,我们会忽略它并立即返回。

其他的成员函数实现

size_type max_size() const{
    return std::numeric_limits::max();
}

对象构造的问题
这是C++20中,被丢弃的内容,但为了完整说明,我在本篇中仍然会提及该内容。

C++20之前的标准库,在allocator中,我们有两个的方法用于构造和销毁对象,一种叫construct,用于在我们分配的内存中构造类型T的实际对象。需要注意的是,当我们调用new操作符分配了原始内存,new操作符并不会执行任何类型T的初始化.

我们看看构造函数是如何工作的,首先,对于allocator下的construct方法,它也是一个类模板的成员函数,如下代码定义,该成员函数接受一个类型U的指针p(已分配了堆内存),construct的运行原理非常简单,他在construct函数内部原地调用类型U的构造函数,并且转发外部的任意参数传递给类型U的构造函数

template
class MyAllocator{
 public:
      //类型别名的代码省略
      //其余代码略

     //构造函数
    template
    void construct(U *p,Args &&...){
          new(p) U(std::forward(args)...);
     }
}

同理的destroy接口实现如下

template
class MyAllocator{
 public:
      //类型别名的代码省略
      //其余代码略

     //构造函数
    template
    void destroy(U *p){
        p->~U();
     }
}

这两个函数默认情况下是完全可选,我这里只是展示如何完整的MyAllocator的自定义实现。

内置绑定机制

上面实现的必选项和可选项的allocator的类成员,都是标准库已经规定的接口名称,没什么好说的。而allocator的难点就是rebind这个内部类成员,而且它也是一个类模板。

rebind内部类成员
根据标准库的定义,rebind被定义为std::allocator类的结构成员; 此结构定义了other成员,该成员定义为专门用于不同参数类型的分配器的实例(other成员定义了可以创建不同类型的对象的分配器类)

class MyAllocator{
public:
    .....
     template
     struct rebind{using other=MyAllocator;}
}

std::MyAllocator模板用于获取某种类型T的对象。 容器内部可能需要分配不同类型的对象。 例如,当您有一个std :: list 时,分配器MyAllocator用于分配类型T的对象,而std :: list 实际上需要分配某个节点类型Node的对象,从模板函数的角度来考虑,我们通常会用另为一个模板参数U来表示 调用节点类型Node,std :: list 需要获得类型U对象的分配器,该分配器使用MyAllocator提供的分配机制。对于程序员在定义自己的数据结构时,需要在自己实现的容器内部定义类似如下的语句

typename MyAllocator::template rebind::other

指定相应的类型。 现在,此声明中有一些语法上令人困惑的地方:

  1. 由于rebind是MyAllocator的成员模板,而MyAllocator是std::list模板参数,因此rebind成为从属名称。 为了表明从属名称是模板,需要在其前面加上template前缀。 如果没有template关键字,则<将被视为小于运算符。
  2. other也取决于模板参数,即它也是一个从属名称。 为了表明从属名称是一种类型,需要typename关键字。

那么,我们在自定义的数据结构当中可能多次用到类U类型的allocator实例,那么我们可以在自定义的数据结构容器类的private作用域定义一个类型别名

typedef typename MyAllocator::template rebind::other  Elem_alloc_type

关于,MyAllocator说到这里应该是比较全面的了,他的全貌大概如下代码,若要确认该自定义的MyAllocator和std::allocator一样能够对所有std的容器起到作用,不妨在增加一些计数器的数据成员

#include 
#include 
#include 
template 
class MyAllocator
{
public:
    //下面是个类型别名,实现中的可选项
    using value_type = T;
    using pointer = T *;
    using const_pointer = const T *;

    using void_pointer = void *;
    using const_void_pointer = const void *;

    using size_type = size_t;
    using difference = std::ptrdiff_t;

    //重新绑定函数
    template 
    struct rebind
    {
        using other = MyAllocator;
    };

    MyAllocator() = default;
    ~MyAllocator() = default;

    //分配内存
    pointer allocate(size_type numObjects)
    {
        allocCount += numObjects;
        std::cout << "MyAllocator::allocate,内存分配:" << numObjects << std::endl;
        return static_cast(operator new(sizeof(T) * numObjects));
    }

    //分配内存
    pointer allocate(size_type numObjects, const_void_pointer hint)
    {

        return allocate(numObjects);
    }

    //释放内存
    void deallocate(pointer p, size_type numObjects)
    {
        std::cout << "MyAllocator::deallocate,内存释放:" << numObjects << std::endl;
        allocCount = allocCount - numObjects;
        operator delete(p);
    }

    //分配器支持最大的内存数
    size_type max_size() const
    {
        return std::numeric_limits::max();
    }

    // //构造对象
    // template 
    // void construct(U *p, Args &&... args)
    // {

    //     new (p) U(std::forward(args)...);
    // }

    // //销毁对象
    // template 
    // void destroy(U *p)
    // {
    //     p->~U();
    // }

    //返回每次分配/删除的内存数
    size_type get_allocations() const
    {
        return allocCount;
    }

private:
    //统计当前内存的使用量
    size_type allocCount;

我们的调用代码基本如下

#include "headers/Allocator.hh"
#include 
#include 
#include 
#include 
#include 

int main()
{
    std::vector> v(0);

    for (size_t i = 0; i < 30; i++)
    {
        sleep(1);
        v.push_back(i);
        std::cout << "当前容器内存占用量:" << v.get_allocator().get_allocations() << std::endl;
    }
}

用vector执行下面的测试如下,附带小福利....

第1篇:C++ 堆内存管理器-allocator_第3张图片
测试效果

小结

这里没有生硬地罗列一堆std::allocator的api,而是采用原生实现的MyAllocator来说明std::allocator的内部机制。

  • 分配器是STL容器的内存分配器。该容器可以将内存分配和取消分配与其元素的初始化和销毁分开。 因此,调用向量vec的vec.reserve(n)仅为至少n个元素分配内存。 每个元素的构造函数都不会执行。因为我们在最终的MyAllocator的construct接口和destoy接口取消了。
  • 我们可以根据需要的容器来调整分配器,例如,您只希望vector内存resize操作的比例
  • new不允许控制调用哪些构造函数,而只能同时构造所有对象。 这是std ::分配器优于new的优势。

你可能感兴趣的:(第1篇:C++ 堆内存管理器-allocator)