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

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

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

int main(){
    std::vector<int> a;
}
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<int,MyAllocator<int>> v;
}

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

template<class T>
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的堆内存接口调用顺序有个清晰的认识,如下图所示。

allocator的堆内存管理接口

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

template<class T>
class MyAllocator{
public:
      .....
      pointer allocate(size_type numObjs){
            return static_cast<pointer>(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<size_type>::max();
}

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

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

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

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

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

同理的destroy接口实现如下

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

     //构造函数
    template<class U,class...Args>
    void destroy(U *p){
        p->~U();
     }
}

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

内置绑定机制

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

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

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

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

typename MyAllocator::template rebind<U>::other

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

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

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

typedef typename MyAllocator::template rebind<U>::other  Elem_alloc_type

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

#include <iostream>
#include <limits>
#include <stdlib.h>
template <class T>
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 <class U>
    struct rebind
    {
        using other = MyAllocator<U>;
    };

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

    //分配内存
    pointer allocate(size_type numObjects)
    {
        allocCount += numObjects;
        std::cout << "MyAllocator::allocate,内存分配:" << numObjects << std::endl;
        return static_cast<pointer>(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<size_type>::max();
    }

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

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

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

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

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

我们的调用代码基本如下

#include "headers/Allocator.hh"
#include <cinttypes>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <vector>

int main()
{
    std::vector<int, MyAllocator<int>> 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执行下面的测试如下,附带小福利....

测试效果

小结

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

  • 分配器是STL容器的内存分配器。该容器可以将内存分配和取消分配与其元素的初始化和销毁分开。 因此,调用向量vec的vec.reserve(n)仅为至少n个元素分配内存。 每个元素的构造函数都不会执行。因为我们在最终的MyAllocator的construct接口和destoy接口取消了。
  • 我们可以根据需要的容器来调整分配器,例如,您只希望vector内存resize操作的比例
  • new不允许控制调用哪些构造函数,而只能同时构造所有对象。 这是std ::分配器优于new的优势。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容