%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% # allocator 分配器相关 C++11 中,引入了两个类模版来提供统一的**动态内存管理**接口。 - `std::allocator`:分配器类,**封装了==分配、释放==动态内存的过程**。 - `std::allocator_traits`:分配器访问类,**提供了统一的、分配器类型无关的接口来 "访问分配器的属性与方法"**。 > [!faq] ❓为什么需要 allocator? > > `std::allocator` 将 "**内存分配**" 与 "**对象构造**" 进行了分离,其分配的内存是 "**==未构造的==**"。**同时,将 "对象析构" 与 "内存释放" 也进行了分离**。 > > - `new` 将 "**内存分配**" 与 "**对象构造**" 组合在了一起; > - `delete` 将 "**对象析构**" 与 "**内存释放**" 组合在了一起。 > > <br><br> # `std::allocator` 类 > `std::allocator` 位于 `<memory>` 头文件 `std::allocator<T>` 是一个类模版,其提供了一种 "**类型感知**" 的**内存分配/释放接口**,封装了底层的内存管理细节[^1]。 allocator 分配内存时,会根据**对象类型**来确定**恰当的 "内存大小" 和 "==对齐位置=="**。 > [!info] `std::allocator` 是 STL 容器的**默认内存管理器**,负责**为容器中的对象分配原始内存,构造或析构对象**。 > 此外,STL 容器允许使用自定义的分配器,以实现特定的内存管理策略,例如使用池分配器来提供性能或跟踪内存使用情况。 > [!important] `std::allocator` 使用 **底层的 `operator new` 和 `operator delete`** 来进行内存分配和释放。 > > 因此,当其分配内存失败时,其行为即 `operator new` 的行为——**会抛出 `std::bad_alloc` 异常**。 ### 使用说明 ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-allocator.assets/IMG-cpp-allocator-83249F8D5030D7F33DC9D1FD04D24E16.png|770]] `std::allocator<>` 类没有成员变量,其包含四个成员函数: - `.allocate(n)`:分配**足够存储 `n` 个元素的==未构造原始内存==**,返回指向这块内存的指针。 - `.construct(p, args)`:在`p`指向的**已分配的原始内存**上**构造一个对象**; - `.destroy(p)`:**析构 `p` 所指对象**; - `.deallocate(p, n)`:**释放** `p` 所指的,由 `allocate` 分配的内存; 使用方式为: - 先通过 `.allocate()` **分配足够的原始内存**,然后使用 `.construct()` 在原始内存上**逐个构造对象**; - 先通过 `.destroy()` **逐个析构对象**,最后通过 `.deallocate()` **释放所申请的所有内存**; > [!caution] `construct()` 和 `destroy()` 在 C++20 中以及被移除,可有两种方式改写: > > - **推荐使用 `std::allocator_traits` 类的包装来访问分配器的属性和方法**,而不是直接使用分配器的成员函数。 > - 使用 `std::construct_at()` 直接在地址内存地址构造对象,替换 `.construct()`;<br>同时,通**过直接调用类类型的析构函数**来替代 `destroy()`。 > 使用示例: ```cpp title:allocator_exam.cpp #include <memory> #include <iostream> int main() { // 创建一个std::allocator<int>对象 std::allocator<int> alloc; // 可动态分配`int`类型的allocator对象. // 申请足够的连续内存空间, 返回指向该内存空间的指针(元素int类型) int* arr = alloc.allocate(5); // 可容纳5个int元素的连续内存空间. 返回指向这片内存空间的指针 // 在原始内存空间上构造元素 for (int i = 0; i < 5; ++i) { // alloc.construct(arr + i, i); //c++20中已移除. // c++20中, 使用`std::construct_at()`直接在指定内存地址构造对象 std::construct_at(arr + i, i); } // 使用数组... for (int i = 0; i < 5; ++i) { std::cout << arr[i] << " "; } std::cout << std::endl; // 析构并释放内存 // for (int i = 0; i < 5; ++i) { // alloc.destory(arr + i); // C++20中已移除. // } // 基本类型没有析构函数, 直接释放内存即可. // 对于用户自定义的类类型, 手动调用析构函数进行释放: `arr[i].~Type();` alloc.deallocate(arr, 5); } ``` <br> #### 拷贝和填充未初始化内存的算法 > 位于头文件 `<memory>` 标准库为 allocator 类提供了几个 "**伴随算法**",用于在**由 allocator 分配的、未初始化的 "指定内存地址"** 上 "**==构造元素==**"(以 "**拷贝构造**" 的方式), ![[_attachment/02-开发笔记/01-cpp/内存管理/cpp-allocator.assets/IMG-cpp-allocator-0F23429952C9F8CF38C0C5A11AB1C920.png|721]] 使用示例: ```cpp vector<int> vec { 1, 2, 3, 4, 5, 6}; allocator<int> alloc; auto p = alloc.allocate(vec.size() * 2); // 分配空间, vec.size()的2倍 auto q = unitialized_copy(vec.begin(), vec.end(), p); // 拷贝vec中的元素, 从p所指内存位置开始构造元素, 返回指向拷贝后最后一项元素之后位置的指针 uninitialized_fill_n(q, vi.size(), 42); // 从q所指内存位置, 以值42, 构造vi.size()个元素, 返回空. ``` <br><br><br> # `std::allocator_traits` 类模版 `std::allocator_traits` 提供了**统一的、与分配器类型无关的接口**来**访问分配器类的各种成员属性和方法**。 通过使用该类,可以编写 "**与分配器类型无关**" 的代码,支持对 "**遵循 C++标准分配器接口的旧式分配器**" 以及 "**带有扩展接口的自定义分配器**" 的访问。 `std::allocator_traits<T>` 以 "**分配器类**" 作为模版参数,提供了一系列**静态成员类型别名和函数**,包括: - 支持 "**查询分配器类型**" 的**属性** - 如 `value_type`、`pointer`、`size_type` 等 - **包装了 "分配器操作"的静态成员函数**(与 `std::allocate` 的成员函数同名): - `allocate`、 `deallocate`、 `construct`、 `destroy` 等; 示例:`std::allocator_traits` 与 `std::allocator` 的搭配使用 ```cpp title:allocator_traits_exam.cpp #include <memory> // 包含std::allocator和std::allocator_traits #include <iostream> int main() { // 创建一个std::allocator<int>分配器实例 std::allocator<int> alloc; // 标准分配器 // 使用std::allocator_traits<> 来包装对std::allocator<int>的访问 // using traits = std::allocator_traits<std::allocator<int>>; using traits = std::allocator_traits<decltype(alloc)>; // 与上语句等效 // 分配原始内存 size_t size = 5; int* arr = traits::allocate(alloc, size); // 足以存储5个int对象的原始内存空间. // 在分配的原始内存上构造对象. for (int i = 0; i < size; ++i) { // 参数: alloc, 对象的地址, 对象构造函数的参数 traits::construct(alloc, arr + i, i); } // 使用分配的对象 for (int i = 0; i < size; ++i) { std::cout << "Value: " << arr[i] << std::endl; } // 查询分配器的属性 // typeid().name()返回的是编译器特定的类型名称, 通常不是"人类可读"的 // 实际应用中, 这些类型信息主要用于模版编程和类型推导中. std::cout << "Pointer type:" << typeid(traits::pointer).name() << '\n'; std::cout << "Size type:" << typeid(traits::size_type).name() << '\n'; std::cout << "Difference type:" << typeid(traits::difference_type).name() << '\n'; std::cout << "Value Type: " << typeid(traits::value_type).name() << '\n'; // 析构对象 for (int i = 0; i < size; ++i) { traits::destroy(alloc, arr + i); } // 释放内存 traits::deallocate(alloc, arr, size); } ``` <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《C++ Primer》P429