菜鸟笔记
提升您的技术认知

C++ STL 容器内存池

C++ 中,为了优化性能,减少频繁的内存分配和释放操作,可以自定义一个内存池分配器,并将其与 STL 容器结合使用。以下是实现自定义内存池分配器的完整教程。


1. 为什么需要自定义内存池分配器

  1. 减少动态分配的开销
    • 使用 newmalloc 动态分配内存会带来较大的开销,尤其是在频繁分配和释放小块内存时。
    • 内存池通过预分配大块内存并按需管理小块,可以显著减少这种开销。
  2. 提升内存访问性能
    • 内存池分配的内存通常是连续的,这有利于缓存友好性(cache locality),从而提高运行速度。
  3. 自定义分配器与 STL 容器结合
    • STL 容器(如 std::vector, std::map 等)支持自定义分配器,用于控制内存分配和释放行为。
    • 使用内存池分配器可以优化 STL 容器的性能。

2. 内存池分配器的设计思路

一个简单的内存池需要包含以下功能:
1. 预分配内存块
– 在初始化时,分配一大块连续内存,用于后续的小块分配。

  1. 小块内存分配
    • 从已分配的内存块中划分小块,按需分配给用户。
  2. 内存释放和重用
    • 用户释放的小块内存可以回收到内存池中,以便再次分配。

3. 实现自定义内存池分配器

以下是完整实现,包括内存池类和与 STL 容器结合使用的分配器。

3.1 内存池实现

#include <cstddef>
#include <iostream>
#include <vector>
#include <memory>

class MemoryPool {
public:
    MemoryPool(size_t blockSize, size_t blockCount)
        : _blockSize(blockSize), _blockCount(blockCount) {
        allocateBlock();
    }

    ~MemoryPool() {
        for (void* block : _memoryBlocks) {
            ::operator delete(block); // 释放所有内存块
        }
    }

    void* allocate() {
        if (_freeBlocks.empty()) {
            allocateBlock();  // 如果没有空闲块,分配新的内存块
        }

        void* ptr = _freeBlocks.back();
        _freeBlocks.pop_back();  // 从空闲块中取一个
        return ptr;
    }

    void deallocate(void* ptr) {
        _freeBlocks.push_back(ptr);  // 将释放的块放回空闲列表
    }

private:
    size_t _blockSize;                 // 每个小块的大小
    size_t _blockCount;                // 每个内存块包含的小块数
    std::vector<void*> _freeBlocks;    // 空闲的小块列表
    std::vector<void*> _memoryBlocks;  // 已分配的内存块

    void allocateBlock() {
        // 分配一整块内存
        void* block = ::operator new(_blockSize * _blockCount);
        _memoryBlocks.push_back(block);

        // 将分配的内存划分为小块,加入空闲列表
        for (size_t i = 0; i < _blockCount; ++i) {
            _freeBlocks.push_back(static_cast<char*>(block) + i * _blockSize);
        }
    }
};

3.2 自定义分配器

实现符合 STL 的分配器接口(即 std::allocator 的行为)。

template <typename T>
class PoolAllocator {
public:
    using value_type = T;

    PoolAllocator(size_t blockCount = 1024) 
        : _pool(sizeof(T), blockCount) {}

    template <typename U>
    PoolAllocator(const PoolAllocator<U>& other) noexcept
        : _pool(other._pool) {}

    T* allocate(size_t n) {
        if (n != 1) {
            throw std::bad_alloc(); // 只支持单个对象的分配
        }
        return static_cast<T*>(_pool.allocate());
    }

    void deallocate(T* ptr, size_t n) {
        if (n != 1) {
            return; // 只支持单个对象的释放
        }
        _pool.deallocate(ptr);
    }

    template <typename U>
    struct rebind {
        using other = PoolAllocator<U>;
    };

private:
    MemoryPool& _pool;

    template <typename U>
    friend class PoolAllocator;
};

3.3 将内存池分配器与 STL 容器结合

可以将自定义分配器与任意支持分配器的 STL 容器结合使用,例如 std::vectorstd::liststd::map 等。

示例:使用 std::vector 和内存池分配器

#include <vector>
#include <iostream>

int main() {
    // 使用自定义内存池分配器
    PoolAllocator<int> allocator;

    // 将分配器绑定到 std::vector
    std::vector<int, PoolAllocator<int>> vec(allocator);

    // 插入数据
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
    }

    // 输出数据
    for (int value : vec) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

3.4 输出结果

运行代码时,std::vector 将使用自定义的内存池分配器来管理内存,输出如下:

0 1 2 3 4 5 6 7 8 9

4. 优化和注意事项

  1. 线程安全
    • 如果需要在多线程环境下使用,可以为 MemoryPool 添加互斥锁(如 std::mutex)来保护 _freeBlocks_memoryBlocks
  2. 对象构造与析构
    • STL 容器会调用元素的构造和析构函数,因此需要确保内存池分配的内存支持对象的正确构造和析构。
    • 可以通过 std::allocator_traitsstd::construct_at 明确调用构造函数
  3. 支持多类型内存分配
    • 内存池可以设计为支持不同大小的对象分配,通过模板实现多类型支持。
  4. 内存池清理
    • 在析构时释放所有内存,确保没有内存泄漏

5. 总结

  1. 使用自定义内存池分配器可以显著提高小内存分配的性能,特别是在高频率内存分配释放场景中。
  2. 自定义内存池分配器需要实现分配(allocate)和释放(deallocate)接口,并与 STL 容器结合。
  3. 在多线程环境中使用时,需要考虑线程安全性。
  4. 内存池适合性能敏感的场景,但在普通场景下可以优先使用标准分配器(std::allocator)。