C++:allocator类(动态数组续)

发布于:2025-03-30 ⋅ 阅读:(24) ⋅ 点赞:(0)

1.为什么需要 allocator?

在 C++ 中,动态内存管理通常通过 new 和 delete 完成:

int* p = new int;    // 分配内存 + 构造对象
delete p;            // 析构对象 + 释放内存

但 new 和 delete 有两个问题:

  1. 耦合性:将内存分配和对象构造合并为一个操作。

  2. 灵活性不足:无法适配不同的内存管理策略(如内存池、共享内存等)。

allocator 类的核心目标就是解耦内存分配和对象构造,提供更灵活的内存管理。


 2.allocator 的核心作用

std::allocator 是标准库容器(如 vectorlistmap 等)默认使用的内存分配器。它主要有以下作用:

1. 分离内存分配和对象构造
  • 分配内存:先分配原始内存块,但不构造对象。

  • 构造对象:在已分配的内存上手动构造对象。

  • 析构对象:手动析构对象,但不释放内存。

  • 释放内存:最终释放原始内存块。

#include <memory>

std::allocator<int> alloc;

// 1. 分配内存(未初始化)
int* p = alloc.allocate(5); // 分配 5 个 int 的空间

// 2. 构造对象
for (int i = 0; i < 5; ++i) {
    alloc.construct(p + i, i); // 在 p[i] 处构造 int 对象,值为 i
}

// 3. 析构对象
for (int i = 0; i < 5; ++i) {
    alloc.destroy(p + i);      // 析构 p[i] 处的对象
}

// 4. 释放内存
alloc.deallocate(p, 5);
2. 支持自定义内存管理策略

通过自定义 allocator,可以实现:

  • 内存池:预分配大块内存,减少碎片。

  • 共享内存:在进程间共享内存区域。

  • 性能优化:针对特定场景优化内存分配速度。


3.allocator 的典型使用场景

场景 1:标准库容器

所有标准库容器(如 std::vector)默认使用 std::allocator

template <class T, class Allocator = std::allocator<T>>
class vector {
private:
    T* data_ = nullptr;         // 指向动态数组的指针
    size_t size_ = 0;          // 当前元素数量
    size_t capacity_ = 0;      // 当前分配的内存容量
    Allocator allocator_;      // 内存分配器对象

public:
    // ... 保留已有构造函数 ...

    // 赋值运算符(拷贝并交换 idiom)
    vector& operator=(const vector& other) {
        if (this != &other) {
            vector temp(other); // 利用拷贝构造函数
            swap(*this, temp);  // 交换资源
        }
        return *this;
    }

    // 移动赋值运算符
    vector& operator=(vector&& other) noexcept {
        if (this != &other) {
            clear();
            allocator_.deallocate(data_, capacity_);
            
            data_ = other.data_;
            size_ = other.size_;
            capacity_ = other.capacity_;
            allocator_ = std::move(other.allocator_);
            
            other.data_ = nullptr;
            other.size_ = other.capacity_ = 0;
        }
        return *this;
    }

    // 交换两个vector(noexcept保证)
    friend void swap(vector& a, vector& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
        swap(a.capacity_, b.capacity_);
        swap(a.allocator_, b.allocator_);
    }

    // 添加emplace_back支持
    template <class... Args>
    void emplace_back(Args&&... args) {
        if (size_ >= capacity_) {
            reserve(capacity_ ? capacity_ * 2 : 1);
        }
        allocator_.construct(data_ + size_++, std::forward<Args>(args)...);
    }

    // 完善reserve的异常安全
    void reserve(size_t new_cap) {
        if (new_cap <= capacity_) return;
        
        T* new_data = allocator_.allocate(new_cap);
        size_t i = 0;
        try {
            for (; i < size_; ++i) {
                allocator_.construct(new_data + i, std::move_if_noexcept(data_[i]));
                allocator_.destroy(data_ + i);
            }
        } catch (...) {
            // 回滚已构造元素
            for (size_t j = 0; j < i; ++j) {
                allocator_.destroy(new_data + j);
            }
            allocator_.deallocate(new_data, new_cap);
            throw;
        }
        
        allocator_.deallocate(data_, capacity_);
        data_ = new_data;
        capacity_ = new_cap;
    }

    // ... 保留其他已有方法 ...
};

要点说明:

1. 使用分配器进行内存管理(allocate/deallocate)
2. 实现RAII原则,在析构函数中释放资源
3. 支持基础操作:push_back/pop_back/clear
4. 包含移动语义优化性能
5. 实现迭代器访问功能
6. 包含简单的扩容策略(容量翻倍)
这个只是简单模仿vector容器的核心机制,实际标准库实现会更复杂(包含异常安全、优化策略等很多东西)

场景 2:自定义内存分配策略

例如,实现一个简单的内存池:

template <typename T>
class MemoryPoolAllocator {
public:
    // 必需的类型定义(C++标准要求)
    using value_type = T;               // 分配的元素类型
    using pointer = T*;                 // 指针类型
    using const_pointer = const T*;     // 常指针类型
    using size_type = std::size_t;      // 大小类型
    using difference_type = std::ptrdiff_t; // 指针差异类型

    // 分配器传播特性(影响容器拷贝行为)
    using propagate_on_container_copy_assignment = std::true_type;
    using propagate_on_container_move_assignment = std::true_type;
    using propagate_on_container_swap = std::true_type;

    /**
     * @brief 默认构造函数(必须支持)
     * @note 需要保证同类型的不同allocator实例可以互相释放内存
     */
    MemoryPoolAllocator() noexcept = default;

    /**
     * @brief 模板拷贝构造函数(必须支持)
     * @tparam U 模板参数类型
     * @note 允许从其他模板实例化的allocator进行构造
     */
    template <typename U>
    MemoryPoolAllocator(const MemoryPoolAllocator<U>&) noexcept {}

    /**
     * @brief 内存分配函数(核心接口)
     * @param n 需要分配的元素数量
     * @return 指向分配内存的指针
     * @exception 可能抛出std::bad_alloc或派生异常
     */
    T* allocate(size_t n) {
        // TODO: 实现内存池分配逻辑
        // 建议方案:
        // 1. 计算总字节数 bytes = n * sizeof(T)
        // 2. 从内存池获取对齐的内存块
        // 3. 返回转换后的指针
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

    /**
     * @brief 内存释放函数(核心接口)
     * @param p 需要释放的内存指针
     * @param n 释放的元素数量
     * @note 必须保证p是通过allocate(n)分配的指针
     */
    void deallocate(T* p, size_t n) noexcept {
        // TODO: 实现内存池回收逻辑
        // 建议方案:
        // 1. 将内存块标记为空闲
        // 2. 返回内存池供后续重用
        ::operator delete(p);
    }

    /**
     * @brief 分配器比较函数(必须支持)
     * @note 不同实例是否应该被视为相等,需根据内存池实现决定
     */
    bool operator==(const MemoryPoolAllocator&) const noexcept { 
        return true; // 假设所有实例使用同一内存池
    }
    
    bool operator!=(const MemoryPoolAllocator&) const noexcept {
        return false;
    }

    /**
     * @brief 可选:对象构造函数(C++20前需要)
     * @tparam Args 构造参数类型
     */
    template <typename... Args>
    void construct(T* p, Args&&... args) {
        ::new(static_cast<void*>(p)) T(std::forward<Args>(args)...);
    }

    /**
     * @brief 可选:对象析构函数(C++20前需要)
     */
    void destroy(T* p) {
        p->~T();
    }
};
场景 3:避免默认初始化

默认的 new 会调用构造函数,而 allocator 可以先分配内存,再按需构造对象:

std::allocator<std::string> alloc;
std::string* p = alloc.allocate(3); // 仅分配内存,不构造对象

// 按需构造
alloc.construct(p, "hello");         // 构造第一个 string
alloc.construct(p + 1, "world");     // 构造第二个 string

4.allocator 的关键接口

以下是 std::allocator 的核心方法:

方法 作用
allocate(n) 分配 n 个对象的原始内存(未初始化)
deallocate(p, n) 释放内存(需先析构所有对象)
construct(p, args) 在位置 p 构造对象,参数为 args
destroy(p) 析构 p 处的对象

5.自定义 allocator 的要点

5.1. 必须提供的类型别名

自定义 allocator 需要定义以下类型:

template <typename T>
class CustomAllocator {
public:
    using value_type = T; // 必须定义
    // 其他必要类型...
};
5.2. 实现必要接口

至少需要实现 allocate 和 deallocate 方法:

T* allocate(size_t n) {
    return static_cast<T*>(::operator new(n * sizeof(T)));
}

void deallocate(T* p, size_t n) {
    ::operator delete(p);
}
5.3. 支持 rebind 机制

容器可能需要分配其他类型的对象(如链表节点的分配器):

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

6.C++17 后的改进

C++17 引入了 std::allocator_traits,简化了自定义 allocator 的实现。即使自定义分配器未实现某些接口,allocator_traits 会提供默认实现:

template <typename Alloc>
using allocator_traits = std::allocator_traits<Alloc>;

// 使用示例
auto p = allocator_traits<Alloc>::allocate(alloc, n);

7.总结

  • 核心作用:解耦内存分配与对象构造,提供更灵活的内存管理。

  • 默认行为:标准库容器使用 std::allocator

  • 自定义场景:内存池、性能优化、特殊内存区域(如共享内存)。

  • 关键接口allocatedeallocateconstructdestroy


网站公告

今日签到

点亮在社区的每一天
去签到