深入理解C++ 中的vector容器
一、引言
在C++ 的标准模板库(STL)中, vector 是一个极为常用且功能强大的序列容器。它就像是一个动态数组,既能享受数组随机访问元素的高效性,又能灵活地动态调整大小。在本文中,我们将深入探讨 vector 的方方面面,包括其基本概念、常用接口、迭代器相关问题以及空间增长策略等。
二、vector 基础介绍
2.1 定义与特性
vector 是表示可变大小数组的序列容器。它采用连续存储空间来存储元素,这使得我们可以像操作数组一样,通过下标高效地访问元素。与普通数组不同的是, vector 的大小能够动态改变,容器会自动处理内存管理相关事宜。
从本质上讲, vector 使用动态分配数组来存储元素。当新元素插入时,如果当前空间不足,它会分配一个新的数组,并将全部元素迁移到新数组中。虽然这个过程相对耗时,但 vector 并非每次插入新元素都进行这样的操作,以平衡效率。
2.2 构造函数
vector 提供了多种构造函数:
- 无参构造: vector() ,创建一个空的 vector 。例如: vector<int> v;
- 指定数量和初始值构造: vector(size_type n, const value_type& val = value_type()) ,构造并初始化 n 个值为 val 的元素。例如: vector<int> v(5, 10); 会创建一个包含5个元素,每个元素值为10的 vector 。
- 拷贝构造: vector(const vector& x) ,用于复制一个已有的 vector 。例如: vector<int> v1{1, 2, 3}; vector<int> v2(v1);
- 使用迭代器初始化构造: vector(InputIterator first, InputIterator last) ,使用迭代器范围初始化 vector 。例如: vector<int> v{1, 2, 3, 4, 5}; vector<int> v3(v.begin(), v.end());
三、vector 常用接口
3.1 增删查改接口
- 尾插(push_back):这是一个重点接口,用于在 vector 的尾部插入一个元素。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);return 0;}
- 尾删(pop_back):用于删除 vector 尾部的元素。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};v.pop_back();return 0;}
- 查找(find):需要注意的是, find 是算法模块实现,并非 vector 的成员接口。它用于在 vector 中查找特定元素。例如:
cpp#include <iostream>#include <vector>#include <algorithm>int main() {vector<int> v{1, 2, 3};auto it = find(v.begin(), v.end(), 2);if (it != v.end()) {std::cout << "找到元素" << std::endl;}return 0;}
- 插入(insert):在指定位置 position 之前插入值为 val 的元素。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};auto it = v.begin();v.insert(it, 0);return 0;}
- 删除(erase):删除指定位置 position 的数据。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};auto it = v.begin();v.erase(it);return 0;}
- 交换(swap):用于交换两个 vector 的数据空间。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v1{1, 2, 3};vector<int> v2{4, 5, 6};v1.swap(v2);return 0;}
- 像数组一样访问(operator[]):这也是重点接口,允许像访问数组元素一样访问 vector 中的元素。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};std::cout << v[1] << std::endl;return 0;}
3.2 容量相关接口
- size:用于获取 vector 中数据的个数。例如: vector<int> v{1, 2, 3}; std::cout << v.size() << std::endl;
- capacity:获取 vector 的容量大小。例如: vector<int> v{1, 2, 3}; std::cout << v.capacity() << std::endl;
- empty:判断 vector 是否为空。例如: vector<int> v; if (v.empty()) { std::cout << "vector为空" << std::endl; }
- resize:改变 vector 的大小。如果新大小大于原大小,会使用指定值(默认值为元素类型的默认值)填充新增位置;如果新大小小于原大小,会删除多余元素。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};v.resize(5, 8);return 0;}
- reserve:改变 vector 的容量,只负责开辟空间,不会对新空间进行初始化。例如: vector<int> v; v.reserve(10);
四、vector 迭代器
4.1 迭代器基本概念与使用
迭代器是 vector 与算法之间的桥梁,它使得算法无需关心底层数据结构。 vector 的迭代器本质上是原生态指针 T* 。
常用的迭代器相关接口有:
- begin + end: begin() 获取第一个数据位置的迭代器/常量迭代器, end() 获取最后一个数据的下一个位置的迭代器/常量迭代器 。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};for (auto it = v.begin(); it != v.end(); ++it) {std::cout << *it << " ";}return 0;}
- rbegin + rend: rbegin() 获取最后一个数据位置的反向迭代器, rend() 获取第一个数据前一个位置的反向迭代器 。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3};for (auto it = v.rbegin(); it != v.rend(); ++it) {std::cout << *it << " ";}return 0;}
4.2 迭代器失效问题
迭代器失效是使用 vector 时需要特别关注的问题。当迭代器底层对应指针所指向的空间被销毁,继续使用该迭代器就会导致程序崩溃。
可能导致 vector 迭代器失效的操作包括:
- 引起底层空间改变的操作,如 resize 、 reserve 、 insert 、 assign 、 push_back 等。以下是示例代码及解释:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3, 4, 5, 6};auto it = v.begin();// resize 操作,底层空间可能改变,迭代器失效v.resize(100, 8);// reserve 操作,改变扩容大小,可能使迭代器失效v.reserve(100);// insert 操作,插入元素可能引起扩容,导致迭代器失效v.insert(v.begin(), 0);// push_back 操作,可能引起扩容,导致迭代器失效v.push_back(8);// assign 操作,重新赋值可能引起底层容量改变,导致迭代器失效v.assign(100, 8);// 这里继续使用 it 迭代器会出错,因为 it 已经失效// 解决方法:在上述操作后,重新给 it 赋值,如 it = v.begin();while (it != v.end()) {std::cout << *it << " ";++it;}std::cout << std::endl;return 0;}
- erase 删除操作:删除元素后,迭代器也可能失效。例如:
cpp#include <iostream>#include <vector>int main() {vector<int> v{1, 2, 3, 4};auto it = v.begin();while (it != v.end()) {if (*it % 2 == 0) {// erase 操作后,it 迭代器失效,需要重新赋值it = v.erase(it); } else {++it;}}return 0;}
五、vector 空间增长策略
vector 的空间增长策略在不同编译器下有所不同。在VS(P版本STL)下, capacity 通常按1.5倍增长;在G++(SGI版本STL)下, capacity 按2倍增长 。以下是测试代码:
cpp// 测试vector的默认扩容机制void TestVectorExpand() {size_t sz;vector<int> v;sz = v.capacity();std::cout << "making v grow:\n";for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()) {sz = v.capacity();std::cout << "capacity changed: " << sz << '\n';}}}
了解这一策略有助于我们在编写代码时更好地预估 vector 的内存使用情况,并且在需要提前分配足够空间以避免频繁扩容时,可以使用 reserve 接口。例如:
cpp// 如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够// 就可以避免边插入边扩容导致效率低下的问题了void TestVectorExpandOP() {vector<int> v;size_t sz = v.capacity();v.reserve(100); std::cout << "making bar grow:\n";for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()) {sz = v.capacity();std::cout << "capacity changed: " << sz << '\n';}}}
六、vector 在OJ中的应用实例
6.1 只出现一次的数字
题目要求在给定的整数数组( vector<int> )中找到只出现一次的数字,其他数字都出现两次。解题思路是利用异或运算的性质:一个数异或它本身结果为0,异或0结果为它本身。代码如下:
cppclass Solution {public:int singleNumber(vector<int>& nums) {int value = 0;for (auto e : nums) {value ^= e;}return value;}};
6.2 杨辉三角
题目要求生成杨辉三角的前 numRows 行。解题的核心思想是找出杨辉三角的规律,即每一行头尾都是1,中间第 j 个数等于上一行第 j - 1 个数加上第 j 个数 。代码如下:
cppclass Solution {public:vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i) {vv[i].resize(i + 1, 1);for (int j = 1; j < i; ++j) {vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];}}return vv;}};
七、总结
vector 作为C++ STL中重要的容器之一,功能丰富且使用广泛。通过深入了解其基本概念、常用接口、迭代器特性以及空间增长策略等方面,我们能够更加得心应手地使用它来解决各种实际问题。无论是日常开发还是算法竞赛,熟练掌握 vector 的使用都能为我们带来极大的便利和效率提升。在实际编程中,要特别注意迭代器失效问题,合理运用其接口,以充分发挥 vector 的优势。