在C++程序运行过程中,使用new、delete或者malloc、free进行动态内存操作时,如果申请和释放的内存块大小不固定、频率过高,就会产生内存碎片,分为内部碎片和外部碎片两类,会直接影响程序的内存使用效率和运行稳定性。

内存碎片的产生与影响
内部碎片是指分配的内存块大小大于实际申请的大小,多余的部分无法被其他申请使用,造成浪费。外部碎片是指内存中存在大量不连续的小空闲块,即使这些块的总大小足够满足新的申请需求,也因为不连续无法被分配出去。
频繁的内存碎片会导致程序可用内存越来越少,甚至触发内存分配失败的错误,同时碎片整理也会消耗额外的CPU资源,降低程序整体性能。
自定义分配器解决内存碎片
自定义分配器是C++标准库支持的内存管理扩展方式,通过重写分配和释放逻辑,可以适配特定的内存使用场景,减少碎片产生。
自定义分配器核心实现
自定义分配器需要符合C++标准分配器的接口要求,核心是实现allocate和deallocate方法,以下是一个简单的固定大小内存块的自定义分配器示例:
#include <cstddef>
#include <iostream>
// 固定大小内存块的自定义分配器,模板参数T为分配的元素类型,BlockSize为每次分配的内存块大小
template <typename T, std::size_t BlockSize = 1024>
class FixedSizeAllocator {
public:
using value_type = T;
// 构造函数
FixedSizeAllocator() noexcept = default;
// 拷贝构造函数
template <typename U>
FixedSizeAllocator(const FixedSizeAllocator<U, BlockSize>&) noexcept {}
// 分配n个T类型的内存
T* allocate(std::size_t n) {
if (n != 1) {
// 仅支持分配单个元素,简化实现
throw std::bad_alloc();
}
// 实际分配BlockSize大小的内存,避免小内存申请产生过多碎片
void* ptr = ::operator new(BlockSize);
std::cout << "分配内存块,大小: " << BlockSize << " 字节" << std::endl;
return static_cast<T*>(ptr);
}
// 释放p指向的内存
void deallocate(T* p, std::size_t n) noexcept {
if (n != 1) {
return;
}
::operator delete(p);
std::cout << "释放内存块" << std::endl;
}
// 判断两个分配器是否相等
bool operator==(const FixedSizeAllocator&) const noexcept { return true; }
bool operator!=(const FixedSizeAllocator&) const noexcept { return false; }
};
// 测试自定义分配器
int main() {
// 使用自定义分配器创建vector,vector内部的内存申请会走自定义分配逻辑
std::vector<int, FixedSizeAllocator<int, 256>> vec;
vec.push_back(10);
vec.push_back(20);
return 0;
}
这个分配器每次分配固定大小的内存块,适合申请固定大小对象的场景,减少了不同大小内存块交错申请释放产生的外部碎片。
内存池技术解决内存碎片
内存池是预先分配一大块连续内存,后续的内存申请都从这块内存中划分,释放时也不直接归还给系统,而是放回内存池复用,从根源上减少系统调用和碎片产生。
简单内存池实现示例
以下是一个固定大小对象的内存池实现,支持对象的快速分配和释放:
#include <cstddef>
#include <iostream>
#include <vector>
// 固定大小对象内存池
template <typename T, std::size_t PoolSize = 1024>
class ObjectMemoryPool {
private:
union MemoryNode {
T data; // 存储对象数据
MemoryNode* next; // 指向下一个空闲节点
MemoryNode() : next(nullptr) {}
};
MemoryNode* memory_block; // 内存池首地址
MemoryNode* free_list; // 空闲链表头
std::size_t allocated_count;// 已分配数量
public:
ObjectMemoryPool() : free_list(nullptr), allocated_count(0) {
// 预分配一大块内存
memory_block = static_cast<MemoryNode*>(::operator new(sizeof(MemoryNode) * PoolSize));
// 初始化空闲链表,将所有节点串起来
for (std::size_t i = 0; i < PoolSize; ++i) {
memory_block[i].next = free_list;
free_list = &memory_block[i];
}
}
~ObjectMemoryPool() {
::operator delete(memory_block);
}
// 分配一个对象内存
T* allocate() {
if (free_list == nullptr) {
throw std::bad_alloc();
}
// 从空闲链表取一个节点
MemoryNode* node = free_list;
free_list = free_list->next;
allocated_count++;
// 调用对象构造函数
new (&node->data) T();
return &node->data;
}
// 释放对象内存
void deallocate(T* ptr) {
// 将节点放回空闲链表
MemoryNode* node = reinterpret_cast<MemoryNode*>(ptr);
// 调用对象析构函数
ptr->~T();
node->next = free_list;
free_list = node;
allocated_count--;
}
// 获取已分配数量
std::size_t get_allocated_count() const {
return allocated_count;
}
};
// 测试内存池
struct TestObject {
int id;
TestObject() : id(0) {}
};
int main() {
ObjectMemoryPool<TestObject, 10> pool;
std::vector<TestObject*> objs;
// 分配5个对象
for (int i = 0; i < 5; ++i) {
TestObject* obj = pool.allocate();
obj->id = i;
objs.push_back(obj);
}
std::cout << "已分配对象数量: " << pool.get_allocated_count() << std::endl;
// 释放所有对象
for (auto obj : objs) {
pool.deallocate(obj);
}
std::cout << "释放后已分配对象数量: " << pool.get_allocated_count() << std::endl;
return 0;
}
这个内存池预先分配了连续的内存块,所有对象的内存都来自这块区域,避免了频繁向系统申请内存,同时相同大小的对象复用内存节点,大幅减少了内存碎片的产生。
两种方案的适用场景
自定义分配器更适合需要和标准容器结合使用的场景,只需要替换容器的分配器模板参数即可生效,改动成本低。内存池技术更适合高频、固定大小对象的分配场景,性能优势更明显,但需要自己管理内存池的生命周期。
实际项目中可以根据内存使用的特点选择方案,也可以将两者结合,用自定义分配器封装内存池的逻辑,同时适配标准库的接口要求。