文章目录
Hello~同学们好,本文将深入探讨 C++ 中的 vector 容器,作为标准模板库(STL)中最常用的动态数组之一,vector 提供了灵活的元素存储和高效的访问方法。我们将从基础知识入手,逐步学习其创建、初始化、遍历、空间管理以及增删查改等操作。通过详细的示例和解析,希望能够帮助读者全面理解和掌握 vector 的使用技巧和注意事项。
一:vector简介
vector文档
- vector是表示可变大小数组的序列容器。
- 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
- 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
- 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
- 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。
使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习
二:vector的创建和初始化
要包头文件#include<vector>
// 默认构造函数,创建一个空的 vector vector<int> v1; // 创建一个包含 4 个默认初始化元素(值为 0)的 vector vector<int> v2(4); // 创建一个包含 4 个元素,每个元素初始化为 10 的 vector vector<int> v3(4, 10); // 使用迭代器范围(v3 的起始和结束迭代器)初始化 vector,v4 将包含 v3 的所有元素 vector<int> v4(v3.begin(), v3.end()); // 拷贝构造函数,创建一个 v2 的副本 vector<int> v5(v2); // 使用初始化列表创建 vector,v6 将包含 1, 2, 3, 4, 5, 6, 7 这些元素 vector<int> v6 = {1, 2, 3, 4, 5, 6, 7}; // 使用 std::string 初始化 string s1("12345"); // 创建一个包含 "12345" 的字符串 s1 // 使用 std::string 的迭代器初始化 std::vector<int> // 这里每个 char 会隐式转换为其对应的 int(ASCII 值) vector<int> v3(s1.begin(), s1.end()); // 使用字符串的迭代器初始化 vector<int>
vector和string的区别:
std::vector:
- 不自动添加 \0:
- std::vector 只是一个通用的动态数组容器,它不会在末尾自动添加 \0。你需要手动管理字符串的结束标志。
- 当你需要将 std::vector 转换为 C 风格字符串时,你必须手动添加一个 \0。
std::string:
- 自动添加 \0:
- std::string 在内部管理一个以 \0 结尾的字符数组。这个空字符保证了字符串可以直接使用 C 风格字符串的函数。
- 当你创建或操作 std::string 对象时,\0 是自动添加和管理的,因此不需要手动处理。
三:vector的遍历
1.[]+下标
void test_vector1() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); for (size_t i = 0; i < v.size(); i++) { cout << v[i] << " "; } cout << endl; }
看一下结果(push_back后面会将,顾名思义也就是尾插)
调试一下看看
2.at()
at() 函数用于访问vector 中的元素,并进行边界检查。与 operator[] 不同,at() 会在访问越界时抛出 std::out_of_range 异常,因此它比 operator[] 更安全,但稍微有点性能开销。
#include <iostream> // 引入输入输出流头文件 #include <vector> // 引入 vector 容器头文件 #include <stdexcept> // 引入标准异常头文件 using namespace std; // 使用标准命名空间 int main() { vector<int> vec = { 10, 20, 30, 40, 50 }; // 初始化一个包含五个整数的 vector // 正常访问 vector 的元素 try { for (size_t i = 0; i < vec.size(); ++i) { cout << "Element at index " << i << " is " << vec.at(i) << endl; // 使用 at() 函数访问元素 } } catch (const out_of_range& e) { cerr << "Out of range error: " << e.what() << endl; // 捕捉并处理越界访问异常 } // 尝试访问越界元素 try { cout << "Element at index 10 is " << vec.at(10) << endl; // 访问索引为 10 的元素,这将抛出异常 } catch (const out_of_range& e) { cerr << "Out of range error————" << e.what() << endl; // 捕捉并处理越界访问异常 } return 0; // 返回 0 表示程序正常结束 }
3.迭代器遍历
iterator的使用 | 接口说明 |
---|---|
begin +end(重点) | 获取第一个数据位置的iterator/const_iterator 获取最后一个数据的下一个位置 的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator 获取第一个数据前一个位置的 reverse_iterator |
迭代器访问+修改
vector<int> v1; // 定义一个空的 vector 容器 v1.push_back(1); // 向容器中添加元素 1 v1.push_back(2); // 向容器中添加元素 2 v1.push_back(3); // 向容器中添加元素 3 v1.push_back(4); // 向容器中添加元素 4 vector<int>::iterator it = v1.begin(); // 初始化迭代器,指向 vector 的起始位置 // 使用 while 循环遍历 vector 的元素 while (it != v1.end()) { // 当迭代器未到达 vector 的末尾时继续循环 *it -= 10; // 将迭代器指向的元素值减去 10 cout << *it << " "; // 打印当前元素值 it++; // 迭代器指向下一个元素 } cout << endl; // 输出换行符
为什么 while (it != v1.end())不能用 it<v1.end()?
因为在 C++ 中,标准库容器(如 std::vector)的迭代器支持比较操作,但通常是使用 != 而不是 < 来判断迭代器是否已经到达容器的末尾。!= 比较更加直观和符合语义。
使用 < 进行比较可能在某些情况下无效,因为并不是所有迭代器都支持 < 运算符。特别是,对于双向迭代器或更复杂的迭代器(如关联容器中的迭代器),它们不支持这种操作。
注意:vector<int>::iterator it = v1.begin();
要用vector::指明是什么类型的迭代器。
4.范围for
void vector_Traversal_test() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); // 范围for for (auto e : v) { cout << e << " "; } cout << endl; for (auto& e : v) { e += 10; cout << e << " "; } cout << endl; //范围for实际上是使用迭代器来遍历容器的 //把上面两个展开其实是下面两个。 for (auto it = v.begin(); it != v.end(); ++it) { auto e = *it; // 通过迭代器获取当前元素 cout << e << " "; } cout << endl; for (auto it = v.begin(); it != v.end(); ++it) { auto& e = *it; // 通过迭代器获取当前元素的引用 e += 10; // 修改元素的值 cout << e << " "; } cout << endl; }
四:vector的空间
size | 获取数据个数 | |
max_size | 容器所能容纳的最大元素数量 | |
capacity | 获取容量大小 | |
resize | 改变vector的size | |
reserve | 改变vector的capacity | |
empty | 判断是否为空 |
1.size
size() 函数返回当前 vector 中的元素个数。
2.max_size
max_size() 函数返回 vector 可以容纳的最大元素数量,这个数量通常是一个非常大的值,取决于系统限制和内存可用性。
3.capacity
capacity() 函数返回当前 vector 内部存储空间的容量,即在重新分配之前可以存储的元素数量。
4.reserve
reserve(n) 函数用于请求 vector 预留足够的存储空间,以容纳至少 n 个元素。这样做可以减少因为容器扩展而导致的重新分配操作,提高插入元素的效率。
如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够,就可以避免边插入边扩容导致效率低下的问题了
void TestVectorExpandOP() { vector<int> v; size_t sz = v.capacity(); v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容 cout << "making bar grow:\n"; for (int i = 0; i < 100; ++i) { v.push_back(i); if (sz != v.capacity()) { sz = v.capacity(); cout << "capacity changed: " << sz << '\n'; } } }
5.resize
resize() 函数用于改变 vector 的大小,即容器中元素的数量。它可以根据传入的参数 n,进行不同的操作:
- 当 n < size 时:
- 容器尾部多余的元素会被销毁。
- capacity 不会改变。
- 当 size <= n <= capacity 时:
- 容器尾部新增加的元素被初始化为默认值(对于 int 类型,默认值是 0)。
- capacity 不会改变。
- 当 n > capacity 时:
- 容器尾部新增加的元素被初始化为默认值。
- size 和 capacity 都会变为 n,并且需要重新分配内存来扩展容器的存储空间。
6.empty
empty() 函数检查 vector 是否为空,如果 vector 中没有元素,则返回 true,否则返回 false。
五:vector的增删查改
push_back | 尾插 |
---|---|
pop_back | 尾删 |
find | 查找(注意这个是算法模块实现,不是vector的成员接口) |
insert | 在pos位置之前插入数据 |
erase | 删除pos位置的数据 |
swap | 交换两个vector的数据空间 |
assign | 用于将新值分配给向量的元素,替换当前内容,并修改向量的大小 |
1.push_back
vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); for (auto e : v) { cout << e << " "; } cout << endl; vector<string> v1; v1.push_back(" we"); v1.push_back(" all"); v1.push_back(" love"); v1.push_back(" C++"); for (auto e : v1) { cout <<e; } cout << endl;
2.pop_back
void test_vector5() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); for (auto e : v) cout << e << " "; cout << endl; v.pop_back(); // 尾删:3 for (auto e : v) cout << e << " "; cout << endl; v.pop_back(); // 尾删:2 for (auto e : v) cout << e << " "; cout << endl; }
3.find
思考:为什么string、map、set、都有自己的find而vector和list没有?
为什么 string、map、set 提供 find 操作?
- std::string:
- std::string 是一个字符序列,提供 find 操作用于子串查找,这是字符串操作中非常常见的需求。
- 例如,查找某个子串在字符串中的位置。
- std::map 和 std::set:
- std::map 和 std::set 是关联容器,基于平衡二叉树(如红黑树)实现。
- find 操作在这些容器中是核心功能,因为它们的主要用途就是快速查找键。
- std::map 提供键值对的查找,而 std::set 提供唯一键的查找。
- 这些容器的查找操作效率是 O(log n)。
为什么 vector 和 list 不提供 find 操作?- std::vector:
- std::vector 是一个动态数组,主要用于顺序存储和访问。
- 查找操作的效率是 O(n),因为需要线性扫描整个数组。
- 提供 find 操作在效率上不占优势,因此没有直接提供。
- std::list:
- std::list 是一个双向链表,适用于频繁插入和删除操作。
- 查找操作的效率同样是 O(n),因为需要线性扫描链表。
- 和 vector 类似,提供 find 操作在效率上不占优势,因此没有直接提供。
#include <algorithm> void test_vector9() { vector<int> v; v.push_back(9); v.push_back(9); v.push_back(6); vector<int>::iterator it = find(v.begin(), v.end(),6); if (it != v.end()) { cout << "找到啦" << endl; cout << *it << endl; } }
4.insert
void test_vector9() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); v.push_back(6); auto it1 = v.begin() + 2;//在第三个位置插入 v.insert(it1, 100); for (auto e : v) { cout << e << " "; }//1 2 100 3 4 5 6 cout << endl; auto it2 = v.begin() + 4; v.insert(it2, 3, 90);//在第五个位置插入3个90 for (auto e : v) { cout << e << " "; }//1 2 100 3 90 90 90 4 5 6 cout << endl; vector<int> vec1 = { 1, 2, 6 }; vector<int> vec2 = { 3, 4, 5 }; auto it3 = vec1.begin() + 2; // 在第三个位置插入元素 vec1.insert(it3, vec2.begin(), vec2.end()); // 在位置 it 处插入 vec2 的所有元素 // 现在 vec1 = {1, 2, 3, 4, 5, 6} }
注意:pos的类型都是iterator;
5.erase
vector<int> v {1, 2, 3, 4, 5}; // 删除第三个元素 auto it = v.erase(v.begin() + 2); // v = {1, 2, 4, 5} for (auto elem : v) { cout << elem << " "; } cout << endl; // 删除第二个到第四个元素 auto first = v.begin() + 1; auto last = v.begin() + 4; v.erase(first, last); // v = {1, 5} for (auto elem : v) { cout << elem << " "; } cout << endl; //注意:[first,last) 是左闭右开的区间所以last可以取v.begin() + 4
- 在调用 erase 后,被移除的元素会被析构,相关的内存也会被释放。
- 对于 erase(iterator first, iterator last),注意 first 和 last 的有效性和顺序,确保不会越界访问或者非法操作。
- 删除元素后,后续的元素会向前移动填补空缺,保持 vector 的连续性。
- erase 操作可能会导致迭代器失效,因此在使用返回的迭代器之前,要确保其仍然有效。
迭代器失效问题我会放在vector模拟实现(下一篇文章)详细讲解。
6.swap
std::vector 提供了一个成员函数 swap,用于交换两个 vector 对象的内容。这个操作可以快速地交换两个容器的元素,而不需要复制它们的内容。
vector<int> v1 {1, 2, 3}; vector<int> v2 {4, 5, 6}; // 使用 swap 交换两个 vector 的内容 v1.swap(v2); cout << "After swapping:\n"; cout << "v1: "; for (auto elem : v1) { cout << elem << " "; //4 5 6 } cout << "\nv2: "; for (auto elem : v2) { cout << elem << " "; //1 2 3 } cout << endl;
效果和注意事项
- 内容交换:swap 函数会交换两个 vector 对象的所有元素,包括它们的大小(size)和容量(capacity)。
- 高效性:swap 操作非常高效,因为它只涉及指针的交换,不需要复制元素。这对于大型的 vector 特别有用,可以在不重新分配内存的情况下快速交换数据。
- 迭代器和引用的影响:swap 操作不会使现有的迭代器、引用和指针失效,因此可以安全地在 swap 后继续使用交换后的 vector 对象。
- 使用场景:swap 可以用于重新排序或重新组织数据,也可以用于优化内存使用,比如在算法中交换两个 vector 来实现更高效的数据处理流程。
- 示例: 上面的示例展示了如何使用 swap 将两个 vector 对象的内容进行交换,从而在输出中显示了交换后的结果。
总之,std::vector 的 swap 函数是一个非常有用的工具,能够快速、高效地交换两个 vector 对象的内容,适合在需要优化内存使用或者重新组织数据时使用。
7.assign
void demonstrate_assign() { // 创建一个空的 vector<int> vector<int> v; // 使用 assign(n, value) 方法赋值 v.assign(5, 10); // 用5个10替换当前内容 cout << "After v.assign(5, 10): "; for (int i : v) { cout << i << " "; // 输出: 10 10 10 10 10 } cout << endl; // 使用 assign(first, last) 方法赋值 int arr[] = {1, 2, 3, 4, 5}; v.assign(arr, arr + 3); // 用数组的前3个元素替换当前内容 cout << "After v.assign(arr, arr + 3): "; for (int i : v) { cout << i << " "; // 输出: 1 2 3 } cout << endl; // 使用 vector 的迭代器范围赋值 vector<int> v2 = {7, 8, 9}; v.assign(v2.begin(), v2.end()); // 用v2的所有元素替换当前内容 cout << "After v.assign(v2.begin(), v2.end()): "; for (int i : v) { cout << i << " "; // 输出: 7 8 9 } cout << endl; }
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!