Vectorの前因后果

Vectorの前因后果🏆

Designed by:ArthurCui.

Time: **2025/4/7 ** 2:12:28.

Blog:Arthur__Cui.

1.背景

​ ⏳在1993年一个夜黑风高的晚上,C++祖师爷本贾尼(Bjarne Stroustrup)在处理当前手上项目的时候多次用到了C语言的静态数组,但是按照需求,他在使用的过程中被频繁需要变动数组大小的需求弄的不厌其烦。他意识到数组实时变动大小的实际应用场景很多很多,但是受限于C语言的玩法又没有一个好的解决办法。

​ 🤓👆干脆祖师爷一拍桌子,联合Boost库(STL库前身)的一众人,打算在自己刚设计的C++语言里面添加一种支持动态扩容的数组形式。但如果只有这一个亮点话那根本没有花费这么多人精力的必要,他还打算扩展一下整个数组用法的生态——无论是泛型支持还是算法协同亦或是新的访问遍历例如迭代器等等,祖师爷都打算重新玩个大的😎,重新颠覆一下老破小的旧章。

​ 就这样,集万千宠爱于一身的vector横空出世!在1994年vector被提交给C++标准委员会,1998年STL库被正式纳入C++98标准,vector成为C++标准库的重要组成部分!

在这里插入图片描述


2.配套接口

经过祖师爷之手,vector在他的带领下生态搭建的比较齐全,整个配套接口也有很多,用起来很舒服! 最常见的增删查改外,还有例如:

//----Constructor:构造初始化---------------
	vector<int> v1;
	vector<int> vec = { 1,2,3,4,5 };
	vector<int> v2(10, 6);
	vector<int> v3(v2.begin(), v2.end());
	string s1("hello kisskernel");
	vector<char> v4(s1.begin(), s1.end());
//----------------------------------------

size_type max_size() const; //返回最大值
bool empty() const;//判断是否为空
void swap (vector& x);//交换vector
void clear();//清空vector
//...

在这里插入图片描述

​ 😎其中有几个触及vector灵魂的接口,例如resize()重设size大小、reserve()预留空间(注意不是reverse)、capacity()容量大小等等我们后面会具体深入的讲到~


3.迭代器

标准STL库中有六大组件,分别是:

空间配置器(std::allocator…)

配接器

stackdeque (默认)后进先出(LIFO)
queuedeque (默认)先进先出(FIFO)
priority_queuevector (默认)优先级队列(堆)

容器(vector/list/set/map…)

● 算法(sort()、find()、count()…)

仿函数(std::sort(vec.begin(), vec.end(), std::greater<int>()); )

迭代器(提供一种通用方法访问遍历所有容器)

在STL所有的组件中,我认为设计的最好的一定是迭代器

🤔什么是迭代器??

迭代器就是指针!,只是被STL封装成一种可以统一访问迭代遍历的样子罢了…(QS:指针…哦不…迭代器已经变成你想要的形状了~🥵)


😊✌️它是这样…

begin()
end()
rbegin()
rend()

我们都可以把上面常用的函数叫成迭代器


🤓✌️也可以长这样…

	vector<int> v{ 1,2,3,4 };
	auto it = v.begin();   

这里it的类型就是:std::vector::iterator,只是被auto自动识别了


🤔✌️或者熟悉用法之后迭代遍历长这样…

vector<int> nums = {1,2,3,4,5};
	vector<int>::iterator it = nums.begin(); //迭代器
	for (it = nums.begin(); it != nums.end(); ++it) {
		std::cout << *it << " ";  // 解引用迭代器获取元素值
	}

最简单普遍的使用迭代器访问的方法,不过毕竟是指针,get到元素还是要解引用使用*号


🤣✌️而统一访问的样子最终是这样(核心优势)…

// 遍历vector(动态数组)
std::vector<int> vec = {1, 2, 3};
for (auto it = vec.begin(); it != vec.end(); ++it) { /* ... */ }

// 遍历list(双向链表)
std::list<int> lst = {1, 2, 3};
for (auto it = lst.begin(); it != lst.end(); ++it) { /* ... */ }

可以说,只要涉及到访问遍历容器,迭代器绝对有着一席之地~

它不仅仅能够支持泛型编程,而且很重要的一点是有越界以及类型安全检查,这一点往往普通方括号访问[] 或者指针访问pointer*会产生越界发生未定义の行为!


3.1迭代器失效

​ 那接下来我们来看一段代码

	vector<int> v{ 1,2,3,4 };
	auto it = v.begin();

	v.push_back(5);
	while (it != v.end()) {
		cout << *it;
		it++;
	}

请问这段简单的代码的运行结果?(VS演示)

在这里插入图片描述

我的编译器运行之后直接就报错了,报错原因是vector的迭代器不兼容。这又唱的是哪一出呢?这就是一个典型的野指针问题了。

其实这里就是我们vector的扩容机制在发力了

这里错误的原因是:我们push_back会有扩容机制的检查,如果发现数组大小不够用了vector就会扩容,而扩容的机制是深拷贝,而不是浅拷贝。

简单来说,就是重新开个新的更大的数组,然后把原来的旧元素都移到新的数组来。(😎QS:说人话就是人还是原来的那些人,不过搬到更大的房子里去了)

用图来表示就是这样:

在这里插入图片描述

it指向的位置已经被删除了,所以变成野指针的问题了,在这里俗称迭代器失效


3.2 解决方案

可是大家在现实使用中发现,现实情况中我们几乎很少遇到这种情况是为什么呢?

原因是因为咱们大佬帮咱们把对策都想好了。😎👆

原来刚学的时候时候我一直很疑惑一个问题:

为什么vector的erase()删除函数的返回值是iterator迭代器类型?

在这里插入图片描述


按照常理来说一个删除函数的返回值不一般是void或者是bool来判断删除是否成功就行了吗?

现在我们知道了这是返回了一个新的迭代器位置,就是大佬们防止这种迭代器失效的情况出现的。

总之,我们只要养好平常的一些好习惯,例如:提前开好空间、正确使用函数等等,基本不会遇到这种迭代器失效的情况~

(🤔QS:为啥不直接使用[ ]访问就好了嘛哈哈哈,可能正式情况都会有安全上面的考虑吧…)

4.扩容机制(重点)

🤔vector既然能够动态调整数组大小,就必然涉及到扩容的问题。

而我们之前提到的resize()、reserve()、capacity()就是用来界定这个的。

如图:

在这里插入图片描述

reserve()函数是预留空间大小(空间不一定能访问)–对标容量函数capacity()

resize()函数是调整空间大小(空间可以访问)–对标数组大小函数size()

我们来看看STL3.0源码中的表现:

在这里插入图片描述

😊在这个函数中可以很明显的看到我们在通过指针界定新的下标位置,很明显就是一个深拷贝的动作


那接下来我们结合一段很简单的代码,通过逐步调试以及监控查看内存我们来看看扩容具体是怎么一个扩法!!(QS:我看看怎么个事儿~😏)

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> vec; 
	vec.push_back(111); //6F
	vec.push_back(222); //DE
	vec.push_back(333); //14D	
	vec.push_back(444);
	vec.push_back(555);
	vec.push_back(666);//不扩容
	vec.push_back(777);
	vec.push_back(888);       
	vec.push_back(999);

	return 0;
}

按照扩容的深拷贝机制,只要容量大小不够用了,我们就会搬到新的数组里面去,也就是说**&vec[0]应该会变,但是&vec不会变**,因为移动的只是元素,而vec作为栈上面的一个对象自然地址不会改变。


首先插入第一个111:

在这里插入图片描述

再插222:

在这里插入图片描述

发现B4B8找不到vec[0]的地址了,那vec[0]跑哪去了? 我们再来取地址看看

这个时候发现跑到新的内存地址 06A8来了,说明深拷贝了,与此同时我们不仅能看到十六进制的111,还能看到222!

在这里插入图片描述

再插333:

在这里插入图片描述

发现06A8又找不到vec[0]了,那我们再取地址看看!

这个时候发现又到新的内存地址上0910去了,于此同时111、222、333都能看到了!

在这里插入图片描述

再插444:

这个时候又深拷贝了,情况与前面同理

在这里插入图片描述

​ 再找&vec[0]

在这里插入图片描述

情况与上面一致…

这个时候问题就来了,为啥我插一个它就扩容一次,是不是哪里有问题??🤔

答案是:不同的编译器会扩容不同的倍数,例如:

g++扩容2倍

MSVC扩容1.5倍 (我用的VS的编译器)

也就是说VS扩容机制1.5x:是这样事儿的

1-2-3-4-6-9…

只有到四个容量的时候下一个就不会扩容了,按照代码,我们只用插入到666的话,当时vec[0]的地址就不会变!我们来试试看看是不是这样~

(跳过插入555,情况与上述一致)

插入666之前我们的内存是这样的,那按照前面1.5倍扩容的说法,这一次应该就不会扩容,地址也就不会像前面一样变化,我们来看是不是这样

插入666之前(记住这个DD58):

在这里插入图片描述

插入666:

在这里插入图片描述

插入完之后我们惊奇的发现这次不需要扩容的话,地址就没有发生变化,而且我们666的十六进制数也成功进入了。

这个演示完说明了两件事:

1.vector的扩容不是原地扩容,而是异地扩容(深拷贝)

2.不同的编译器会有不同的扩容倍数。

好,那至此为止,咱们就从头到尾的解释清楚了vector的来龙去脉以及一些底层的机制,不过这只是冰山一角,具体的源码比这还要复杂得多!

那今天就分享到这里!(QS:哎呀妈呀周日肝到凌晨两点多越写越起劲也是没谁了…✌️)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Arthur___Cui

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

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

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

打赏作者

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

抵扣说明:

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

余额充值