STL库(vector)逐步分析vector( 包含常用的接口的使用讲解)

个人主页:小则又沐风

个人专栏:<数据结构>

<竞赛专栏>

<C语言>
<C++>

座右铭

路虽远,行则将至;事虽难,做则必成

目录

前言

构造函数

迭代器

有关容量的接口

迭代器失效

insert

erase

assign

二维数组

总结:


前言

在之前的文章中我们了解了string的使用和模拟实现,他对应的是在C语言中的字符串数组,今天我们要学的是这个vector就是可以理解为可以存储各种类型的数组

构造函数

先来看看vector的构造函数吧

老规矩我们先来看看默认构造函数的行为是怎么样的

vector <int> v

指明是vector的类型 vector将要存储数据的类型 创建出vector的名字

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	return 0;
}

我们通过这监视窗口可以看到一点这个vector的内部结构,可以猜想到是一个指针指向一片连续的空间也就是存储着数据的地方,还有两个变量分别是_size和_capacity分别表示存储的有效的数据个数,空间的大小

可以看到无参数构造的行为就是构造出一个空间大小为0的空vector

下面来看看这个构造函数的行为

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
int main()
{
	/*vector<int> v;*/
	vector <int> v1(10, 9);
	return 0;
}

那么这个构造函数的行为就是用n个数据来构造

但是在这个构造函数可以发现这个函数的参数居然是一个迭代器,这个是怎么使用的呢?

在这里先解释一下为什么要设计这个构造函数,这是因为在之后需要学习的容器或者是新的数据结构,他们都会支持迭代器的访问,那么如果我们需要把一个容器中的数据构造到另一种的容器中的话,就可以使用这个构造函数就行了

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/
	set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());
	return 0;
}

在这里我使用了set这个数据结构他是由二叉搜索树树为基础实现的,在之后我会讲解并且进行模拟实现,在这里不多讲解

最后一个构造函数就是特殊的拷贝构造了

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);
	return 0;
}

在这里的a的构造是C++11支持的引入的列表初始化 + 调用 initializer_list 构造函数

通俗的理解就是把我们传入的数据通过列表初始化然后构造出一个这样的类型的临时对象,然后传递到这个构造函数的参数,进行构造 (是不是觉得这样的构造函数香的一批)

构造函数的内容就告一段落了

迭代器

当你看到这张图片的时候,就可以知道这个容器的迭代器是双向的迭代器,支持正向和反向迭代器

在之前我们就知道了迭代器的使用,在这里只是简单的讲解使用

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int>::iterator it = a.begin();
	while (it != a.end())
	{
		cout << *it << ' ';
		it++;
	}
	return 0;
}

有关容量的接口

常用的有

size

capacity

empty

reserve

resize

在string中我们知道reserve对于小于原有的capacity的时候什么都不会发生,只会在需要扩容的时候起到作用

那么在vector中呢?

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
template<class T>
void print(const vector<T>& a)
{
typename vector<T>::const_iterator it = a.begin();
	while (it != a.end())
	{
		cout << *it << ' ';
		it++;
	}
}
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	vector<int>a(20, 0);
	a.reserve(5);
	a.reserve(21);
	return 0;
}

在执行第一个语句后的capacity是20

执行我们想要缩容的操作的时候还是20

所以这个reserve在vector的行为是和string一样的

那么resize呢?

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
template<class T>
void print(const vector<T>& a)
{
typename vector<T>::const_iterator it = a.begin();
	while (it != a.end())
	{
		cout << *it << ' ';
		it++;
	}
}
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	vector<int>a(20, 0);
	//a.reserve(5);
	//a.reserve(21);
	a.resize(10, 9);
	print(a);
	return 0;
}

有缩容的功能

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
template<class T>
void print(const vector<T>& a)
{
typename vector<T>::const_iterator it = a.begin();
	while (it != a.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
}
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	vector<int>a(20, 0);
	//a.reserve(5);
	//a.reserve(21);
	a.resize(10, 9);
	print(a);
	a.resize(30, 9);
	print(a);
	return 0;
}

也有扩容的功能

接下来我们来看看vector的扩容的机制

cpp 复制代码
// 测试vector的默认扩容机制
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

运行结果如图:

我们可以发现这个扩容的机制并不是我们想象的2倍扩容,更多的是像1.5倍

这时候我们了解了扩容的相关的操作,下面我们来认识一下什么叫做迭代器失效

迭代器失效

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
using namespace std;
template<class T>
void print(const vector<T>& a)
{
typename vector<T>::const_iterator it = a.begin();
	while (it != a.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
}
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	//vector<int>a(20, 0);
	////a.reserve(5);
	////a.reserve(21);
	//a.resize(10, 9);
	//print(a);
	//a.resize(30, 9);
	//print(a);
	vector<int>a(10, 8);
	auto it = a.begin();
	a.reserve(20);
	cout << *it;
	return 0;
}

我们来看上面的这串代码,这串代码的运行结果会是什么呢?

是编译错误还是运行崩溃,还是正常呢?

运行看看

可以看到编译并没有报错

那么运行起来呢?

怎么回事???怎么崩溃了?

这就是因为迭代器失效造成的问题,我们来仔细分析一下我们的这串代码,首先创建出了一个vector数组然后我们用一个迭代器指向了这个数组的开始,然后我们对这个vector进行了扩容的处理,

然后尝试进行对迭代器的访问.

问题出现了,我们在之前的string中知道reserve的底层就是再创建出一片新的空间然后把旧空间的数据拷贝到新的空间中然后!!!释放掉旧空间,我们把旧的空间释放掉了,但是我们迭代器还是指向旧空间的指针,这不是成为了野指针吗???

所以迭代器失效了

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器 底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。

那么都是什么样的操作会造成迭代器失效的风险呢?

会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、 assign、push_back等。

下面就来边介绍这些操作,边介绍引起迭代器失效的场景(resize,reserve不再讲解)

insert

和之前的一样 就是在pos的位置插入一个数据

那么这个为什么会引起迭代器的失效的问题呢?

我们可以看到这个insert的返回值是一个迭代器我们先来看看这个迭代器是指向的什么

cpp 复制代码
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	//vector<int>a(20, 0);
	////a.reserve(5);
	////a.reserve(21);
	//a.resize(10, 9);
	//print(a);
	//a.resize(30, 9);
	//print(a);
	/*vector<int>a(10, 8);
	auto it = a.begin();
	a.reserve(20);
	cout << *it;*/
	/*TestVectorExpand();*/
	vector<int>a({ 1,2,3,4,5,6,7 });
	auto it=a.insert(a.begin()+2, 100);
	cout << *it << endl;
	return 0;
}

可以看到返回的迭代器是指向插入数据的迭代器

那么下面我将会演示一下迭代器失效的场景

这是插入前后的capacity的变化我们可以看到发生了扩容的操作.

那么如果有一个迭代器指向着之前的空间中的位置

cpp 复制代码
int main()
{
	/*vector<int> v;*/
	/*vector <int> v1(10, 9);*/ 
	/*set<string> s({ "apple","banana","pear","orange" });
	vector <string> v2(s.begin(), s.end());*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	vector<int> v(a);*/
	/*vector<int> a({ 1,2,3,4,5,6,7 });
	print(a);*/
	//vector<int>a(20, 0);
	////a.reserve(5);
	////a.reserve(21);
	//a.resize(10, 9);
	//print(a);
	//a.resize(30, 9);
	//print(a);
	/*vector<int>a(10, 8);
	auto it = a.begin();
	a.reserve(20);
	cout << *it;*/
	/*TestVectorExpand();*/
	vector<int>a({ 1,2,3,4,5,6,7 });
	auto it2 = a.end();
	auto it=a.insert(a.begin()+2, 100);
	//cout << *it << endl;
	*it2++;
	return 0;
}

可以这样不出我们的意料程序崩溃了

erase

erase支持的是传入一个迭代器那么他就会删除这个迭代器上的数据,如果传入的是两个参数的话就是删除这个区间上的数据

erase的代码就不展示了,下面我们来看一下这个会导致迭代器失效的代码

cpp 复制代码
void eraseerror()
{
	int a[] = { 1,2,3,4 };
	vector<int> v(a, a + sizeof(a) / sizeof(a[0]));
	auto it = v.begin();
	v.erase(v.begin());
	cout << *it;
}

的确是崩溃了,但是这个erase按道理说应该不会崩溃啊我们删除了第一个数据,那么这个begin还是会指向下一个数据啊,为什么会失效呢?

这是因为erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理 论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end 的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素 时,vs就认为该位置迭代器失效了

下面我们来看一串有关这个erase的代码

cpp 复制代码
void text1()
{
	vector<int> v{ 1, 2, 6,3, 4,5 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
			it=v.erase(it);
		++it;
	}
	print(v);
}
void text2()
{
	vector<int> v{ 1, 2, 6,3, 4,5 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
			it = v.erase(it);
		else
			++it;
	}
	print(v);
}

哪一个是删除偶数的代码?

我们来看一下运行结果分别是什么?

text2

text1

可以看出来text2是正确的代码,但是这两串代码到底有什么区别呢?

这时候我们就需要了解一下这个erase的底层了,erase删除成功后是会返回删除元素下一个的迭代器也就是说当我们删除成功后我们的erase会自动帮助我们实现了++;所以如果我们还要++就会跳过了元素导致了漏判

assign

有点像构造函数

下面我来演示一下这个assign的用法

cpp 复制代码
int main()
{
	///*vector<int> v;*/
	///*vector <int> v1(10, 9);*/ 
	///*set<string> s({ "apple","banana","pear","orange" });
	//vector <string> v2(s.begin(), s.end());*/
	///*vector<int> a({ 1,2,3,4,5,6,7 });
	//vector<int> v(a);*/
	///*vector<int> a({ 1,2,3,4,5,6,7 });
	//print(a);*/
	////vector<int>a(20, 0);
	//////a.reserve(5);
	//////a.reserve(21);
	////a.resize(10, 9);
	////print(a);
	////a.resize(30, 9);
	////print(a);
	///*vector<int>a(10, 8);
	//auto it = a.begin();
	//a.reserve(20);
	//cout << *it;*/
	///*TestVectorExpand();*/
	//vector<int>a({ 1,2,3,4,5,6,7 });
	//auto it2 = a.end();
	//auto it=a.insert(a.begin()+2, 100);
	////cout << *it << endl;
	//*it2++;
	//eraseerror();
	//text1();
	//text2();
	vector<int>v({ 1,2,3,4,5,6 });
	vector<int>a;
	a.assign(v.begin() + 1, v.end());
	print(a);
	a.assign(10, 8);
	print(a);
	return 0;
}

那么这个会造成扩容的操作就会有迭代器失效的风险

cpp 复制代码
vector<int>v({ 1,2,3,4,5,6 });
vector<int>a;
a.assign(v.begin() + 1, v.end());
print(a);
auto it = a.begin() + 1;
a.assign(10, 8);
print(a);
*it++;

以上就是常见的迭代器失效的场景,那么该怎么解决这个问题呢?

答案很简单啊就是避免使用扩容前的迭代器,及时更新迭代器就好了

vector的其他接口和string的接口都是类似的

二维数组

下面讲解一下这个

vector<vector<int>>;

cpp 复制代码
int main()
{
	vector<vector<int>>vv;
	vv.push_back({ 1,2,3 });
	vv.push_back({ 4,5,6 });
	cout << vv[1][1] << endl;
	/*cout << vv.size();*/
	for (int i = 0; i < vv.size(); i++)
	{
		for (int j = 0; j < vv[i].size(); j++)
		{
			cout << vv[i][j] << ' ';
		}
		cout << endl;
	}
	return 0;
}

这个就是二维数组的样子,vv[1]就是访问的是vv存储的第二个vector他的结构就像是一个个抽屉一样

使用标准库中vector构建动态二维数组时与上图实际是一致的

总结:

以上就是今天的所有的内容:

之后会模拟实现一下这个vector的常用的接口和函数

谢谢大家的观看!!!

相关推荐
暮雪倾风3 小时前
【JS-Node】node.js环境安装及使用
开发语言·javascript·node.js
Dxy123931021610 小时前
Python 使用正则表达式将多个空格替换为一个空格
开发语言·python·正则表达式
故事和你9111 小时前
洛谷-数据结构1-1-线性表1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
脱氧核糖核酸__11 小时前
LeetCode热题100——53.最大子数组和(题解+答案+要点)
数据结构·c++·算法·leetcode
脱氧核糖核酸__12 小时前
LeetCode 热题100——42.接雨水(题目+题解+答案)
数据结构·c++·算法·leetcode
techdashen12 小时前
Rust项目公开征测:Cargo 构建目录新布局方案
开发语言·后端·rust
星空椰12 小时前
JavaScript 进阶基础:函数、作用域与常用技巧总结
开发语言·前端·javascript
忒可君12 小时前
C# winform 自制分页功能
android·开发语言·c#
Rust研习社13 小时前
Rust 智能指针 Cell 与 RefCell 的内部可变性
开发语言·后端·rust