1 std::vector 的高级特性
1.1 std::vector 的扩容与收缩
std::vector 的扩容和收缩主要涉及到内存分配和重新分配。
扩容
当 std::vector 中的元素数量超过其当前容量时,std::vector 会自动扩容以容纳更多的元素。扩容通常是通过分配一个更大的内存块,将旧内存块中的元素复制到新内存块,然后释放旧内存块来实现的。
扩容的大小通常是当前容量的两倍,但这并不是固定的,具体实现可能因标准库的不同而有所差异。扩容操作可能会导致额外的性能开销,因为它涉及到内存分配、复制和释放。
收缩
当 std::vector 中的元素数量减少时,它可能会收缩以减少内存使用。然而,std::vector 并不总是立即收缩。在某些实现中,std::vector 可能会保留一些额外的容量,以便在将来添加元素时能够避免频繁的扩容操作。这种行为可以通过 std::vector 的 shrink_to_fit 成员函数来改变。
shrink_to_fit 成员函数尝试减少 std::vector 的容量以匹配其当前大小。但是,这并不总是成功的,因为具体实现可能会选择保留一些额外的容量。
性能考虑
扩容和收缩操作可能会导致性能问题,尤其是在涉及大量元素时。为了减少这些开销,可以考虑在创建 std::vector 时预先指定一个合理的初始容量,或者在添加大量元素之前使用 reserve 成员函数预留足够的空间。
此外,如果 std::vector 的大小经常变化,并且需要特别关注内存使用,可以考虑使用其他容器,如 std::deque,它在添加和删除元素时具有更好的性能特性。
如下是一个考虑到性能的 std::vector 扩容与收缩的样例:
#include <iostream>
#include <vector>
int main()
{
// 创建一个 std::vector,并预分配足够的空间
std::vector<int> vec;
// 假设知道最终将会有大约 1000 个元素
vec.reserve(1000);
// 向 vector 中添加元素
for (int i = 0; i < 1000; i++) {
vec.push_back(i);
}
// 此时,vector 的大小是 1000,容量至少也是 1000
std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
// 如果确定不再需要添加更多元素,可以调用 shrink_to_fit 来尝试减少容量
vec.shrink_to_fit();
// 注意:shrink_to_fit 不一定总是能减少容量,这取决于具体实现
std::cout << "Size after shrink_to_fit: " << vec.size() << ", Capacity after shrink_to_fit: " << vec.capacity() << std::endl;
// 如果需要再次添加大量元素,可以再次使用 reserve
vec.clear(); // 清除现有元素
vec.reserve(2000); // 假设现在知道最终将会有大约 2000 个元素
for (int i = 0; i < 2000; i++) {
vec.push_back(i);
}
// 再次输出大小和容量
std::cout << "Size after adding 2000 elements: " << vec.size() << ", Capacity after adding 2000 elements: " << vec.capacity() << std::endl;
return 0;
}
上面代码的输出为:
Size: 1000, Capacity: 1000
Size after shrink_to_fit: 1000, Capacity after shrink_to_fit: 1000
Size after adding 2000 elements: 2000, Capacity after adding 2000 elements: 2000
在上面代码中,首先预分配了足够的空间来容纳大约 1000 个元素。这避免了在添加这些元素时进行多次扩容操作。然后,当确定不再需要添加更多元素时,调用了 shrink_to_fit 来尝试减少 vector 的容量。需要注意的是,shrink_to_fit 不保证一定能减少容量,这取决于标准库的具体实现。
最后,当需要再次添加大量元素时,再次使用 reserve 来预分配足够的空间。这样可以确保在添加新元素时不会频繁地扩容。
通过合理地使用 reserve 和 shrink_to_fit,可以提高 std::vector 的性能(尤其是在处理大量数据时)。
1.2 std::vector 的内存优化与性能调优
std::vector 的内存优化和性能调优主要涉及到减少内存分配和释放的次数,以及利用 std::vector 的特性来有效地管理内存。以下是一些优化 std::vector 的方法:
(1)预分配内存:
使用 reserve 方法预先为 std::vector 分配足够的内存空间。这样可以避免在添加元素时频繁地进行内存重新分配和元素复制。例如,如果知道最终将会有 N 个元素,则可以在开始时调用 vec.reserve(N)。
(2)避免小对象分配:
对于小对象,使用 std::vector<char[]> 或 std::vector<std::aligned_alloc<>> 可以减少内存分配的开销。这是因为 std::vector 通常会为每个元素分配单独的内存块,而对于小对象来说,这种分配可能过于昂贵。通过将小对象作为字符数组存储在 std::vector 中,可以更有效地利用内存。
(3)减少扩容次数:
扩容操作可能非常昂贵,因为它涉及到分配新内存、复制元素和释放旧内存。通过减少添加和删除元素的次数,你可以减少扩容发生的频率。考虑使用其他数据结构(如 std::deque)来避免不必要的扩容。
(4)避免不必要的拷贝:
当将一个 std::vector 赋值给另一个时,默认情况下会进行深拷贝。如果不需要拷贝元素,而是希望共享同一个内存块,可以考虑使用引用、指针或移动语义。例如,使用 std::vector<std::shared_ptr<T>> 可以共享所有权,而 std::move 可以用于转移所有权。
(5)使用 emplace 和 emplace_back:
emplace 和 emplace_back 方法允许在容器内部直接构造元素,而不是先构造元素然后再将其复制到容器中。这可以避免额外的拷贝或移动操作。
(6)清理无效内存:
当从 std::vector 中删除元素时,内存并不会立即释放。如果此时需要立即释放内存,可以使用 std::vector::shrink_to_fit。然而需要注意的是,shrink_to_fit 不保证一定会减少容量。
(7)选择正确的迭代器类型:
使用正确的迭代器类型(如 const_iterator、reverse_iterator 等)可以避免不必要的拷贝和转换,从而提高性能。
(8)避免不必要的范围检查:
如果确定索引是有效的,使用 operator[] 而不是 at 方法可以避免范围检查。然而,这可能会增加出现未定义行为的风险,所以请谨慎使用。
(9)自定义分配器:
对于更高级的内存管理优化,可以自定义一个分配器(allocator),以便更好地控制内存的分配和释放。这允许使用特殊的内存池、缓存策略或其他优化手段。
(10)考虑使用其他容器:
如果 std::vector 的性能不符合程序的需求,可以考虑使用其他容器,如 std::deque、std::list 或 std::forward_list,它们具有不同的内存分配和性能特性。
在进行性能优化时,建议使用性能分析工具(如 gprof、Valgrind 的 Callgrind、Intel VTune 等)来识别瓶颈,并根据实际情况进行针对性的优化。此外,编写清晰、简洁和可维护的代码同样重要,因为过度优化可能会使代码难以理解和维护。
如下是一个使用 std::move 避免 std::vector 赋值时的元素拷贝过程:
#include <iostream>
#include <vector>
// 自定义的类,用于演示构造函数和析构函数
class CustomObject
{
public:
// 构造函数
CustomObject() {
std::cout << "CustomObject constructor called." << std::endl;
}
// 拷贝构造函数
CustomObject(const CustomObject& other) {
std::cout << "CustomObject copy constructor called." << std::endl;
}
// 析构函数
~CustomObject() {
std::cout << "CustomObject destructor called." << std::endl;
}
// 一些成员函数或数据成员...
};
int main()
{
// 创建一个包含自定义对象的std::vector
std::vector<CustomObject> source;
source.push_back(CustomObject()); // 添加一些对象到源向量中
source.push_back(CustomObject());
// 打印源向量的内容(此时会调用构造函数)
std::cout << "Source vector before move." << std::endl;
// 创建一个目标向量
std::vector<CustomObject> destination;
// 使用std::move将源向量的所有权转移给目标向量
destination = std::move(source);
// 打印目标向量的内容(此时不会调用构造函数,因为对象是被移动的)
std::cout << "Destination vector after move." << std::endl;
// 源向量现在处于有效但未定义的状态,不应再使用
// 如果尝试访问它的元素,可能会导致未定义行为
// 当源向量和目标向量离开作用域时,它们的析构函数会被调用
// 由于使用了移动语义,只有目标向量的元素会被析构,源向量的元素不会被析构(因为它们已经被移动走了)
return 0;
}
上面代码的输出为:
CustomObject constructor called.
CustomObject copy constructor called.
CustomObject destructor called.
CustomObject constructor called.
CustomObject copy constructor called.
CustomObject copy constructor called.
CustomObject destructor called.
CustomObject destructor called.
****** Source vector before move. ******
****** Destination vector after move. ******
CustomObject destructor called.
CustomObject destructor called.
在上面代码中给,由于使用了移动语义,source 向量中的 CustomObject 实例被移动到 destination 向量中,而没有进行任何拷贝。当 source 和 destination 向量离开作用域时,只有 destination 向量中的对象会被析构,因为它们的所有权已经被转移了。
2 std::vector 的实战应用
2.1 std::vector 在排序算法中的应用
std::vector 提供了随机访问迭代器,这意味着可以使用 std::vector 高效地实现各种排序算法。在排序算法中,std::vector 通常用作待排序数据的存储容器。
以下是一些在 C++ 中使用 std::vector 实现排序算法的例子:
(1)使用 std::sort 函数:
C++ 标准库提供了 std::sort 函数,该函数可以对 std::vector 中的元素进行排序。这是最简单且最常用的排序方法。
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> vec = { 2, 4, 3, 5, 1 };
std::sort(vec.begin(), vec.end());
for (const auto& num : vec)
{
std::cout << num << " ";
}
return 0;
}
上面代码的输出为:
1 2 3 4 5
(2)针对自定义类型对象的排序:
当需要对包含自定义对象的 std::vector 进行排序时,需要提供一个比较函数或比较对象,告诉 std::sort 如何比较这些对象。比较函数或对象应该接受两个参数(即你要比较的两个对象)并返回一个布尔值,表明第一个参数是否应该在排序后位于第二个参数之前。
如下是样例代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// 自定义的Person类
class Person
{
public:
Person(const std::string& name, int age) : name(name), age(age) {}
public:
std::string name;
int age;
};
// 比较函数,用于std::sort
bool comparePersonsByAge(const Person& a, const Person& b) {
return a.age < b.age; // 升序排序
// 如果要降序排序,则返回 a.age > b.age;
}
int main()
{
// 创建一个包含Person对象的std::vector
std::vector<Person> peoples = {
{"zhangsan", 21},
{"lisi", 20},
{"wangwu", 32}
};
// 使用std::sort和自定义的比较函数对vector进行排序
std::sort(peoples.begin(), peoples.end(), comparePersonsByAge);
// 打印排序后的vector
for (const auto& person : peoples) {
std::cout << person.name << std::endl;
}
return 0;
}
上面代码的输出为:
lisi
zhangsan
wangwu
comparePersonsByAge 函数是一个比较函数,它接受两个 Person 对象作为参数,并根据它们的 age 成员变量进行比较。这个函数用于告诉 std::sort 如何排序 people 向量中的 Person 对象。
另外,如果不想单独写一个比较函数,也可以使用 C++11 引入的 Lambda 表达式来提供比较逻辑:
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
return a.age < b.age; // 升序排序
});
这种方式更为简洁,并且可以直接在调用 std::sort 的地方定义比较逻辑。
(3)实现自定义排序算法:
std::vector 作为数据的存储容器,也可以实现自定义的排序算法,如冒泡排序、选择排序、插入排序、快速排序、归并排序等。
以下是使用 std::vector 实现冒泡排序的示例:
#include <iostream>
#include <vector>
void bubbleSort(std::vector<int>& vec)
{
int n = vec.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (vec[j] > vec[j + 1]) {
std::swap(vec[j], vec[j + 1]);
}
}
}
}
int main()
{
std::vector<int> vec = { 2, 4, 3, 5, 1 };
bubbleSort(vec);
for (const auto& num : vec) {
std::cout << num << " ";
}
return 0;
}
上面代码的输出为:
1 2 3 4 5
(4)使用其他 STL 算法:
除了 std::sort,C++ 标准库还提供了许多其他算法,如 std::nth_element(用于找到第 k 小的元素)、std::partial_sort(用于部分排序)、std::stable_sort(用于稳定排序)等。这些算法都可以与 std::vector 配合使用。
注意事项
对于大型数据集,使用 std::sort 通常比使用简单的排序算法(如冒泡排序)更高效。
当使用自定义排序算法时,注意优化算法以提高性能,特别是在处理大型数据集时。
std::vector 提供了随机访问迭代器,这使得使用许多 STL 算法变得高效。但是,对于某些算法(如堆排序),可能需要使用其他容器,如 std::priority_queue。
2.2 std::vector 在动态数组处理中的应用
std::vector 在动态数组处理中的应用非常广泛,因为它提供了一个灵活且易于使用的接口来管理动态数组。std::vector 允许在运行时动态地添加和删除元素,而无需手动管理内存。以下是一些 std::vector 在动态数组处理中的常见应用:
(1)动态数据集合:
std::vector 可以作为动态数据集合来使用,并在其中存储任意类型的元素。这种情况下,std::vector 提供了一种便捷的方式来存储、访问和修改元素。
std::vector<int> intVector; // 存储整数的动态数组
std::vector<std::string> strVector; // 存储字符串的动态数组
(2)动态数据结构:
std::vector 可以作为实现其他动态数据结构的基础,如栈、队列等。
栈
可以使用 std::vector 来实现一个栈(后进先出)数据结构:
std::vector<int> stack;
// 入栈
stack.push_back(10);
// 出栈
if (!stack.empty()) {
int top = stack.back();
stack.pop_back();
}
队列
同样,也可以使用 std::vector 来实现一个队列(先进先出)数据结构,但通常 std::deque 是更好的选择,因为它提供了更高效的队列操作。
std::vector<int> queue;
// 入队
queue.push_back(10);
// 出队
if (!queue.empty()) {
int front = queue.front();
queue.erase(queue.begin());
}
(3)动态内存管理:
使用 std::vector 可以避免手动管理动态数组所需的内存分配和释放。当向 std::vector 添加元素时,它会自动分配更多的内存(如果需要的话)。同样,当从 std::vector 中删除元素时,它会释放不再需要的内存。
(4)高效的元素访问:
std::vector 提供了随机访问迭代器,这意味着可以通过索引直接访问任何位置的元素,这通常比链表等数据结构更快。
std::vector<int> vec = {1, 2, 3, 4, 5};
int secondElement = vec[1]; // 访问第二个元素,值为2
3 std::vector 的扩展与自定义
std::vector 在内部封装了动态数组的实现。在大多数情况下,不需要对其做扩展,因为它已经提供了非常完整的功能集。然而,如果需要更高级的功能或定制行为,则可以考虑以下几种方法:
(1)继承自 std::vector:
可以通过继承 std::vector 并添加自定义成员函数来扩展其功能。然而,这通常不是推荐的做法,因为继承标准库组件可能导致未定义的行为或意外的副作用。标准库组件通常设计为不可继承的,并且它们的内部实现可能会在不同版本的编译器或标准库中发生变化。
(2)使用适配器模式:
适配器模式允许将一个类的接口转换为另一个类的接口,而不需要修改原始类。可以创建一个适配器类,它封装了一个 std::vector 实例,并提供了自定义的接口。这样,可以在不修改 std::vector 本身的情况下添加新功能。
(3)自定义迭代器:
std::vector 允许使用自定义迭代器。开发人员可以创建自己的迭代器类,该类提供对 std::vector 元素的访问,并在迭代过程中添加自定义逻辑。然后,可以将这些自定义迭代器传递给 std::vector 的成员函数,以在遍历元素时应用自定义行为。
(4)使用代理类:
代理类是一个设计模式,它允许一个类代表另一个类的功能,并在调用功能时添加额外的逻辑。你可以创建一个代理类,它封装了一个 std::vector 实例,并提供与 std::vector 相同的接口。在代理类的实现中,可以在调用 std::vector 的方法之前或之后添加自定义逻辑。
(5)自定义分配器:
std::vector 使用分配器来管理内存。可以通过提供一个自定义的分配器来定制 std::vector 的内存分配行为。自定义分配器可以允许你控制内存的分配策略,例如使用内存池、共享内存或其他高级内存管理技术。
4 实现一个简单的 std::vector 容器
如下是一个简单的自定义 std::vector 容器,它只包含最基本的构造函数、析构函数、push_back 和 size 成员函数。这个简化的版本没有包含迭代器、容量管理、内存优化等高级特性。
#include <iostream>
#include <new> // for placement new
template <typename T>
class SimpleVector
{
public:
SimpleVector() : m_data(nullptr), m_size(0), m_capacity(0) {}
SimpleVector(const SimpleVector& other) : m_size(other.m_size), m_capacity(other.m_capacity) {
m_data = new T[m_size];
for (size_t i = 0; i < size; i++) {
m_data[i] = other[i]; // Copy elements
}
}
SimpleVector(SimpleVector&& other) noexcept : m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
~SimpleVector() {
clear();
}
SimpleVector& operator=(const SimpleVector& other) {
if (this != &other) {
clear();
m_size = other.m_size;
m_capacity = other.m_size;
m_data = new T[m_size];
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = other.m_data[i]; // Copy elements
}
}
return *this;
}
SimpleVector& operator=(SimpleVector&& other) noexcept {
if (this != &other) {
clear();
m_data = other.m_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
return *this;
}
T& operator[](size_t i) {
return m_data[i];
}
void push_back(const T& value) {
if (m_size == m_capacity) {
reallocate(m_capacity ? m_capacity * 2 : 1); // Double the capacity if necessary
}
new (m_data + m_size) T(value); // Use placement new
m_size++;
}
size_t size() const {
return m_size;
}
size_t capacity() const {
return m_capacity;
}
void clear() {
for (size_t i = 0; i < m_size; i++) {
m_data[i].~T(); // Call destructors
}
delete[] m_data;
m_data = nullptr;
m_size = 0;
}
private:
T* m_data;
size_t m_size;
size_t m_capacity;
void reallocate(size_t newSize)
{
if (newSize == 0) {
delete[] m_data;
m_data = nullptr;
m_capacity = 0;
return;
}
T* newData = new T[newSize];
for (size_t i = 0; i < m_size; i++) {
new (newData + i) T(std::move(m_data[i])); // Use placement new and move constructor
}
delete[] m_data;
m_data = newData;
m_capacity = newSize;
}
};
int main()
{
SimpleVector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
for (size_t i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;
return 0;
}
这个简化的 SimpleVector 类实现了构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符、析构函数、push_back 和 size 成员函数。注意,这个实现假设 T 类型有默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。此外,它使用了“就地构造”(placement new)来在已分配的内存中构造对象,并显式调用析构函数来销毁对象。