23. std::allocator:标准库分配器模型、自定义分配器、与智能指针的结合
聊到内存管理,很多人第一反应就是 new 和 delete,或者智能指针。但说实话,真正决定内存怎么来、怎么去的底层机制,往往被忽略了。这就是我们今天要聊的主角——分配器(Allocator)。
分配器是 C++ 标准库容器和智能指针的“后勤部长”。它负责向操作系统要内存,也负责把用完了的内存还回去。你平时用的 std::vector、std::string,甚至某些智能指针的定制删除器,背后都离不开分配器的身影。
23.1 标准库分配器模型
标准库提供了一个默认分配器 std::allocator<T>。它的行为很简单:调用 ::operator new 分配原始内存,调用 ::operator delete 释放内存。但它不是直接构造对象,而是将“分配内存”和“构造对象”这两个步骤分开了。
我个人觉得,这种分离设计非常优雅。为什么?因为很多时候我们只需要原始内存,比如 std::vector 扩容时,它先分配一大块内存,然后逐个构造元素。如果内存分配和对象构造绑在一起,那扩容效率会大打折扣。
#include <memory>
#include <iostream>
int main() {
std::allocator<int> alloc;
// 分配 5 个 int 的原始内存
int* p = alloc.allocate(5);
// 构造对象
for (int i = 0; i < 5; ++i) {
alloc.construct(p + i, i * 10);
}
// 使用
for (int i = 0; i < 5; ++i) {
std::cout << p[i] << " ";
}
// 析构对象
for (int i = 0; i < 5; ++i) {
alloc.destroy(p + i);
}
// 释放内存
alloc.deallocate(p, 5);
return 0;
}
你看,allocate 只管要内存,construct 负责在内存上构造对象,destroy 负责析构,deallocate 负责归还。每一步都清清楚楚。
construct 和 destroy 被标记为 deprecated,推荐使用 std::allocator_traits 的 construct 和 destroy,或者直接用 std::uninitialized_fill 等算法。
23.2 自定义分配器:为什么需要?
默认分配器够用吗?大部分场景下够。但我在项目中遇到过几次特殊情况:
- 内存池场景:频繁分配小块内存,默认分配器开销太大。我曾经在一个实时音频处理模块里,用自定义分配器实现了固定大小的内存池,延迟降低了 40%。
- 对齐要求:某些硬件需要 64 字节对齐的内存,默认分配器不保证。
- 统计与监控:想统计某个容器分配了多少内存,自定义分配器可以轻松插入计数逻辑。
- 共享内存:多个进程需要共享同一块内存区域,默认分配器做不到。
说白了,自定义分配器就是让你掌控内存的“来龙去脉”。
23.3 如何编写一个自定义分配器
标准库对分配器有一套约定(Allocator Requirements)。你不需要继承某个基类,只需要实现几个关键接口:
value_type:分配的元素类型allocate(size_t n):分配 n 个元素的内存deallocate(T* p, size_t n):释放内存- 拷贝构造函数、
==和!=比较运算符
下面是一个简单的内存池分配器示例:
#include <cstdlib>
#include <new>
#include <vector>
template<typename T>
class PoolAllocator {
public:
using value_type = T;
PoolAllocator() = default;
template<typename U>
PoolAllocator(const PoolAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
// 简单实现:直接调用 malloc,实际项目可用内存池
if (auto p = static_cast<T*>(std::malloc(n * sizeof(T)))) {
return p;
}
throw std::bad_alloc();
}
void deallocate(T* p, std::size_t) noexcept {
std::free(p);
}
// 比较运算符
template<typename U>
bool operator==(const PoolAllocator<U>&) const { return true; }
template<typename U>
bool operator!=(const PoolAllocator<U>&) const { return false; }
};
// 使用自定义分配器的 vector
std::vector<int, PoolAllocator<int>> vec;
vec.push_back(42);
23.4 分配器与智能指针的结合
智能指针本身不直接使用分配器,但你可以通过自定义删除器来间接控制内存释放行为。更常见的是,std::allocate_shared 允许你使用自定义分配器来创建 std::shared_ptr。
为什么需要这个?因为 std::make_shared 默认使用 ::operator new,如果你想让 shared_ptr 的控制块和对象本身都从同一个内存池分配,那就得用 std::allocate_shared。
#include <memory>
#include <vector>
// 使用自定义分配器创建 shared_ptr
auto sp = std::allocate_shared<int>(PoolAllocator<int>(), 42);
// 也可以用于容器
std::vector<std::shared_ptr<int>, PoolAllocator<std::shared_ptr<int>>> vec;
vec.push_back(sp);
嗯,这里要注意:std::allocate_shared 会把分配器传递给控制块,控制块在释放对象时也会调用同一个分配器。这样整个生命周期内的内存管理就统一了。
23.5 知识体系图
下面这张图展示了分配器在整个内存管理生态中的位置:
23.6 避坑指南
我曾经在项目里犯过一个低级错误:自定义分配器没有实现 == 运算符,结果容器在拷贝时直接崩溃。标准库要求:两个分配器相等时,一个分配器释放的内存可以被另一个分配器重用。如果不实现 ==,容器会认为所有分配器都不相等,导致行为未定义。
另一个常见坑:分配器的 allocate 和 deallocate 必须成对出现,且 deallocate 的第二个参数必须与 allocate 的请求大小一致。否则内存泄漏或崩溃等着你。
boost::pool_allocator 或 tbb::scalable_allocator,它们经过了大量生产环境验证。
23.7 总结
分配器是 C++ 内存管理体系中容易被忽视但极其重要的一环。标准库的 std::allocator 提供了基础能力,而自定义分配器让你在特殊场景下获得极致性能。与智能指针结合时,std::allocate_shared 是统一内存管理的利器。
记住一句话:分配器管“怎么拿内存”,智能指针管“什么时候还”。两者配合,才能写出高效且安全的内存管理代码。