上一章我们对vector有了一定的了解,这次简单了解一下std::allocator(vector 默认分配器)。
一、分配器的核心定位
std::allocator 是 C++ 标准库提供的默认内存分配器,属于 STL 容器的 “内存管家”,核心职责是:
- 分离 “内存分配” 和 “元素构造”:先分配原始内存(未构造元素),再按需在内存上构造元素;
- 分离 “内存释放” 和 “元素析构”:先析构内存中的元素,再释放原始内存;
- 屏蔽不同平台的内存操作差异:提供统一的接口,底层调用
new/delete(或更高效的系统调用)。
对 vector 来说,你写的 vector<int> v 等价于 vector<int, std::allocator<int>> v—— 第二个模板参数就是分配器,默认使用 std::allocator。
二、std::allocator 的核心接口(C++17 前经典版本)
std::allocator 的接口极简,核心只有 6 个成员函数(C++11 后做了简化,C++17 进一步精简),先看最易理解的经典版本:
| 接口 | 功能 | 底层实现(逻辑) |
|---|---|---|
allocate(n) | 分配能容纳 n 个 T 类型对象的原始内存,返回 T* | 调用 operator new[](或 malloc),仅分配内存,不构造元素 |
deallocate(p, n) | 释放 p 指向的、能容纳 n 个 T 的内存 | 调用 operator delete[](或 free),仅释放内存,不析构元素 |
construct(p, args...) | 在 p 指向的内存上构造 T 对象,传入参数 args | 调用 new (p) T(args...)(定位 new,placement new) |
destroy(p) | 析构 p 指向的 T 对象 | 调用 p->~T()(显式调用析构函数) |
addressof(x) | 返回 x 的实际地址(避免重载 & 运算符的干扰) | &x(C++11 后封装为 std::addressof) |
max_size() | 返回最多能分配的 T 类型对象个数 | size_t(-1) / sizeof(T) |
关键概念:定位 new(placement new)
construct 接口的核心是定位 new—— 它不分配内存,只在已分配的原始内存上构造对象,语法:
// 语法:new (内存地址) 类型(构造参数)
T* p = alloc.allocate(1); // 分配1个T的内存(未构造)
alloc.construct(p, 10); // 等价于 new (p) T(10),在p指向的内存上构造T对象,值为10
这是分配器 “内存分配” 和 “元素构造” 分离的核心实现。
三、std::allocator 的简化实现(理解原理)
标准库的 std::allocator 实现会考虑平台兼容性、异常安全等细节,但核心逻辑可简化为以下代码(能帮你快速理解本质):
#include <new> // 定位 new 依赖 <new>
#include <cstdlib> // malloc/free 依赖
template <typename T>
class MyAllocator { // 模拟 std::allocator
public:
// 类型别名(STL 要求的规范)
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = size_t;
using difference_type = ptrdiff_t;
// 1. 分配内存(仅分配,不构造)
pointer allocate(size_type n) {
if (n == 0) return nullptr;
// 检查溢出:n * sizeof(T) 可能超过 size_t 范围
if (n > max_size()) {
throw std::bad_alloc(); // 分配失败抛异常
}
// 底层调用 operator new(或 malloc)
void* raw_mem = operator new(n * sizeof(T));
return static_cast<pointer>(raw_mem);
}
// 2. 释放内存(仅释放,不析构)
void deallocate(pointer p, size_type n) {
if (p == nullptr) return;
// 底层调用 operator delete(或 free)
operator delete(p);
}
// 3. 构造元素(定位 new)
template <typename... Args>
void construct(pointer p, Args&&... args) {
// 完美转发参数,在p指向的内存上构造T对象
new (p) T(std::forward<Args>(args)...);
}
// 4. 析构元素(显式调用析构函数)
void destroy(pointer p) {
p->~T(); // 调用T的析构函数
}
// 5. 返回最大可分配个数
size_type max_size() const noexcept {
return size_type(-1) / sizeof(T);
}
// 6. 复制构造(空实现,分配器无状态)
template <typename U>
MyAllocator(const MyAllocator<U>&) noexcept {}
};
// 分配器相等性判断(无状态分配器,任意两个实例都相等)
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return false;
}
核心特点:无状态分配器
std::allocator 是无状态分配器—— 分配器的实例不持有任何数据(比如内存池、分配统计等),任意两个 std::allocator 实例都是等价的,这也是为什么上面的复制构造是空实现。
四、分配器如何参与 vector 的内存管理(核心场景)
vector 的所有内存操作都通过分配器完成,我们结合 vector 的常用操作,拆解分配器的参与过程:
场景 1:vector 构造 + push_back(核心流程)
vector<int> v; // 等价于 vector<int, std::allocator<int>> v;
v.push_back(10);
背后的分配器操作步骤:
- 初始状态:vector 的
start_/finish_/end_of_storage_均为 nullptr,size=0,capacity=0; - push_back 触发扩容:
- 分配器
allocate(1):分配 1 个 int 的内存(capacity 变为 1); - 分配器
construct(finish_, 10):在新内存上构造 int 对象,值为 10; - vector 更新
finish_指针(size 变为 1);
- 分配器
- 后续 push_back 再次扩容(比如 capacity=1 时再 push_back):
- 分配器
allocate(2):分配 2 个 int 的新内存(2 倍扩容); - 分配器
construct(新内存地址, 旧元素值):将旧内存的元素移动 / 拷贝到新内存; - 分配器
destroy(旧内存地址):析构旧内存的元素; - 分配器
deallocate(旧内存, 1):释放旧内存; - vector 更新指针(capacity 变为 2,size 变为 2)。
- 分配器
场景 2:reserve(仅分配内存,不构造)
vector<int> v;
v.reserve(5); // 预分配5个int的内存
背后的分配器操作:
- 分配器
allocate(5):分配 5 个 int 的原始内存; - vector 更新
end_of_storage_指针(capacity 变为 5); - 注意:分配器未调用
construct,size 仍为 0,内存上无任何 int 对象。
场景 3:resize(分配内存 + 构造 / 析构)
vector<int> v = {1,2,3};
v.resize(5); // 扩容到5个元素,新增元素默认值0
背后的分配器操作:
- 若 capacity < 5:分配器
allocate(5)分配新内存,移动旧元素,释放旧内存; - 分配器
construct(finish_, 0):在第 4、5 个位置构造 int 对象,值为 0; - vector 更新
finish_指针(size 变为 5)。
场景 4:clear(析构元素,不释放内存)
v.clear(); // 清空vector,size=0,capacity不变
背后的分配器操作:
- 遍历
[start_, finish_)区间,分配器destroy(p)析构每个 int 对象; - vector 更新
finish_指针(size 变为 0); - 注意:分配器未调用
deallocate,内存仍保留(capacity 不变)。
场景 5:vector 析构(析构 + 释放)
{
vector<int> v = {1,2,3};
} // 离开作用域,vector析构
背后的分配器操作:
- 遍历所有元素,分配器
destroy(p)析构每个元素; - 分配器
deallocate(start_, capacity):释放所有内存; - vector 的指针置空。
场景 6:erase/pop_back(删除元素,仅析构,不释放内存)
v.pop_back(); // 删除尾部元素
v.erase(v.begin()+1); // 删除指定位置元素
pop_back:分配器析构finish_-1位置的对象,vector将finish_前移,size-1,capacity不变;erase:分配器析构指定位置的对象,vector将后续对象前移(覆盖),size-1,capacity不变;- 核心:删除元素仅销毁对象,分配器从不释放原始内存,
vector的容量只会随扩容增加,不会随删除减少(除非手动调用shrink_to_fit)。
场景 7:shrink_to_fit(收缩容量,内存重分配)
v.reserve(100); v.push_back(10); // size=1,capacity=100
v.shrink_to_fit(); // 收缩容量到1
- 分配器先
allocate(1)申请新的小内存,在新内存上构造 1 个int对象; - 析构旧内存的 1 个对象,释放旧的 100 个 int 的内存;
vector更新指针,size=1,capacity=1;- 注:
shrink_to_fit是请求式操作,编译器可忽略,并非强制收缩。
五、C++11/17 对 std::allocator 的简化
C++ 标准演进中,std::allocator 的接口被逐步精简(因为很多接口可通过通用函数替代):
1. C++11 变化
- 移除
construct/destroy接口:直接用std::construct_at/std::destroy_at替代; - 新增
std::addressof:替代分配器的addressof成员; - 分配器要求更宽松,无需继承
std::allocator_base。
2. C++17 核心变化
std::allocator 仅保留核心的 allocate/deallocate 接口,其他接口全部移除,因为:
construct→ 用std::construct_at(p, args);destroy→ 用std::destroy_at(p);addressof→ 用std::addressof(x);
简化后的 C++17 版本 std::allocator 核心逻辑:
template <typename T>
struct allocator {
using value_type = T;
// 仅保留分配/释放接口
T* allocate(size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, size_t n) {
::operator delete(p);
}
// 空的复制/移动构造
allocator() noexcept = default;
template <typename U>
allocator(const allocator<U>&) noexcept {}
};
六、分配器的进阶:为什么需要自定义分配器?
std::allocator 是通用分配器,但在某些场景下不够高效,需要自定义分配器:
- 内存池优化:频繁小内存分配(比如 vector 频繁扩容)时,内存池可减少
new/delete的系统调用开销; - 对齐要求:某些硬件 / 算法要求内存按 16/32 字节对齐,
std::allocator仅保证基本对齐; - 调试 / 统计:自定义分配器可统计内存分配次数、大小,排查内存泄漏;
- 共享内存:分配器可从共享内存区域分配内存,实现进程间数据共享。
示例:简单内存池分配器(核心思路)
// 简化版内存池分配器:预分配一块大内存,按需切割
template <typename T>
class PoolAllocator {
private:
char* pool_; // 内存池起始地址
size_t pool_size_; // 内存池总大小
size_t used_; // 已使用大小
public:
PoolAllocator(size_t pool_size = 1024 * 1024) : pool_size_(pool_size), used_(0) {
pool_ = static_cast<char*>(malloc(pool_size)); // 预分配1MB内存池
}
~PoolAllocator() {
free(pool_);
}
T* allocate(size_t n) {
size_t req_size = n * sizeof(T);
if (used_ + req_size > pool_size_) {
throw std::bad_alloc();
}
T* p = reinterpret_cast<T*>(pool_ + used_);
used_ += req_size;
return p;
}
void deallocate(T* p, size_t n) {
// 内存池不单独释放小块内存,析构时统一释放(简化版)
}
};
七、核心总结
- std::allocator 核心职责:分离 “内存分配 / 释放” 和 “元素构造 / 析构”,vector 的所有内存操作都通过它完成;
- 核心接口:
allocate/deallocate:处理原始内存(底层operator new/delete);construct/destroy:处理元素构造 / 析构(底层定位 new / 显式析构);
- vector 与分配器的协作:
reserve→ 调用allocate(仅分配内存);push_back/emplace_back→ 先检查容量,不足则allocate扩容,再construct构造元素;clear→ 调用destroy析构元素,不deallocate;- 析构 → 先
destroy所有元素,再deallocate释放内存;
- 关键简化:C++17 后
std::allocator仅保留allocate/deallocate,其他操作通过通用函数实现; - 自定义分配器场景:内存池、对齐要求、调试统计、共享内存等。
简单记:分配器是 vector 的 “内存管家”—— 管家负责找内存(allocate)、布置家具(construct)、扔掉家具(destroy)、收回内存(deallocate),vector 只负责指挥管家干活。
八、分配器在stl中的使用
通用规则:分配器与 STL 容器的内存管理协作原则
不只是vector,string、deque、list等所有 STL 容器的内存管理,都遵循与分配器的固定协作原则,这也是 STL 设计的核心思想之一:
- 容器管逻辑,分配器管底层:容器只负责维护元素的逻辑结构(如
vector的连续序列、list的链表节点),不直接调用new/delete,所有内存和对象的底层操作都委托给分配器; - 内存和对象分离操作:
- 申请内存时,只分配原始空间,不构造任何对象(
reserve/allocate); - 添加元素时,只在已分配的内存上构造对象,不重新申请内存(无需扩容时);
- 删除元素时,只析构对象,不释放内存(
clear/erase/pop_back); - 容器析构时,必须先析构所有对象,再释放内存(避免内存泄漏);
- 申请内存时,只分配原始空间,不构造任何对象(
- 分配器是模板参数,可替换:容器的分配器是模板参数(如
vector<T, Alloc>),std::allocator是默认选择,可根据需求替换为自定义分配器(如内存池分配器、对齐分配器),容器的逻辑无需任何修改; - 无状态分配器为主流:
std::allocator是无状态分配器(实例不持有任何数据),任意两个实例等价,这也是 STL 推荐的分配器设计方式,保证容器的拷贝 / 移动语义正常。
一句话总结分配器的核心作用
分配器是容器和底层内存之间的抽象层,通过将「内存分配 / 释放」和「对象构造 / 析构」分离,让容器无需关心底层内存的具体实现(如是普通堆内存、内存池还是共享内存),实现了容器逻辑与内存实现的解耦,既保证了 STL 的通用性,又为性能优化和特殊场景(如内存池、共享内存)提供了扩展能力。
拓展:为什么 vector 的扩容必须由分配器完成?
如果 vector 自己直接调用new T[n],会同时分配内存并构造 n 个 T 对象,而 vector 需要的是仅分配内存、不构造对象的原始空间(按需构造元素),new T[n]无法满足这个需求;同理,delete[] p会同时析构对象并释放内存,而 vector 需要的是仅析构对象、不释放内存(如clear)或仅释放内存、不析构对象(如扩容后的旧内存)。
分配器的allocate/deallocate和construct/destroy接口,恰好完美解决了这个问题,这也是 STL 设计分配器的根本原因。
365



被折叠的 条评论
为什么被折叠?



