一文详解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_backpush_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的更多用法(例如自定义比较运算符),请参考此处

posted @ 2024-04-30 14:30  overxus  阅读(334)  评论(0编辑  收藏  举报