面试题答案
一键面试1. 内存池设计方案
- 固定大小块内存池:
- 数据结构:
- 定义一个结构体
MemoryBlock
来表示内存块,包含一个指向下一个内存块的指针next
。 - 定义一个类
MemoryPool
,包含一个指向内存块链表头的指针freeList
,以及一个用于分配内存的总大小totalSize
。
- 定义一个结构体
- 初始化:
在
MemoryPool
的构造函数中,根据需要预先分配一大块内存。将这块内存分割成固定大小的小块,并将这些小块通过next
指针链接成一个自由链表,freeList
指向链表头。 - 分配内存:
从
freeList
中取出第一个内存块返回给调用者。如果freeList
为空,则说明当前内存池已无可用内存,可选择抛出异常或者尝试扩展内存池。 - 释放内存:
将释放的内存块重新插入到
freeList
的头部,这样就完成了内存的回收。
- 数据结构:
示例代码如下:
#include <iostream>
#include <cstdlib>
struct MemoryBlock {
MemoryBlock* next;
};
class MemoryPool {
private:
MemoryBlock* freeList;
size_t totalSize;
public:
MemoryPool(size_t blockSize, size_t numBlocks) {
totalSize = blockSize * numBlocks;
char* buffer = new char[totalSize];
freeList = reinterpret_cast<MemoryBlock*>(buffer);
MemoryBlock* current = freeList;
for (size_t i = 0; i < numBlocks - 1; ++i) {
current->next = reinterpret_cast<MemoryBlock*>(reinterpret_cast<char*>(current) + blockSize);
current = current->next;
}
current->next = nullptr;
}
~MemoryPool() {
delete[] reinterpret_cast<char*>(freeList);
}
void* allocate() {
if (freeList == nullptr) {
// 这里可以选择抛出异常或扩展内存池
throw std::bad_alloc();
}
MemoryBlock* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* block) {
reinterpret_cast<MemoryBlock*>(block)->next = freeList;
freeList = reinterpret_cast<MemoryBlock*>(block);
}
};
- 多级内存池:
- 数据结构: 由多个不同大小的固定大小块内存池组成。例如,对于小对象(如小于64字节)使用一个内存池,对于中等大小对象(如64字节到256字节)使用另一个内存池,依此类推。
- 分配内存: 根据对象大小选择合适的内存池进行分配。如果某个内存池没有可用内存,可尝试从其他内存池或系统堆中分配(如果允许扩展)。
- 释放内存: 将释放的内存块归还给对应的内存池。
2. 关键问题及解决方案
线程安全
- 互斥锁:
- 方案:在
MemoryPool
类中添加一个互斥锁(如std::mutex
)。在allocate
和deallocate
方法中,在操作freeList
等共享资源前,先锁定互斥锁,操作完成后解锁。 - 示例代码:
- 方案:在
class MemoryPool {
private:
MemoryBlock* freeList;
size_t totalSize;
std::mutex poolMutex;
public:
//...
void* allocate() {
std::lock_guard<std::mutex> lock(poolMutex);
if (freeList == nullptr) {
throw std::bad_alloc();
}
MemoryBlock* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* block) {
std::lock_guard<std::mutex> lock(poolMutex);
reinterpret_cast<MemoryBlock*>(block)->next = freeList;
freeList = reinterpret_cast<MemoryBlock*>(block);
}
};
- 无锁数据结构:
- 方案:使用无锁数据结构(如无锁链表)来管理内存块。例如,使用
std::atomic
类型来实现无锁链表的指针操作,避免使用互斥锁带来的性能开销。但实现较为复杂,需要仔细处理并发访问的情况。
- 方案:使用无锁数据结构(如无锁链表)来管理内存块。例如,使用
内存对齐
- 计算对齐偏移:
- 方案:在分配内存时,计算需要的对齐偏移量。例如,如果系统要求8字节对齐,而当前内存地址不是8的倍数,计算需要偏移的字节数,然后从下一个8字节对齐的地址开始分配内存。
- 示例代码:
void* alignedAllocate(size_t size, size_t alignment) {
void* rawPtr = allocate();
size_t currentAddr = reinterpret_cast<size_t>(rawPtr);
size_t offset = alignment - (currentAddr % alignment);
if (offset == alignment) offset = 0;
char* alignedPtr = reinterpret_cast<char*>(rawPtr) + offset;
// 存储偏移量,以便释放时能正确找到原始内存块
*reinterpret_cast<size_t*>(alignedPtr - sizeof(size_t)) = offset;
return alignedPtr;
}
void alignedDeallocate(void* alignedPtr) {
size_t offset = *reinterpret_cast<size_t*>(reinterpret_cast<char*>(alignedPtr) - sizeof(size_t));
void* rawPtr = reinterpret_cast<char*>(alignedPtr) - offset;
deallocate(rawPtr);
}
- 使用aligned_alloc:
- 方案:在C++17及以上,可以使用
std::aligned_alloc
函数来分配对齐的内存。在内存池实现中,使用该函数来确保分配的内存满足对齐要求。
- 方案:在C++17及以上,可以使用
内存回收策略
- 立即回收:
- 方案:在
deallocate
方法中,将释放的内存块立即插入到freeList
中,以便下次分配时可以立即使用。这是最简单直接的回收策略,适合于内存需求相对稳定的场景。
- 方案:在
- 延迟回收:
- 方案:维护一个已释放内存块的临时列表。在内存池内存不足时,将临时列表中的内存块重新插入到
freeList
中。这样可以减少频繁的内存块插入操作,提高性能。同时,可以设置一个定时器,定期清理临时列表,确保内存能及时回收。
- 方案:维护一个已释放内存块的临时列表。在内存池内存不足时,将临时列表中的内存块重新插入到
- 合并相邻内存块:
- 方案:如果内存块在物理上相邻,可以在释放时将它们合并成一个更大的内存块。这需要在内存块结构中记录内存块的大小等信息,以便在释放时判断相邻内存块是否可以合并。这样可以减少内存碎片,提高内存利用率。