23. std::allocator:标准库分配器模型、自定义分配器、与智能指针的结合

聊到内存管理,很多人第一反应就是 newdelete,或者智能指针。但说实话,真正决定内存怎么来、怎么去的底层机制,往往被忽略了。这就是我们今天要聊的主角——分配器(Allocator)

分配器是 C++ 标准库容器和智能指针的“后勤部长”。它负责向操作系统要内存,也负责把用完了的内存还回去。你平时用的 std::vectorstd::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 负责归还。每一步都清清楚楚。

小提示:C++17 之后,constructdestroy 被标记为 deprecated,推荐使用 std::allocator_traitsconstructdestroy,或者直接用 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);
注意:自定义分配器必须满足“可拷贝”和“无状态”的约定(除非你明确需要状态分配器)。否则容器在拷贝、移动时可能会出问题。我曾经踩过这个坑——分配器里带了一个文件句柄,结果 vector 拷贝时直接崩溃。

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 知识体系图

下面这张图展示了分配器在整个内存管理生态中的位置:

分配器在内存管理中的角色 std::vector / std::string 标准容器 std::shared_ptr 智能指针 std::allocate_shared 分配器 + 智能指针 分配器(Allocator) std::allocator<T> 或 自定义分配器 allocate / deallocate / construct / destroy 操作系统内存(堆 / 虚拟内存) malloc / free / mmap / VirtualAlloc 内存流向

23.6 避坑指南

我曾经在项目里犯过一个低级错误:自定义分配器没有实现 == 运算符,结果容器在拷贝时直接崩溃。标准库要求:两个分配器相等时,一个分配器释放的内存可以被另一个分配器重用。如果不实现 ==,容器会认为所有分配器都不相等,导致行为未定义。

另一个常见坑:分配器的 allocatedeallocate 必须成对出现,且 deallocate 的第二个参数必须与 allocate 的请求大小一致。否则内存泄漏或崩溃等着你。

我的建议:如果只是想要内存池效果,别自己造轮子。看看 boost::pool_allocatortbb::scalable_allocator,它们经过了大量生产环境验证。

23.7 总结

分配器是 C++ 内存管理体系中容易被忽视但极其重要的一环。标准库的 std::allocator 提供了基础能力,而自定义分配器让你在特殊场景下获得极致性能。与智能指针结合时,std::allocate_shared 是统一内存管理的利器。

记住一句话:分配器管“怎么拿内存”,智能指针管“什么时候还”。两者配合,才能写出高效且安全的内存管理代码。


公众号:蓝海资料掘金营,微信 deep3321