C++ 标准模板库(STL)——空间分配器(allocator)

分配器allocator

#include <memory>//用于标准库中的STL containers

1、背景

分配器顾名思义,分配空间内存。
在C++中,分配单个对象时,通常希望将内存分配和对象初始化组合在一起。一个对象的内存配置和释放(new、delete)一般都包含以下步骤:

  • new(new[]),首先是调用operator new(new[])来配置内存,然后调用对象的类的构造函数进行初始化;内存分配和对象构造组合在了一起
  • delete(delete[]),首先是调用析构函数,然后调用 operator delete(new[])进行释放。对象析构和内存释放组合在了一起。

当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象的创建操作(同时付出一定开销)。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。

举例:

auto p = new string[100];
for (int i = 0; i < 5; ++i){
	p[i] = "balaba...";
}

只需要5个string,而new把100个对象全部构造好了(每个string已经被初始化为空字符串,也就是""),也就是前面将p[0-4]赋值为空字符串的操作,变得毫无意义。

2、定义

  • 标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
  • 作用相当于operator new 和operator delete的功能。
  • allocator是一个类,有着叫allocate()和deallocate()成员函数(相当于malloc和free)。它还有用于维护所分配的内存的辅助函数和指示如何使用这些内存的typedef(指针或引用类型的名字)。如果标准容器中最后一个模板参数没有指定,那么就是allocator默认参数
    在这里插入图片描述

3、 最简单的实现

程序实现

template<class T>
class allocator
{
public:
    // 1、为什么需要下面这些成员,有什么作用呢?
    typedef T          value_type;
    typedef T*         pointer;
    typedef const T*   const_pointer;
    typedef T&         reference;
    typedef const T&   const_reference;
    typedef size_t     size_type;       // size_t是无符号整数
    // ptrdiff_t是有符号整数,代表指针相减结果的类型
    typedef ptrdiff_t  difference_type;
 
    // 2、这是做什么用的,为何template是U,而不是与allocator的T一致?
    template<class U>
    struct rebind
    {
        typedef allocator<U> other;
    };
 
    // 默认构造函数,什么都不做
    allocator() noexcept
    {
    }
 
    // 泛化的构造函数,也什么都不做
    // 3、为什么需要这个泛化的构造函数,不同类型的allocator复制合适吗?
    template<class U>
    allocator(const allocator<U>&) noexcept
    {
    }
 
    // 析构函数,什么都不做
    ~allocator() noexcept
    {
    }
 
    // 返回对象地址
    pointer address(reference val) const noexcept
    {
        //non-const版调用const版,参见《Effective C++》条款3
        return const_cast<reference>(address(static_cast<const_reference>(val)));
    }
 
    // 返回对象地址
     //    reference => T&          const_reference => const T&
    const_pointer address(const_reference val) const noexcept
    {
        return &val;
    }
 
    // 申请内存,count是指对象个数,不是字节数。
    // 4、hint是做什么的?
    pointer allocate(size_type count, allocator<void>::const_pointer hint = nullptr)
    {
        return static_cast<pointer>(::operator new(count * sizeof(value_type)));
    }
 
    // 释放内存
    void deallocate(pointer ptr, size_type count)
    {
        ::operator delete(ptr);
    }
 
    // 可配置的最大量(指对象个数,不是字节数)
    size_type max_size() const noexcept
    {
        return (static_cast<size_type>(-1) / sizeof(value_type));
    }
 
    // 构造对象,Args是模板参数包,见《C++ Primer》第5版16.4节
    template <class U, class... Args>
    void construct(U* p, Args&&... args)
    {
        ::new ((void *)p) U(::std::forward<Args>(args)...);
    }
 
    // 析构对象
    template <class U>
    void destroy(U* p)
    {
        p->~U(); // 原来模板还可以这样用
    }
};
 
// 5、为什么要对void进行特化?
template<>
class allocator<void>
{
public:
    typedef void value_type;
    typedef void *pointer;
    typedef const void *const_pointer;
    template <class U> struct rebind
    {
        typedef allocator<U> other;
    };
};

问题解答

  • 1、STL的规范,同时这些type在迭代器和traits技术中有用。
  • 2、摘自MSDN:
    • A structure that enables an allocator for objects of one type to allocate storage for objects of another type. (一种结构,它使一种类型的对象的分配器能够为另一种类型的对象分配存储。)
    • This structure is useful for allocating memory for type that differs from the element type of the container being implemented.(此结构对于为与所实现的容器的元素类型不同的类型分配内存很有用。)
    • The member template class defines the type other. Its sole purpose is to provide the type name allocator<_Other>, given the type name allocator.(成员模板类定义其他类型。给定类型名称allocator ,其唯一目的提供类型名称allocator <_Other>。)
For example, given an allocator object al of type A, you can allocate an object of type _Other with the expression:
(例如,给定类型A的分配器对象al,您可以使用以下表达式分配类型为_Other的对象:)
A::rebind<Other>::other(al).allocate(1, (Other *)0)

Or, you can name its pointer type by writing the type:
(可以通过编写以下类型来命名其指针类型:)

A::rebind<Other>::other::pointer

具体例子:一个保存int的列表list,列表存储的对象并不是int本身,而是一个数据结构,它保存了int并且还包含指向前后元素的指针。那么,list<int, allocator>如何知道分配这个内部数据结构呢?毕竟allocator只知道分配int类型的空间。这就是rebind要解决的问题。通过allocator::rebind<_Node>()你就可以创建出用于分配_Node类型空间的分配器了。

  • 3、allocator类的模板参数只有一个,代表分配的元素类型,如果allocator封装的仅是内存的分配策略而与元素类型无关,定义泛型复制构造好像没什么不合理,同时如果不定义成泛型rebind将无法使用。construct成员函数和destroy成员函数也是泛型,allocator的使用条件还是特别宽松的。
  • 4、hint:
    //hint:0 or 通过另外一个allocate获得的非零值且没有使用deallocate释放
    //当 hint != 0 时,这个值可以作为一个提示,通过分配接近指定的新存储块来提高性能。相邻元素地址是一个不错的选择
  • 5、只有void *变量,没有void变量,没有void&变量,不能typedef void value_type等等。

4、使用步骤

  • 1、allocator与类绑定,因为allocator是一个泛型类
  • 2、allocate()申请指定大小空间
    尝试分配n个T类型的存储空间,然后返回第一个元素的起始地址,只是分配空间,不构造对象
pointer allocate(size_type n, allocator<void>::const_pointer hint = 0);

//pointer => T* const_pointer => const T* size_type => size_t

在标准默认allocator,存储块是使用 一次或多次 ::operator new 进行分配,如果他不能分配请求的存储空间,则抛出bad_alloc异常

  • 3、construct()构建对象,其参数为可变参数,所以可以选择匹配的构造函数
template <class U, class... Args>
 void construct(U* p, Args&&... args);
    • 在p指向的位置构建对象U,此时该函数不分配空间,pointer p是allocate分配后的起始地址

constructor将其参数转发给相应的构造函数构造U类型的对象,相当于 ::new ((void*) p) U(forward<Args> (args)...)

  • 4、使用,与其它指针使用无异
  • 5、destroy()析构对象,此时空间还是可以使用
    销毁p指向的对象,但是不会释放空间,也就意味着,这段空间依然可以使用
 template <class U>void destroy (U* p);

该函数使用U的析构函数,就像使用下面的代码一样:P->〜U();

  • 6、deallocate()回收空间
    释放先前allocate分配的且没有被释放的存储空间
void deallocate(pointer p, size_t n);
    • p:指向以前使用allocator :: allocate分配的存储块的指针。
    • n:在调用allocator :: allocate时为这个存储块分配的元素数量。
      在默认的allocator中,使用 ::operator delete进行释放
      在这里插入图片描述

5、应用举例

//https://www.cnblogs.com/SimonKly/p/7819122.html
//#include "CAnimal.h"
#include <memory>
#include <iostream>

using namespace std;

class Animal
{
public:
#if 1        //即使为0,没有默认构造也是可以,
    Animal() : num(0)
    {
        cout << "Animal constructor default" << endl;
    }
#endif
    Animal(int _num) : num(_num)
    {
        cout << "Animal constructor param" << endl;
    }

    ~Animal()
    {
        cout << "Animal destructor" << endl;
    }

    void show()
    {
        cout << this->num << endl;
    }

private:
    int num;
};

int main()
{
    allocator<Animal> alloc;        //1.
    Animal *a = alloc.allocate(5);    //2.

    //3.
    alloc.construct(a, 1);
    alloc.construct(a + 1);
    alloc.construct(a + 2, 3);
    alloc.construct(a + 3);
    alloc.construct(a + 4, 5);

    //4.
    a->show();
    (a + 1)->show();
    (a + 2)->show();
    (a + 3)->show();
    (a + 4)->show();

    //5.
    for (int i = 0; i < 5; i++)
    {
        alloc.destroy(a + i);
    }
    //对象销毁之后还可以继续构建,因为构建和内存的分配是分离的
    //6.
    alloc.deallocate(a, 5);

    cin.get();
    return 0;
}

参考

1、https://www.cnblogs.com/lsgxeva/p/7689989.html
2、https://blog.csdn.net/qingdujun/article/details/85224771
3、https://blog.csdn.net/wallwind/article/details/22176291
4、https://blog.csdn.net/xiyanggudao/article/details/51543839
5、https://www.cnblogs.com/SimonKly/p/7819122.html
6、《STL源码剖析》

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值