1. allocator
分配器 (allocator) 是C++ STL库的基石之一,它是一种策略模式,允许用户将内存管理从容器中解耦出来,进行更具体化的操作。通过使用 allocator,我们可以自定义内存的分配和释放方式,从而可以更好地控制内存的使用。
1.1 为何使用allocator
在C++中,内存的申请和释放是一个昂贵的操作,频繁的申请和释放可能导致系统的内存碎片,使程序性能下降。通过使用allocator,我们可以自定义内存的申请和释放方式,减少系统的内存碎片,提高程序的性能。
此外,allocator还有一个重要的作用,那就是将对象的构造和内存的申请分开。在传统的内存申请方式中,我们在申请内存的同时就会调用对象的构造函数,但有时候,我们可能只是想申请内存,而不想立即构造对象,这时候,就可以使用allocator。
1.2 allocator的基本使用
在C++ STL中,allocator是一个模板类,我们可以通过为它提供一个类型参数来创建一个特定类型的allocator。以下是一个基本的例子:
#include <memory> int main() {
std::allocator<int> alloc; // 创建一个分配int的allocator int* p = alloc.allocate(10); // 分配10个int的空间 // 使用未构造的内存 for (int i = 0; i < 10; ++i) {
alloc.construct(p + i, i); // 在分配的内存上构造对象 } // 销毁对象并释放内存 for (int i = 0; i < 10; ++i) {
alloc.destroy(p + i); // 销毁对象 } alloc.deallocate(p, 10); // 释放内存 return 0; }
1.3 自定义分配器
通过自定义分配器,我们可以更灵活地控制内存的申请和释放。例如,我们可以将vector的数据直接存储到数据库、共享内存或者文件中,实现了数据的持久化和共享。
一个自定义分配器需要提供以下几个接口:
typedefs
:为使用的类型定义别名allocate(n)
:分配能容纳n个对象的内存deallocate(p, n)
:释放前面分配的内存construct(p, val)
:在指针p所指向的内存上构造一个对象,其值为valdestroy(p)
:销毁指针p所指向的对象
以下是一个简单的自定义分配器的例子:
template <class T> class MyAllocator {
public: typedef T value_type; MyAllocator() = default; template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
} T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n*sizeof(T))); } void deallocate(T* p, std::size_t) noexcept {
::operator delete(p); } }; template <class T, class U> bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) {
return true; } template <class T, class U> bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) {
return false; }
在这个例子中,我们创建了一个自定义的分配器MyAllocator
,这个分配器使用全局new
和delete
操作符来分配和释放内存。
2. 分配器概述
2.1 分配器的作用和重要性
分配器在C++中扮演着至关重要的角色,它们用于实现容器算法时,能够与存储细节隔离从而解耦合。使用分配器的优点在于,开发者可以专注于算法的实现,而无需关心内存的管理。不仅如此,分配器也为我们提供了存储分配与释放的标准方法,以及一些用于对象构造和销毁的函数。
2.2 STL中的标准分配器
C++ STL库中提供了一个标准的分配器:std::allocator
,它实现了最基本的内存分配和释放策略。在大多数情况下,它的性能已经足够高,但是在某些特殊情况下(例如大量小对象的分配和销毁),使用自定义的分配器可能会获得更好的性能。
2.3 分配器的使用
下面的代码展示了如何使用std::allocator
。其中,allocate
用于分配内存,construct
用于在已分配的内存上构造对象,destroy
用于销毁对象,deallocate
用于释放内存。需要注意的是,从C++17开始,construct
和destroy
函数已被废弃,我们需要使用std::allocator_traits
来调用构造和析构。
#include <memory> int main() {
std::allocator<int> alloc; // 创建一个分配int的allocator int* p = alloc.allocate(10); // 分配10个int的空间 // 使用未构造的内存 for (int i = 0; i < 10; ++i) {
std::allocator_traits<std::allocator<int>>::construct(alloc, p+i, i); } // 销毁对象并释放内存 for (int i = 0; i < 10; ++i) {
std::allocator_traits<std::allocator<int>>::destroy(alloc, p+i); } alloc.deallocate(p, 10); // 释放内存 return 0; }
上述代码中,首先我们创建了一个分配int的allocator,并分配了10个int的空间。然后,我们使用std::allocator_traits
的construct
方法在分配的内存上构造对象。最后,我们使用std::allocator_traits
的destroy
方法销毁对象,并使用deallocate
方法释放内存。
3. 自定义分配器
C++ STL库的灵活性主要源于其策略模式的设计,分配器就是这种设计的一个重要应用。通过自定义分配器,我们可以实现一些特殊的内存管理策略,比如内存共享、内存泄漏探测,预分配对象存储、内存池等。
3.1 自定义分配器的应用场景
以下列出了一些自定义分配器的应用场景:
- 内存共享:对于多进程或者多线程应用,我们可能需要共享内存空间。自定义分配器可以使我们将对象存储在共享内存中。
- 内存泄漏探测:在复杂的应用中,内存泄漏可能是一个难以定位的问题。自定义分配器可以帮助我们追踪内存的分配和释放,从而检测内存泄漏。
- 预分配对象存储:对于一些知道内存需求的应用,预先分配内存可以避免频繁的内存分配和释放,提高性能。
- 内存池:对于频繁分配和释放小块内存的应用,使用内存池可以减少内存碎片,提高性能。
3.2 自定义分配器的实现
一个自定义分配器需要实现以下几个接口:
typedefs
:为使用的类型定义别名allocate(n)
:分配能容纳n个对象的内存deallocate(p, n)
:释放前面分配的内存construct(p, val)
:在指针p所指向的内存上构造一个对象,其值为valdestroy(p)
:销毁指针p所指向的对象
下面的代码演示了如何实现一个自定义的分配器:
template <class T> class MyAllocator {
public: typedef T value_type; MyAllocator() = default; template <class U> constexpr MyAllocator(const MyAllocator<U>&) noexcept {
} T* allocate(std::size_t n) {
// 你的内存分配策略 } void deallocate(T* p, std::size_t) noexcept {
// 你的内存释放策略 } template<typename... Args> void construct(T* p, Args&&... args) {
// 你的对象构造策略 } void destroy(T* p) {
// 你的对象销毁策略 } }; template <class T, class U> bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) {
return true; } template <class T, class U> bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) {
return false; }
3.3 自定义分配器的使用
自定义分配器可以用于STL中的任何容器,包括vector、list等。以下是一个使用自定义分配器的vector的例子:
#include <vector> #include "MyAllocator.h" // 包含你的自定义分配器的头文件 int main() {
std::vector<int, MyAllocator<int>> vec; // 使用自定义分配器的vector vec.push_back(1); vec.push_back(2); vec.push_back(3); return 0; }
在这个例子中,我们创建了一个使用MyAllocator
的std::vector
。因此,这个vector
的内存管理策略将由我们的MyAllocator
来决定。同样的方法也可以应用于std::list
或其他STL容器。
4. 未初始化内存算法
在 C++ STL 中,有一系列的未初始化内存算法,这些算法用于在未初始化的内存上直接构造对象,可以提高程序的效率。这些算法的名称通常以 uninitialized_
开头,其中 uninitialized_copy
是最常用的一种。
4.1 uninitialized_copy 算法
uninitialized_copy
是一种用于在未初始化内存上复制序列的算法。它接受两个输入迭代器(定义了要复制的序列)和一个输出迭代器(定义了未初始化内存的起始位置),并尝试在输出范围内构造与输入序列相同的素。
以下是 uninitialized_copy
的基本用法:
#include <memory> #include <vector> int main() {
std::vector<int> vec {
1, 2, 3, 4, 5}; std::allocator<int> alloc; // 使用 allocator 分配未初始化内存 int* p = alloc.allocate(vec.size()); // 使用 uninitialized_copy 将 vec 中的素复制到未初始化的内存中 std::uninitialized_copy(vec.begin(), vec.end(), p); // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源 for (std::size_t i = 0; i < vec.size(); ++i) {
alloc.destroy(p + i); } alloc.deallocate(p, vec.size()); return 0; }
在上述代码中,我们首先创建了一个包含五个整数的 vector
。然后,我们使用 allocator
分配了一块足以存储 vector
中所有素的未初始化内存。接着,我们使用 uninitialized_copy
将 vector
中的素复制到这块未初始化的内存中。最后,我们遍历这块内存,对每个素调用 destroy
,然后调用 deallocate
释放整块内存。
需要注意的是,由于 uninitialized_copy
不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。
4.2 uninitialized_copy_n 算法
uninitialized_copy_n
是 uninitialized_copy
的一个变体,它接受一个输入迭代器(定义了要复制的序列的起始位置)、一个大小值n(定义了要复制的素数量)和一个输出迭代器(定义了未初始化内存的起始位置),并尝试在输出范围内构造与输入序列前n个相同的素。
以下是 uninitialized_copy_n
的基本用法:
#include <memory> #include <vector> int main() {
std::vector<int> vec {
1, 2, 3, 4, 5}; std::allocator<int> alloc; // 使用 allocator 分配未初始化内存 int* p = alloc.allocate(vec.size()); // 使用 uninitialized_copy_n 将 vec 中的前3个素复制到未初始化的内存中 std::uninitialized_copy_n(vec.begin(), 3, p); // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源 for (std::size_t i = 0; i < 3; ++i) {
alloc.destroy(p + i); } alloc.deallocate(p, vec.size()); return 0; }
在上述代码中,我们首先创建了一个包含五个整数的 vector
。然后,我们使用 allocator
分配了一块足以存储 vector
中所有素的未初始化内存。接着,我们使用 uninitialized_copy_n
将 vector
中的前3个素复制到这块未初始化的内存中。最后,我们遍历这块内存,对复制的每个素调用 destroy
,然后调用 deallocate
释放整块内存。
需要注意的是,由于 uninitialized_copy_n
不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。这点与 uninitialized_copy
是一样的。
4.3 uninitialized_fill 算法
uninitialized_fill
是一种在未初始化内存上填充值的算法。它接受两个迭代器(定义了未初始化内存的范围)和一个值,然后尝试在指定范围内构造这个值。
以下是 uninitialized_fill
的基本用法:
#include <memory> int main() {
std::allocator<int> alloc; // 使用 allocator 分配未初始化内存 int* p = alloc.allocate(5); // 使用 uninitialized_fill 将值42填充到未初始化的内存中 std::uninitialized_fill(p, p + 5, 42); // 使用完成后,需要手动调用 destructor 和 deallocate 释放资源 for (std::size_t i = 0; i < 5; ++i) {
alloc.destroy(p + i); } alloc.deallocate(p, 5); return 0; }
在上述代码中,我们使用 allocator
分配了一块可以存储5个整数的未初始化内存。然后,我们使用 uninitialized_fill
将值42填充到这块未初始化的内存中。最后,我们遍历这块内存,对每个素调用 destroy
,然后调用 deallocate
释放整块内存。
需要注意的是,由于 uninitialized_fill
不会自动调用 destructor 和 deallocate,所以我们需要手动调用它们以防止内存泄露。
其他的未初始化内存算法(uninitialized_fill_n
、uninitialized_default_construct
、uninitialized_value_construct
)用法与 uninitialized_copy
类似,也需要注意手动调用 destructor 和 deallocate 以防止内存泄露。
5. construct_at、destroy_at 对象构造和销毁
在 C++17 和 C++20 中,有两个非常重要的函数:std::construct_at
和 std::destroy_at
。这两个函数可以分别在给定的内存位置上构造和销毁对象。
5.1 std::construct_at
std::construct_at
是一种在指定内存位置上构造对象的方法。它接受一个指针和一系列构造函数参数,然后在指针指向的内存位置上构造一个对象。
以下是 std::construct_at
的基本用法:
#include <memory> struct MyStruct {
int x; float y; MyStruct(int x, float y) : x(x), y(y) {
} }; int main() {
std::allocator<MyStruct> alloc; // 使用 allocator 分配未初始化内存 MyStruct* p = alloc.allocate(1); // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象 std::construct_at(p, 42, 3.14f); // 使用完成后,需要手动调用 destroy_at 和 deallocate 释放资源 std::destroy_at(p); alloc.deallocate(p, 1); return 0; }
在上述代码中,我们首先定义了一个名为 MyStruct
的结构。然后,我们使用 allocator
分配了一块足以存储一个 MyStruct
对象的未初始化内存。接着,我们使用 construct_at
在这块未初始化的内存上构造一个 MyStruct
对象。最后,我们调用 destroy_at
销毁这个对象,然后调用 deallocate
释放整块内存。
5.2 std::destroy_at
std::destroy_at
是一种在指定内存位置上销毁对象的方法。它接受一个指针,然后调用该指针指向的对象的析构函数。
在上述代码的 std::construct_at
部分,我们已经展示了 std::destroy_at
的基本用法。这里再给出一个独立的例子:
#include <memory> struct MyStruct {
int x; float y; MyStruct(int x, float y) : x(x), y(y) {
} ~MyStruct() {
// 自定义析构函数 std::cout << "MyStruct object is being destroyed.\n"; } }; int main() {
std::allocator<MyStruct> alloc; // 使用 allocator 分配未初始化内存 MyStruct* p = alloc.allocate(1); // 使用 construct_at 在未初始化的内存中构造一个 MyStruct 对象 std::construct_at(p, 42, 3.14f); // 使用 destroy_at 销毁这个对象 std::destroy_at(p); // 使用完成后,需要手动调用 deallocate 释放资源 alloc.deallocate(p, 1); return 0; }
在上述代码中,我们首先定义了一个名为 MyStruct
的结构,它具有一个自定义的析构函数。然后,我们使用 allocator
分配了一块足以存储一个 MyStruct
对象的未初始化内存。接着,我们使用 construct_at
在这块未初始化的内存上构造一个 MyStruct
对象。接下来,我们调用 destroy_at
销毁这个对象,可以看到自定义析构函数的输出信息。最后,我们调用 deallocate
释放整块内存。
总的来说,std::construct_at
和 std::destroy_at
提供了一种方便、安全的方式在指定的内存位置上构造和销毁对象,与直接使用 new
和 delete
相比,它们提供了更好的控制,尤其是在处理未初始化的内存时。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/87040.html