欢迎来到c++基础系列。
在之前的博客我们介绍了c++的string 类 ,并且对string 进行了部分模拟实现。
链接如下:
【C++】深入理解String类(一)
今天我们来讲解c++ 的vector 类。
目录
vector 介绍
vector 是序列容器,表示可以更改大小的数组
就像数组一样,vector 也采用连续存储空间来存储元素。 也就意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是与数组不同,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小
.vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的
与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好
vector 使用
1. vector 的构造函数
vector 的构造一般是4种:
构造函数声明 | 接口说明 |
---|---|
vector() | 无参构造 |
vector(size_type n, const value_type& val =value_type()) | 构造并初始化n个val |
vector(const vector& x) | 拷贝构造 |
vector(Inputlterator first,Inputiterator last) | 使用迭代器进行初始化构造 |
我们使用这些构造来试验以下:
void test_vector1()
{
//无参构造
vector<int> first;
//构造一个有四个 100 的容器
vector<int> second(4, 100);
//拷贝second
vector<int> fourth(second);
//使用其他容器的迭代器构造 【4个100】
vector<int> third(second.begin(), second.end());
}
其中涉及迭代器初始化的部分,之后我们会再拿出来讲解。
2. vector iterator 的使用
由于都是存储在连续的空间上,所以vector的迭代器和string类似。
iterator | 接口说明 |
---|---|
begin+end | 获取第一个数据位置的iterator/const iterator / 获取最后一个数据位置的iterator/const iterator |
rbegin+rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
着两组接口在之前的string 类中已经介绍的比较详细了,所以这里我们
不再赘述。
#include <iostream>
#include <vector>
using namespace std;
void PrintVector(const vector<int>& v){
// const对象使用const迭代器进行遍历打印
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main(){
// 使用push_back插入4个数据
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
// 使用迭代器进行遍历打印
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
// 使用迭代器进行修改
it = v.begin();
while (it != v.end())
{
*it *= 2;
++it;
}
}
3. vector的常用接口
vector 空间增长相关
容量空间 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector放入capacity |
上面这些接口在 之前的string类中都有讲过,我在只这里强调几点:
- capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL
- reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题
- resize在开空间的同时还会进行初始化,影响size
vector 的增删查改
vector增删查改 | 接口说明 |
---|---|
push_back (常用) | 尾插 |
pop_back (常用) | 尾删 |
find | 查找(这个是算法模块实现,不是vector的成员接口) |
insert | 中间插入 (在position 之前插入) |
erase | 删除postion 位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] (常用) | 像数组一样访问 |
这里面的大部分接口与string 中是相同的,所以这里我们不多做讲解,直接用例子来巩固一下:
// push_back/pop_back
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a+sizeof(a)/sizeof(int));
vector<int>::iterator it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
v.pop_back();
v.pop_back();
it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
// find / insert / erase
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 在pos位置之前插入30
v.insert(pos, 30);
vector<int>::iterator it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据
v.erase(pos);
it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
迭代器失效问题
在这里我们要注意一个问题:迭起失效:
问题引入
我们先来看一段代码:
vector<int>v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator pos =find(v.begin(),v.end(),2);
if(pos!=v.end())
{
v.insert(pos,20);
}
这段代码表示我们在 v 中 找寻 2,并且在2之前插入20。 显然。这段代码是没有问题的。
吃我们对 pos 位置的元素进行访问,应该是20:
cout<< *pos <<endl;
我们发现程序报错,此时pos的值从2变成了随机值。
很显然,这个问题是由于插入过程中增容导致的:
在vs 2019 环境下,增容是按照1.5倍式的:
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141
当vector 中已经有4个元素,再插入元素就会增容为6,而增容是开辟了一块新的有空间,再把原数据拷贝到新空间,所以pos 所指向的空间 在增容后已经被释放了,所以我们对pos 解引用得到的是随机值。
但是,如果在insert 过程空间足够,没有增容,pos 还是指向原来的空间,那么我们依旧认为pos失效。
因为失效时指: pos 的意义变化了,pos不再指向原来的值。
我们将insert 换作为 erase 再次验证一次:
vector<int>v;
v.reserve(2);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator pos = find(v.begin(), v.end(), 2);
if (pos != v.end())
{
v.erase(pos);
}
cout << *pos << endl;
按照推理,当我们删除了2,那么pos将指向3。
但是我们运行程序的时候:
我们是无法访问 pos 指针的。
所以 就像之前所说的:就算不增容,我们的pos也失效了。
erase 导致pos 失效,pos 没有野指针,只是意义变化了,但是 vs 版本之下进行了强制的检查,都不能访问。
同样的代码在g++ 运行,是不会报错的,说明两个环境的检查机制是不一样的。但是无论编译器是否报错,在erase(pos)之后,我们就认为pos 失效了,失效了之后就不要*pos 访问,可能会出现问题。
有的同学肯定会说,这里到底会导致说明样的问题?
我们举个例子来验证以下:
删除v中的所有偶数
vector<int>v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator it =v.begin();
while (it != v.end())
{
if(*it %2 == 0)
{
v.erase(it);
}
++it;
}
我们使用 gc++ 运行这段程序,发生了段错误:
当 it 指向 4 的时候,earse掉 4 之后,_finish–,it++,两者刚好错过,造成越界。
同时,如果 v中 数据为 1 2 2 3 4 5的时候,虽然不会有报错,但是得到的结果是错的。
问题解决
那么,我们如何解决迭代器失效?
其实很简单,c++ 在定义earse的时候 设置了返回值:会返回删除位置的下一个位置。
所以我们应该这样写:
vector<int>v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator it =v.begin();
while (it != v.end())
{
if(*it %2 == 0)
{
//erase 返回删除数据的下一个数据位置
it = v.erase(it);
}
else
{
++it;
}
}
同理 ,对于insert,也会返回插入元素的位置。