C++ 内存分配器的作用
在 C++ 中,内存分配器(Allocator) 是一个用于管理内存分配和释放的机制。
📚 一、内存分配器基础
默认分配器:std::allocator<T>
C++ 标准库提供了一个默认的分配器模板类 std::allocator<T>
,它封装了底层的内存分配和释放操作。默认分配器通过调用全局 operator new
和 operator delete
来分配和释放内存。
基本方法:
- allocate(n):分配原始未构造的内存空间,能容纳 n 个
T
类型对象。 - deallocate(p, n):释放由
allocate()
分配的内存,p 是指针,n 是元素数。 - construct(p, args…):在已分配的内存 p 上构造一个对象。
- destroy§:销毁 p 指向的对象。
✅ 二、使用默认分配器 vs. 自定义分配器
使用默认分配器
当我们创建一个标准容器(如 std::vector
),如果没有指定分配器,则会使用默认分配器 std::allocator<T>
。这意味着每次需要扩展容器容量时,都会调用 new
和 delete
来分配和释放内存。
示例代码:
#include <iostream>
#include <vector>int main() {std::vector<int> vec;for (int i = 0; i < 10; ++i) {vec.push_back(i);std::cout << "Vector size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;}return 0;
}
输出示例:
Vector size: 1, capacity: 1
Vector size: 2, capacity: 2
Vector size: 3, capacity: 4
Vector size: 4, capacity: 4
Vector size: 5, capacity: 8
Vector size: 6, capacity: 8
Vector size: 7, capacity: 8
Vector size: 8, capacity: 8
Vector size: 9, capacity: 16
Vector size: 10, capacity: 16
可以看到,随着元素数量的增加,vector
的容量会按照一定的策略增长(通常是翻倍),这会导致多次调用 new
和 delete
,带来一定的性能开销。
自定义分配器示例:
#include <iostream>
#include <vector>
#include <memory>template<typename T>
class SimplePoolAllocator {
public:using value_type = T;SimplePoolAllocator() noexcept = default;template<typename U>SimplePoolAllocator(const SimplePoolAllocator<U>&) noexcept {}T* allocate(std::size_t n) {std::cout << "Allocating " << n << " elements of type " << typeid(T).name() << std::endl;return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, std::size_t n) {std::cout << "Deallocating " << n << " elements" << std::endl;::operator delete(p);}
};template<typename T, typename U>
bool operator==(const SimplePoolAllocator<T>&, const SimplePoolAllocator<U>&) { return true; }template<typename T, typename U>
bool operator!=(const SimplePoolAllocator<T>&, const SimplePoolAllocator<U>&) { return false; }int main() {std::vector<int, SimplePoolAllocator<int>> vec;for (int i = 0; i < 10; ++i) {vec.push_back(i);std::cout << "Vector size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;}return 0;
}
输出示例:
Allocating 1 elements of type i
Vector size: 1, capacity: 1
Allocating 2 elements of type i
Deallocating 1 elements
Vector size: 2, capacity: 2
Allocating 4 elements of type i
Deallocating 2 elements
Vector size: 3, capacity: 4
...
在这个例子中,我们简单地记录了内存分配和释放的操作。
当然可以!在实际应用中,自定义分配器(Custom Allocator) 最常见的用途之一就是实现 内存池(Memory Pool)。内存池是一种预分配固定大小的内存块集合,避免频繁调用 new
和 delete
,从而大幅提升性能,尤其适用于高频创建和销毁对象的场景。
三、应用场景:游戏开发中的对象池
以一个游戏引擎为例,游戏中可能会频繁创建和销毁“子弹”对象。如果每次都使用默认分配器进行动态内存分配,会导致:
- 高频调用
malloc/free
或new/delete
- 内存碎片
- 分配速度慢
我们可以使用一个 内存池分配器 来解决这些问题。
📦示例:带内存池的自定义分配器(用于 std::vector
)
我们将实现一个简单的固定大小内存池分配器(Fixed Block Memory Pool Allocator),并将其用于 std::vector
。
✅ 示例目标:
- 实现一个能管理固定大小对象的内存池。
- 将其作为
std::vector
的自定义分配器。 - 提升频繁分配/释放小对象时的性能。
🔨 步骤 1:实现内存池类
#include <iostream>
#include <vector>
#include <memory>
#include <cassert>// 固定大小内存池
class FixedBlockAllocator {
private:struct FreeListNode {FreeListNode* next;};size_t block_size_;size_t pool_size_;char* memory_pool_;FreeListNode* free_list_;public:FixedBlockAllocator(size_t block_size, size_t num_blocks): block_size_(block_size), pool_size_(num_blocks) {assert(block_size >= sizeof(FreeListNode));memory_pool_ = reinterpret_cast<char*>(::operator new(block_size * num_blocks));free_list_ = reinterpret_cast<FreeListNode*>(memory_pool_);// 初始化空闲链表for (size_t i = 0; i < num_blocks - 1; ++i) {FreeListNode* node = reinterpret_cast<FreeListNode*>(memory_pool_ + i * block_size_);node->next = reinterpret_cast<FreeListNode*>(memory_pool_ + (i + 1) * block_size_);}FreeListNode* last_node = reinterpret_cast<FreeListNode*>(memory_pool_ + (num_blocks - 1) * block_size_);last_node->next = nullptr;}~FixedBlockAllocator() {::operator delete(memory_pool_);}void* allocate() {if (!free_list_) {return ::operator new(block_size_); // 超出池容量,退化为普通 new}void* p = free_list_;free_list_ = free_list_->next;return p;}void deallocate(void* p) {if (p >= memory_pool_ && p < memory_pool_ + block_size_ * pool_size_) {// 归还到内存池FreeListNode* node = static_cast<FreeListNode*>(p);node->next = free_list_;free_list_ = node;} else {// 不属于内存池,使用 delete::operator delete(p);}}
};
🔨 步骤 2:实现自定义分配器适配器
为了让这个内存池可以作为 STL 容器的分配器,我们需要封装成标准接口。
template <typename T, size_t BLOCK_SIZE = 1024>
class PoolAllocator {
private:FixedBlockAllocator& pool_;public:using value_type = T;PoolAllocator(FixedBlockAllocator& pool) noexcept : pool_(pool) {}template <typename U>PoolAllocator(const PoolAllocator<U, BLOCK_SIZE>& other) noexcept: pool_(other.pool_) {}T* allocate(std::size_t n) {assert(n == 1); // 只支持单个对象分配return static_cast<T*>(pool_.allocate());}void deallocate(T* p, std::size_t n) {assert(n == 1);pool_.deallocate(p);}
};template <typename T, typename U, size_t BS>
bool operator==(const PoolAllocator<T, BS>& a, const PoolAllocator<U, BS>& b) {return &a.pool_ == &b.pool_;
}template <typename T, typename U, size_t BS>
bool operator!=(const PoolAllocator<T, BS>& a, const PoolAllocator<U, BS>& b) {return !(a == b);
}
🔨 步骤 3:使用内存池分配器的 std::vector
我们来测试一下这个分配器在 std::vector
中的表现。
struct Bullet {float x, y, angle;int damage;
};int main() {const size_t POOL_SIZE = 1000;FixedBlockAllocator pool(sizeof(Bullet), POOL_SIZE);using BulletVector = std::vector<Bullet, PoolAllocator<Bullet>>;BulletVector bullets(PoolAllocator<Bullet>(pool));// 插入大量子弹对象for (int i = 0; i < 2000; ++i) {bullets.push_back({1.0f, 2.0f, 0.5f, 10});}std::cout << "Total elements: " << bullets.size() << std::endl;return 0;
}
📈 三、性能优势分析
指标 | 默认分配器 (std::allocator ) | 自定义内存池分配器 |
---|---|---|
内存分配速度 | 慢(每次调用 new ) | 快(从链表取) |
内存释放速度 | 慢(每次调用 delete ) | 极快(归还链表) |
内存碎片 | 多 | 几乎无 |
扩展性 | 不可控 | 可定制(如缓存、日志等) |
在本例中,前 1000 次分配使用内存池,后 1000 次自动回退到
new
,你可以根据需要扩展内存池大小或实现多级内存池。
🛠️ 四、示例进阶
要支持 不同大小对象的分配,我们可以构建一个更通用的内存池系统,即所谓的 Slab Allocator(分块分配器) 或 多级内存池(Multi-block Memory Pool)。
这种分配器可以管理多个固定大小的内存池,每个池专门处理某一类大小的对象。当请求分配时,根据对象大小选择合适的池进行分配,从而避免频繁调用 new/delete
,减少内存碎片并提升性能。
🧱 一、设计目标
- 支持任意大小对象的分配。
- 内部维护多个固定大小的内存池(例如:4B、8B、16B、32B…)。
- 对于大于某个阈值的对象(如 1024B),直接使用默认分配器。
- 提供标准接口用于 STL 容器或自定义类型。
🔨 二、实现步骤
我们将构建以下组件:
- MemoryPoolManager:主控类,管理多个固定大小的内存池。
- FixedSizePool:单个固定大小的内存池(类似前面的
FixedBlockAllocator
)。 - GeneralAllocator:对外的标准分配器接口,适配 STL 容器。
✅ 示例代码:支持多种大小对象分配的通用分配器
步骤 1:固定大小内存池类
// FixedSizePool.h
#pragma once
#include <cstddef>
#include <vector>
#include <cassert>class FixedSizePool {
private:size_t block_size_;size_t num_blocks_;void* memory_pool_;void** free_list_;public:FixedSizePool(size_t block_size, size_t num_blocks);~FixedSizePool();void* allocate();void deallocate(void* p);size_t block_size() const { return block_size_; }
};
// FixedSizePool.cpp
#include "FixedSizePool.h"
#include <iostream>
#include <cstdlib>FixedSizePool::FixedSizePool(size_t block_size, size_t num_blocks): block_size_(block_size), num_blocks_(num_blocks) {assert(block_size >= sizeof(void*)); // 至少能放一个指针memory_pool_ = std::malloc(block_size * num_blocks_);if (!memory_pool_) throw std::bad_alloc();free_list_ = reinterpret_cast<void**>(memory_pool_);char* pool_bytes = reinterpret_cast<char*>(memory_pool_);for (size_t i = 0; i < num_blocks_ - 1; ++i) {free_list_[i] = pool_bytes + block_size_ * (i + 1);}free_list_[num_blocks_ - 1] = nullptr;
}FixedSizePool::~FixedSizePool() {std::free(memory_pool_);
}void* FixedSizePool::allocate() {if (!free_list_) return nullptr;void* p = free_list_;free_list_ = reinterpret_cast<void**>(*free_list_);return p;
}void FixedSizePool::deallocate(void* p) {if (!p) return;// 检查是否在内存池范围内char* start = reinterpret_cast<char*>(memory_pool_);char* end = start + block_size_ * num_blocks_;if (p >= start && p < end) {*reinterpret_cast<void**>(p) = free_list_;free_list_ = reinterpret_cast<void**>(p);}
}
步骤 2:多级内存池管理器
// MemoryPoolManager.h
#pragma once
#include "FixedSizePool.h"
#include <unordered_map>
#include <vector>
#include <mutex>class MemoryPoolManager {
private:std::vector<FixedSizePool> pools_;std::unordered_map<size_t, FixedSizePool*> size_to_pool_;std::mutex mtx_;public:static MemoryPoolManager& instance() {static MemoryPoolManager manager;return manager;}void initialize();void* allocate(size_t size);void deallocate(void* ptr, size_t size);
};
// MemoryPoolManager.cpp
#include "MemoryPoolManager.h"
#include <iostream>void MemoryPoolManager::initialize() {static const size_t pool_sizes[] = { 4, 8, 16, 32, 64, 128, 256, 512 };for (auto sz : pool_sizes) {pools_.emplace_back(FixedSizePool(sz, 1000));}for (auto& pool : pools_) {size_to_pool_[pool.block_size()] = &pool;}
}void* MemoryPoolManager::allocate(size_t size) {std::lock_guard<std::mutex> lock(mtx_);// 找到合适大小的池(向上对齐)for (const auto& kv : size_to_pool_) {if (kv.first >= size) {void* p = kv.second->allocate();if (p) return p;}}// 超出预设池范围,使用默认 newreturn ::operator new(size);
}void MemoryPoolManager::deallocate(void* ptr, size_t size) {std::lock_guard<std::mutex> lock(mtx_);for (const auto& kv : size_to_pool_) {if (kv.first >= size) {kv.second->deallocate(ptr);return;}}// 非池内内存,使用 delete::operator delete(ptr);
}
步骤 3:自定义通用分配器(适配 STL)
// GeneralAllocator.h
#pragma once
#include <memory>
#include "MemoryPoolManager.h"template<typename T>
class GeneralAllocator {
public:using value_type = T;GeneralAllocator() noexcept = default;template<typename U>GeneralAllocator(const GeneralAllocator<U>&) noexcept {}T* allocate(std::size_t n) {if (n != 1) throw std::bad_alloc(); // 本例只支持单对象分配return static_cast<T*>(MemoryPoolManager::instance().allocate(sizeof(T)));}void deallocate(T* p, std::size_t n) {if (n != 1) return;MemoryPoolManager::instance().deallocate(p, sizeof(T));}
};template<typename T, typename U>
bool operator==(const GeneralAllocator<T>&, const GeneralAllocator<U>&) {return true;
}template<typename T, typename U>
bool operator!=(const GeneralAllocator<T>& a, const GeneralAllocator<U>& b) {return false;
}
✅ 四、使用示例:为多种对象提供内存池服务
#include <vector>
#include <list>
#include <string>
#include "GeneralAllocator.h"int main() {// 初始化内存池MemoryPoolManager::instance().initialize();// 使用通用分配器创建容器using MyAlloc = GeneralAllocator<int>;std::vector<int, MyAlloc> vec;for (int i = 0; i < 1000; ++i) {vec.push_back(i);}// 不同大小对象也能共享内存池std::list<std::string, GeneralAllocator<std::string>> str_list;for (int i = 0; i < 100; ++i) {str_list.emplace_back("Hello");}return 0;
}
📈 五、优势总结
特性 | 描述 |
---|---|
支持任意大小对象 | 根据大小选择最适合的内存池 |
减少内存碎片 | 同一类大小对象使用相同池 |
提高性能 | 避免频繁调用 new/delete |
可扩展性强 | 可以添加线程安全、日志等功能 |
兼容 STL | 作为分配器可插入任意容器 |
🧱 四、自定义分配器的优势
性能优化
通过自定义分配器,特别是实现内存池技术,可以显著减少内存碎片化并加快分配速度。例如,在游戏开发或高性能计算领域,使用定制化的内存池可以避免频繁的 new/delete
操作带来的性能损失。
资源隔离
不同的模块可以使用独立的分配器,从而避免不同模块间的内存污染问题。这对于大型项目尤其重要,可以帮助定位内存泄漏等问题。
日志和调试
可以在自定义分配器中添加日志记录功能,方便追踪内存分配和释放的情况,有助于检测潜在的内存泄漏或性能瓶颈。
🧪 五、总结
方案 | 优点 | 缺点 |
---|---|---|
使用默认分配器 | 简单易用,适用于大多数场景 | 可能在高并发或高性能要求下表现不佳 |
使用自定义分配器 | 可根据需求优化内存分配策略,提升性能 | 实现复杂,容易出错 |
对于大多数应用程序来说,默认分配器已经足够好。然而,在特定场景下(如高性能要求、资源受限环境),自定义分配器可以带来显著的好处。
📘 六、参考资料
- C++ Reference - std::allocator