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
相比,它们提供了更好的控制,尤其是在处理未初始化的内存时。