MST

星途 面试题库

面试题:C++中std::shared_ptr自定义分配器内存管理

实现一个`std::shared_ptr`的自定义分配器,该分配器从一个预先分配好的内存池(假设是一个`std::vector<char>`)中分配内存。当`std::shared_ptr`的引用计数为0时,内存要归还给内存池。请写出完整的自定义分配器类代码,并分析可能存在的内存泄漏风险以及如何避免。
39.2万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试
#include <memory>
#include <vector>
#include <iostream>

class MemoryPool {
public:
    MemoryPool(size_t size) : pool(size) {}

    void* allocate(size_t bytes) {
        if (nextFreeIndex + bytes <= pool.size()) {
            void* ptr = &pool[nextFreeIndex];
            nextFreeIndex += bytes;
            return ptr;
        }
        return nullptr;
    }

    void deallocate(void* ptr, size_t bytes) {
        // 简单实现,这里假设只有一种大小的分配,且释放顺序与分配顺序相反
        char* charPtr = static_cast<char*>(ptr);
        if (charPtr >= &pool[0] && charPtr + bytes <= &pool[nextFreeIndex]) {
            nextFreeIndex -= bytes;
        }
    }

private:
    std::vector<char> pool;
    size_t nextFreeIndex = 0;
};

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

    PoolAllocator(MemoryPool& pool) : pool(pool) {}

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

    T* allocate(size_t n) {
        return static_cast<T*>(pool.allocate(n * sizeof(T)));
    }

    void deallocate(T* p, size_t n) {
        pool.deallocate(p, n * sizeof(T));
    }

private:
    MemoryPool& pool;
};

template<typename T, typename U>
bool operator==(const PoolAllocator<T>&, const PoolAllocator<U>&) {
    return true;
}

template<typename T, typename U>
bool operator!=(const PoolAllocator<T>&, const PoolAllocator<U>&) {
    return false;
}

内存泄漏风险分析及避免方法:

  1. 风险

    • 如果MemoryPool::allocate函数返回nullptr(例如内存池空间不足),而调用者没有正确处理这种情况,可能会导致后续代码尝试使用空指针,这不是严格意义上的内存泄漏,但会导致程序崩溃。
    • MemoryPool::deallocate函数中,如果释放的指针不在内存池的有效范围内,或者释放的大小与分配的大小不一致,可能会导致内存池内部状态混乱,后续分配可能出现错误,甚至可能出现内存访问越界等问题,间接导致内存泄漏。
  2. 避免方法

    • 在调用PoolAllocator::allocate后,调用者应检查返回值是否为nullptr,如果是,应进行适当处理(如抛出异常或返回错误码)。
    • MemoryPool::deallocate函数中,可以增加更严格的边界检查,确保释放的指针确实是从该内存池分配的,并且大小与分配时一致。例如,可以在分配时记录每个分配块的大小和起始位置,在释放时进行验证。另外,对于释放顺序与分配顺序无关的复杂情况,可以使用更复杂的数据结构(如链表)来管理内存池中的空闲块。