容器如何使用分配器
std::allocator#
std::allocator
是标准库提供的默认分配器模板,如果容器中没有显式指明分配器的话,就会使用这个分配器模板的实例为容器申请和释放内存。
std::allocator
的使用方式也很简单:
allocator<int> alloc;
int *numptr = alloc.allocate(2);
cout << *numptr << endl;
cout << *(numptr + 1) << endl;
*numptr = 42;
*(numptr + 1) = 13;
cout << *numptr << endl;
cout << *(numptr + 1) << endl;
alloc.deallocate(numptr, 2);
这里实例化了一个int类型的分配器alloc,该实例对象只有两个成员函数allocate
和deallocate
,分别对应申请内存和释放内存(在C++17之前,std::allocator还有两个成员函数construct
和destory
,分别用于在申请到的内存上构造和析构对应类型的对象,C++17后弃用,并在C++20后彻底删除,并将这部分功能转移到了allocator_traits
中)。
在调用了allocate
方法后,分配器申请到了两个int大小的内存,并以指针的形式传递出去,此时这块内存是未进行初始化的,然后通过传递出来的指针,可以对申请到的内存进行操作,比如为其赋值。
然后在使用完成后,调用deallocate
方法释放掉分配器分配的内存。deallocate
需要传入指向内存的指针和对应的size,在默认分配器中,由于是直接使用的delete
,size并不影响内存释放,但如果使用的是自定义分配器,这样可以获得更大的灵活性。
在std::allocator
中定义了一些类型特性,如果使用自定义的分配器,为了兼容标准库的接口,应该定义相同的类型特性,或者直接继承std::allocator
模板类,这些类型特性包括:
allocator<int>::difference_type;
allocator<int>::size_type;
allocator<int>::value_type;
allocator<int>::propagate_on_container_move_assignment;
allocator<int>::is_always_equal;
std::allocator_traits#
allocator_traits
是分配器的萃取类型,其为所有符合标准的分配器提供了一致的接口,并为指定分配器中未实现的特性提供默认的实现。
allocator<double> alloc2;
allocator_traits<allocator<double>> alloc_t;
double* dbptr = alloc_t.allocate(alloc2, 2);
alloc_t.construct(alloc2, dbptr, 10);
alloc_t.construct(alloc2, dbptr + 1, 20);
cout << *dbptr << endl;
cout << *(dbptr + 1) << endl;
alloc_t.destroy(alloc2, dbptr + 1);
alloc_t.destroy(alloc2, dbptr);
alloc_t.deallocate(alloc2, dbptr, 2);
allocator_traits
实例化时指定包装的分配器类型,然后在所有的静态方法的第一个入参指定对应分配器的实例对象。allocator_traits
在分配器的基础上提供了construct
和destory
方法,用于构造和析构对应的元素和类型。
allocator_traits
同样可以用于自定义类型的分配器:
class CustomC {
public:
CustomC() { cout << "default construct custom object\n"; }
CustomC(int num) : num(num) { cout << "construct custom object\n"; }
~CustomC() { cout << "destory custom object\n"; }
int num{-1};
};
allocator<CustomC> alloc3;
allocator_traits<allocator<CustomC>> alloc_t2;
CustomC *custom_ptr = alloc_t2.allocate(alloc3, 2);
alloc_t2.construct(alloc3, custom_ptr); // default construct custom object
alloc_t2.construct(alloc3, custom_ptr + 1, 100); // construct custom object
cout << custom_ptr->num << endl; // -1
cout << (custom_ptr + 1)->num << endl; // 100
alloc_t2.destroy(alloc3, custom_ptr); // destory custom object
alloc_t2.destroy(alloc3, custom_ptr + 1); // destory custom object
alloc_t2.deallocate(alloc3, custom_ptr, 2);
allocator_traits
对于标准库默认的分配器有一个特化实现,但我们也完全可以使用自定义的分配器:
template<typename T>
class MyAlloc : public std::allocator<T> { };
MyAlloc<CustomC> myalloc;
allocator_traits<MyAlloc<CustomC>> alloc_t3;
CustomC* mycustom = alloc_t3.allocate(myalloc, 2);
alloc_t3.construct(myalloc, mycustom);
alloc_t3.construct(myalloc, mycustom + 1, 33);
alloc_t3.destroy(myalloc, mycustom + 1);
alloc_t3.destroy(myalloc, mycustom);
alloc_t3.deallocate(myalloc, mycustom, 2);
这与上述默认的分配器执行结果相同,但会匹配allocator_traits
更一般的模板,并且由于我们没有实现construct
和destory
函数,执行时会转而使用allocator_traits
默认的方法。这正体现了allocator_traits
优势——只需要实现关心的部分,其他无关的部分标准库会帮你实现。
在容器中使用分配器#
为了让容器可以更灵活地使用分配器,分配器萃取类allocator_traits
提供了rebind_traits
和rebind_alloc
类型特征(代替并简化了了C++11中分配器的rebind操作),通过这两个类型特征,可以生成一个与模板参数类型不同,但分配策略相同的分配器和萃取类。
从int类型的分配器中获取一个double类型的分配器:
MyAlloc<int> myialloc;
allocator_traits<MyAlloc<int>> myialloc_trait;
using my_rebind_alloc_type = allocator_traits<MyAlloc<int>>::rebind_alloc<double>;
using my_rebind_alloc_trait = allocator_traits<MyAlloc<int>>::rebind_traits<double>;
my_rebind_alloc_type mydballoc;
my_rebind_alloc_trait my_dballoc_trait;
double* dbptr = my_dballoc_trait.allocate(mydballoc, 2);
my_dballoc_trait.construct(mydballoc, dbptr, 1.3);
my_dballoc_trait.construct(mydballoc, dbptr + 1, 4.2);
cout << *dbptr << endl;
cout << *(dbptr + 1) << endl;
my_dballoc_trait.destroy(mydballoc, dbptr);
my_dballoc_trait.destroy(mydballoc, dbptr + 1);
my_dballoc_trait.deallocate(mydballoc, dbptr, 2);
用途在于容器为了方便管理,往往会对元素类型再次封装,而模板参数传入的分配器是元素类型的,通过重新绑定到容器内部封装的类型,保证分配策略的一致性。
最后演示一个自定义的链表容器和插入操作实现,链表容器内部通过Node类型管理插入的元素:
template <typename T, typename Alloc = std::allocator<T>>
class List {
public:
using value_type = T;
using alloc_trait = typename std::allocator_traits<Alloc>;
using node_alloc_type = typename alloc_trait::template rebind_alloc<Node>;
using rebind_trait = typename alloc_trait::template rebind_traits<Node>;
struct Node {
value_type data{};
Node* next{nullptr};
};
List() = default;
void insert(T elem) {
rebind_trait rebind_t;
Node* node = rebind_t.allocate(m_alloc, 1);
rebind_t.construct(m_alloc, node);
node->data = elem;
return insert(m_root, node);
}
private:
void insert(Node* root, Node* node) {
if(m_root == nullptr) {
m_root = node;
return;
}
if(m_root->next == nullptr) {
m_root->next = node;
return;
}
return insert(m_root->next, node);
}
public:
Node* m_root{nullptr};
node_alloc_type m_alloc{};
};
在列表容器内部,保存了容器元素类型的分配器,但在容器内部元素是通过Node结构来封装的。所以每次插入元素时,我们都将分配器重新绑定一个模板参数为Node的分配器,其使用的分配策略和指定传给容器的分配器相同,只是分配的类型不同。
List<int> mylist;
mylist.insert(10);
mylist.insert(20);
cout << mylist.m_root->data << endl; // 10
cout << mylist.m_root->next->data << endl; // 20
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· Apache Tomcat RCE漏洞复现(CVE-2025-24813)