分配器是C++标准库的重要组成部分,负责管理STL容器在运行时的动态内存分配与释放,默认使用通用分配器并允许定制化实现。该概念由亚历山大·斯特潘诺夫在1994年设计标准模板库(STL)时提出,旨在解耦容器与底层存储模型,后纳入C++标准时因性能考量缩减了数据模型的抽象化程度 [1]。
斯特潘诺夫与比雅尼·斯特劳斯特鲁普等人重构了容器接口以适配分配器,其核心功能包括定义内存指针类型、提供分配/释放接口及rebind机制 [1]。C++11标准引入作用域分配器并放宽对状态分配器的限制,使其支持共享内存管理 [3-4]。自定义分配器常用于优化内存池性能、管理特殊内存区域及调试内存错误,需实现构造、析构等基础功能 [1-2] [7]。内存分配器的实现策略包括池分配器、哈希映射FreeList池等,通过减少系统调用和内存碎片提升分配效率
简介
播报
编辑
在C++编程中,分配器(英语:allocator)是C++标准库的重要组成部分。C++的库中定义了多种被统称为“容器”的数据结构(如链表、集合等),这些容器的共同特征之一,就是其大小可以在程序的运行时改变;为了实现这一点,进行动态内存分配就显得尤为必要,在此分配器就用于处理容器对内存的分配与释放请求。换句话说,分配器用于封装STL容器在内存管理上的低层细节。默认情况下,C++标准库使用其自带的通用分配器,但根据具体需要,程序员也可自行定制分配器以替代之。
分配器最早由亚历山大·斯特潘诺夫作为C++标准模板库(Standard Template Library,简称STL)的一部分发明,其初衷是创造一种能“使库更加灵活,并能独立于底层数据模型的方法”,并允许程序员在库中利用自定义的指针和引用类型;但在将标准模板库纳入C++标准时,C++标准委员会意识到对数据模型的完全抽象化处理会带来不可接受的性能损耗,为作折中,标准中对分配器的限制变得更加严格,而有鉴于此,与斯特潘诺夫原先的设想相比,现有标准所描述的分配器可定制程度已大大受限。
虽然分配器的定制有所限制,但在许多情况下,仍需要用到自定义的分配器,而这一般是为封装对不同类型内存空间(如共享内存与已回收内存)的访问方式,或在使用内存池进行内存分配时提高性能而为。除此以外,从内存占用和运行时间的角度看,在频繁进行少量内存分配的程序中,若引入为之专门定制的分配器,也会获益良多。
使用需求
播报
编辑
任意满足分配器使用需求的C++类都可作分配器使用。具体来说,当一个类(在此设为类A)有为一个特定类型(在此设为类型T)的对象分配内存的能力时,该类就必须提供以下类型的定义:
A::pointer指针
A::const_pointer常量指针
A::reference引用
A::const_reference常量引用
A::value_type值类型
A::size_type所用内存大小的类型,表示类A所定义的分配模型中的单个对象最大尺寸的无符号整型
A::difference_type指针差值的类型,为带符号整型,用于表示分配模型内的两个指针的差异值。
如此才能以通用的方式声明对象与对该类对象的引用T。allocator提供这些指针或引用的类型定义的初衷,是隐蔽指针或引用的物理实现细节;因为在16位编程时代,远指针(far pointer)是与普通指针非常不同的,allocator可以定义一些结构来表示这些指针或引用,而容器类用户不需要了解其是如何实现的。
虽然按照标准,在库的实现过程中允许假定分配器(类)A的A::pointer(指针)与A::const_pointer(常量指针)即是对T*与T const*的简单的类型定义,但一般更鼓励支持通用分配器。
另外,设有对于为某一对象类型T所设定的分配器A,则A必须包含四项成员函数,分别为分配函数、解除分配函数、最大个数函数和地址函数:
A::pointer A::allocate(size_type n, A<void>::const_pointer hint = 0)。分配函数用以进行内存分配。其中调用参数n即为需要分配的对象个数,另一调用参数hint(须为指向已为A所分配的某一对象的指针)则为可选参数,可用于在分配过程中指定新数组所在的内存地址,以提高引用局部性,但在实际的分配过程中程序也可以根据情况自动忽略掉该参数。该函数调用时会返回指向分配所得的新数组的第一个元素的指针,而这一数组的大小足以容纳n个T类元素。在此需要注意的是,调用时只为此数组分配了内存,而并未实际构造对象。
void A::deallocate(A::pointer p, A::size_type n)。解除分配函数。其中p为需要解除分配的对象指针(以A::allocate函数所返回的指针做参数),n为对象个数,而调用该函数时即是将以p起始的n个元素解除分配,但同时并不会析构之。C++标准明确要求在调用deallocate之前,该地址空间上的对象已经被析构。
A::max_size(),最大个数函数。返回A::allocate一次调用所能成功分配的元素的最大个数,其返回值等价于A::size_type(-1) / sizeof(T)的结果。
A::pointer A::address ( reference x ),地址函数。调用时返回一个指向x的指针。
除此以外,由于对象的构造/析构过程与分配/解除分配过程分别进行,因而分配器还需要成员函数A::construct(构造函数)与A::destroy(析构函数)以对对象进行构造与析构,且两者应等价于如下函数:
template <typename T>
void A::construct(A::pointer p, A::const_reference t) { new ((void*) p) T(t); }
template <typename T>
void A::destroy(A::pointer p){ ((T*)p)->~T(); }
以上代码中使用了placementnew语法,且直接调用了析构函数。
分配器应是可复制构造的,任举一例,为T类对象而设的分配器可由另一为U类所设的分配器构造。若某分配器分配了一段存储空间,则这段存储空间只能由与该分配器等价的分配器解除分配。分配器还需要提供一个模板类成员类template <typename U> struct A::rebind { typedef A<U> other; };,以模板 (C++)参数化的方式,借之来针对不同的数据类型获取不同的分配器。例如,若给定某一为整型(int)而设的分配器IntAllocator,则可执行IntAllocator::rebind<long>::other以获取对应长整型(long)的相关分配器。实际上,stl::list<int>实际要分配的是包含了双向链表指针的node<int>,而不是实际分配int类型,这是引入了rebind的初衷。
与分配器相关联的operator ==,仅当一个allocator分配的内存可以被另一个allocator释放时,上述相等比较算符返回真。operator!=的返回结果与之相反。
Copyright © 2022-2024 厦门雄霸电子商务有限公司 版权所有 备案号:闽ICP备14012685号-33