一文详解C++的vector
vector是C++中使用频率最高的标准库,可以在程序运行时动态改变其大小(例如添加或删除元素),因此又被称为动态数组。使用时,用户无需在意底层内存管理的细节,因为它已经帮你做了这件事情。使用前需要导入<vector>
头文件,以下是vector的常见用法:
1. 创建vector
vector用于保存一组同类型的数据,在创建时需要指定元素的类型:
vector<int> a; // 创建一个int类型的vector
vector<Student> student_list; // 创建一个Student类型的vector(Student是用户自定义的类型)
在实际使用中,常常需要创建指定大小的数组,例如创建一个大小为n的vector:
vector<float> v(n); // 创建一个大小为n的float型vector
可以在创建时指定一个初值,从而vector中的所有元素都被初始化为这个初值:
vector<float> f2(n, -3.14f); // 创建大小为n的float型vector,其中所有的元素都被初始化为3.14
另外一种常用的方式是使用给定的若干值创建vector,例如:
vector<double> d {-2.4, 1.3, 2.5, 4.5};
另外,二维数组又被称为一维数组的数组,例如,可以按照如下方式创建一个大小为NxN的二维数组:
vector<vector<int>> matrix(N, vector<int>(N)); // 创建一个大小为NxN的二维数组
vector<vector<int>> matrix2(N, vector<int>(N, -1)); // 创建大小为NxN的二维数组,且所有元素都被初始化为-1
这一写法并没有什么好惊讶的,只是vector<int>
作为类型被传入vector中,而vector<int>(N)
只是一个用于初始化的值而已。再比如说,创建一个大小为mxn的矩阵:
vector<vector<float>> matrix3(m, vector<float>(n));
2. 访问元素
得益于C++的运算符重载,可以使用数组运算符[]
访问vector中的元素:
vector<int> v {1, 2, 3, 4};
int a = v[0]; // 访问v的第0个元素
v[2] = -5; // 修改v的第2个元素
另外,vector提供了两个特殊的方法:front()
和back()
,分别用于访问vector的第一个和最后一个元素:
int b = v.front(); // 读取v的第0个元素
v.back() = -9; // 修改v的最后一个元素
使用下标访问vector时,最需要注意的是下标的越界问题,如果使用[]访问越界,那么得到的结果是未定义的,这取决于编译器的具体实现;或者使用vector的at()
方法,此方法会检查下标访问是否越界,如果越界,或抛出std::out_of_range
异常:
int c = v[6]; // v的大小为4,下标为6的元素不存在,这一表达式的结果未定义
v.at(6); // 访问的下标越界,抛出std::out_of_range异常
3. 获取大小
vector实际上是带有内存管理功能的数组,为了避免频繁地分配和释放内存(这些是非常耗时的操作),vector会在分配时额外多分配一些内存,当vector中元素的个数减少到一定程度后才会释放内存。具体实现是比较复杂的内存管理策略,我们无需关心,只要记住下面这张图就好:
使用size()
方法可以获取vector中元素的个数,使用capacity()
可以获取vector实际占据的内存个数。
resize()
方法可以改变vector的大小,例如:
vector<int> v1 {1, 2, 3, 4};
v1.resize(2); // v1 = {1, 2}
vector<int> v2 {5, 6, 7};
v2.resize(4); // v2 = {5, 6, 7, 0}
vector<int> v3 {5, 6, 7};
v3.resize(4, -9); // v3 = {5, 6, 7, -9}, 可以指定一个初值,当resize后的大小变大时,使用指定的初值初始化新增加的元素
使用clear()
方法可以清空vector (size()
为0,但capacity()
不一定为0):
v.clear();
4. 尾端插入/删除元素
正是由于这种内存布局,使得向vector的尾端插入一个元素是一个十分高效的操作,通过push_back()
方法实现此功能:
v.push_back(12);
提示
emplace_back
与push_back
的功能相同,都是向vector的末尾插入一个元素,但在具体实现上有所差异。至于何时使用emplace_back
性能更好,解释这一点需要C++的其它知识,在此不作说明,有实际需要的朋友可自行研究。
另外还有pop_back()
方法,用于删除vector末尾的元素:
v.pop_back();
不过需要注意的是,pop_back()
的返回值为空,也就是说,它不会返回vector的最后一个元素,仅执行删除末尾元素的功能。
5. 遍历vector
遍历vector有两种方法,一种是使用下标遍历:
int n = v.size();
for (int i = 0; i < n; i++) {
cout << v[i];
}
另外,由于vector实现了迭代器方法,因此可以通过for ... in ... 循环遍历:
for (int element : v) {
cout << element;
}
或者直接获取迭代器的开始和结束位置,自行实现遍历功能:
for (auto i = v.begin(); i != v.end(); i++) {
cout << *i;
}
如果需要逆序遍历,使用.rbegin()
和.rend()
即可,在此不再赘述。
6. 任意位置插入/删除元素
插入和删除操作都需要借助迭代器,如果需要操作v的第i个元素,首先需要通过v.begin()
或begin(v)
获取迭代器开始的位置,再通过v.begin() + i
获取第i个元素的位置,之后对这个位置的元素进行插入或删除操作。
插入操作通过insert
方法实现:
vector<int> v = {1, 2, 3, 4};
v.insert(v.begin() + 2, 10); // 在第2个位置插入10,插入后,v = 1, 2, 10, 3, 4
使用insert
方法不仅可以向指定位置插入单个元素,也可以插入一个区间内的元素,这同样是通过迭代器完成的。例如:
vector<int> v1 {1, 2, 3, 4};
vector<int> v2 {-8,-10};
v1.insert(begin(v1) + 1, v2.begin(), v2.end()); // 向v1的第1个位置插入整个v2
以上插入操作的图示如下:
使用insert
方法,可以将两个vector合并:
v1.insert(v1.end(), v2.begin(), v2.end());
提示
为了保证vector可以通过下标实现随机访问,向第i个位置插入元素,会导致从第i个元素起的所有元素后移,这是一项非常耗时的操作,在具体使用时务必注意这一点。
删除操作通过erase
方法实现:
v.erase(v.begin() + 2); // 删除v[2]
同样地,可以使用erase
删除指定区间内的所有元素:
vector<int> v {1, 3, 5, 7, 9};
v.erase(begin()+1, begin()+3); // 这里删除的是v[1]和v[2], begin()+3处的元素不会被删除。删除后, v = {1, 7, 9};
7. 与旧的API交互
在C++项目中,如有可能,应尽量使用vector替代原生数组。然而,使用C++编程,难免会与一些旧时的API打交道,例如下面这个API:
void old_api(int* arr, int size);
如果vector需要调用上述API,使用data()
可以获取底层数组的首地址,也就是说,可以按照如下方式调用上述API:
old_api(v.data(), v.size());
8. 排序
vector的排序功能是通过C++标准算法库中的sort
方法支持的,使用前需要导入头文件<algorithm>
. 最简单的使用方法如下:
sort(v.begin(), v.end());
关于sort的更多用法(例如自定义比较运算符),请参考此处。