【C++】深入理解vector类(一)

本文讲解了C++中的vector容器,包括其构造函数、迭代器使用、常用接口如容量调整、增删查改,以及重点讨论了迭代器失效的问题及解决方案。通过实例演示了vector在动态扩容和迭代器操作中的行为和注意事项。
摘要由CSDN通过智能技术生成

欢迎来到c++基础系列。

在之前的博客我们介绍了c++的string 类 ,并且对string 进行了部分模拟实现。

链接如下:
【C++】深入理解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,也会返回插入元素的位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ornamrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值