vector详解

在开始学习C++的STL之后,相信大家都学会通过查文档来了解一些库函数,今天我来给大家介绍vector,从基本的使用到vector背后的源码实现,迭代器等展开讲,仔细阅读,一定有所收获!

 

vector基本介绍

首先大家要知道,vector是一个序列式容器,什么是序列式容器

所谓序列式容器,就是其中的元素都有序,但是这里的有序不是通常意义上我们理解的有序,而是元素被存储的顺序和容器中存储的顺序一致。C++中的序列式容器有C++语言提供一个序列式容器array,同时STL还提供了 vector,list,deque,stack,queue等等序列式容器。

vector是一个什么样的容器?

vector这个容器理解起来就像一个大小可变的数组,与数组不同的地方就是数组是一块静态的空间,而vector是一块动态的空间,随着元素的假如,vector的内部机制会自行扩充空间大小以收纳更多的元素。

vector的使用

通过查阅文档,我们来了解一下vector的使用:

构造函数

首先来介绍一下构造函数,第二个空间配置器我们这里先不讲,主要说一下基本的构造函数的使用。

最常见的第一个构造函数,你可以什么都不写,直接构造一个vector对象:

vector<int> v1;

 

默认构造的对象,size和capacity都为0。

还可以一次构造多个对象:

vector<int> v2(10);

 

 但是这些元素都会调用他们自己的构造函数,这里要明确一点,vector这个容器可是很强大的,里面不仅可以放内置类型的元素,还可以放自定义类型的元素。所以放元素的时候,就会涉及到调用元素的构造函数。在C语言中我们可能没见过下面这行代码:

int a = int();
cout << a;

最终打印的结果是0,也就是默认a被初始化为0,这样大家就很好理解为什么上面的监视窗口的元素都是0了。

拷贝构造也说的很简单,可以直接传被拷贝对象的引用,也可以传递两个迭代器分别指向要拷贝的begin和end。

其他接口

vector文档

 这里有两个值得一说的接口:resize和reserve

 

对比发现,reserve是对capacity进行调整,所以参数也之后一个n,n比capacity大就扩容,但是不回发生对空间的回收。

resize主要是对元素进行调整,如果n比size大, 发生扩容,并且这里要对元素进行初始化,调用对应的构造函数。

assign这个接口可以让之前vector容器中的内容完全被改变,改变的方式有两种:

用另一个vector对象的迭代器来标定区间

用n个值为val的对象进行填充 

	vector<int> v1;
	vector<int> v2(10);
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	//v2.assign(v1.begin(),v1.end());
	v2.assign(4, 5);
	for (auto E : v2)
	{
		cout << E;
	}

其他的类函数这里就不一一列举了。接下来聊一下vector的迭代器

vector的迭代器

大家在一开始学C++的时候可能都看过这样一段代码(如下图),这段代码看起来很厉害,我们之前C语言需要很多行代码才能实现的效果它很容易就实现了,其实很简单,就是使用了迭代器。迭代器是一个类,里面实现了很多的运算符重载。

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> ret;
	ret.push_back(1);
	ret.push_back(2);
	ret.push_back(3);
	ret.push_back(4);
	for (auto R : ret)
	{
		cout << R << " ";
	}
	return 0;
}

vector是一个连续的线性空间,就像数组一样,区别于数组之处在于vector是可以对数组的大小进行动态修改的。但是在访问和操作上也可以像数组一样提供一种指针的访问方式,那么这就是vector迭代器,但是迭代器千万不可以被理解为它就是指针,在之后的list的文章中我们还会再次提及。

vector迭代器可以实现的操作行为有:operator*,operator->,operator++,operator--,operator+,operator-,operator+=,operator-=,是的,这不就是指针都能实现的嘛。

vector是支持随机存取的。所以迭代器指向的每个位置的操作都是可以被执行的。

上面的范围for是怎么实现的呢?本质就是使用了迭代器对ret这个容器进行了遍历。总的来说,在vector中,由于vector的数据结构,vector迭代器甚至可以直接认为是指针。

int main()
{
	vector<int> ret;
	ret.push_back(1);
	ret.push_back(2);
	ret.push_back(3);
	ret.push_back(4);
	//通过迭代器找到值为2的元素并对其进行修改
	vector<int>::iterator it = ret.begin();
	while (it != ret.end())
	{
		if (*it == 2)
			*it = 10;
		++it;
	}
	for (auto R : ret)
	{
		cout << R << " ";
	}
	return 0;
}

大家应该都注意到了代码中的两个函数:begin()和end()

begin返回的是一个指向容器第一个元素的迭代器,而end返回的是一个指向最后一个元素的下一个位置的迭代器。所以begin与end维护的是一段前闭后开的区间,这一点要格外注意,之后的练题中很容易出现越界访问的问题。

而rbegin和rend则和begin与end相反,也就是rbegin和end一个意思,而rend和begin一个意思。

vector的数据结构

vector采用的数据结构非常简单,线性连续空间,以两个迭代器start和finish分别指向连续空间中已经被使用的范围的头尾,而迭代器end_of_storage指向整块连续的空间的尾端,所以模拟实现vector需要三个迭代器实现:

template<class T,class Alloc = alloc>
class vector
{
    //...
protected:
    iterator start;            //表示目前使用空间的头
    iterator finish;           //表示目前使用空间的尾
    iterator end_of_storage;   //表示目前可用空间的尾
//...
}

为了降低空间配置时的速度成本,vector实际配置的空间大小可能比客户端需求的更大,一旦容量等于大小,便是满载,下次再有新增元素,整个vector就要扩容。

画一张图方便大家理解:

扩容中的一些问题

上面提到了空间不够用的时候会出现扩容的现象,但是实际上扩容的过程中有很多我们要注意的事情。整个扩容的过程不像顺序表中的简单的扩容,直接使用realloc就扩容完成了,实际中会遇到一些问题我也会一一展开说,最后我们再一起看一下源码的一些实现方法。

迭代器失效问题

设想我们一直在一块空间中插入元素,很快finish所指向的位置就到了end_of_storage,空间需要扩容,那么在插入数据完成后,就不可以简单的将迭代器进行++的操作,这里画一张图大家理解一下:

 迭代器失效就是没有更新迭代器所指向的位置,导致出现了程序的报错。

#include <iostream>
#include <vector>

using namespace std;
int main()
{
	vector<int> V1;
	V1.push_back(1);
	V1.push_back(2);
	V1.push_back(3);
	V1.push_back(4);
	vector<int>::iterator it = V1.begin();
	while (it != V1.end())
	{
		if ((*it) % 2 == 0)
			V1.erase(it);
		else
			++it;
	}
	for (auto& num : V1)
	{
		cout << num;
	}
	return 0;
}

上面的代码会出现报错,当然这里举例的是一个erase的例子,insert的例子图片表示的很清楚了。上面的代码会不会报错大家猜一下。

答案是在VS平台下会报错,但是在g++下不会报错。这是为什么?

VS平台下的stl的实现是和g++下的实现是不一样的,stl会对插入和删除后的迭代器进行强制的检查,删除后迭代器指向的位置可能是非法的,所以VS下的stl的实现防止非法的访问直接进行了强制检查,而g++的erase则返回删除元素的下一个,并不会进行强制检查,这也就是为什么在g++下不会报错的原因。但本质上还是这就是一段错误的代码,erase本身是由返回值的,会返回删除后的迭代器,如果不接受而是自己进行迭代器的调整就会导致迭代器失效

所以当你写一个程序在有的环境下可以跑过有的环境下跑不过,先别怪编译器,一定是代码有问题。

扩容出现的浅拷贝问题

扩容的时候会出现数据拷贝的问题,当我们模拟实现的时候,如果就单纯的使用memcpy就会出现浅拷贝问题。

如果被拷贝的对象是自定义类型,如果单纯的拷贝自定义类型对象的地址,在拷贝结束后就会对之前的对象内容进行回收,那么再去使用已经被回收的对象就会报错。

 源码中的扩容

void push_back(const T& x)
{
	if (_fininsh != end_of_storage)
	{
		construct(finish, x);
		++finish;
	}
	else//没有备用空间
		insert_aux(end(), x);
}

template<class T,class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x)
{
	if (_finish != end_of_storage)//还有备用空间
	{
		//在备用空间起始处构造一个元素,并以vector最后一个元素值为初始值
		construct(finish, *(finish - 1));
		//调整水位
		++_finish;
		T x_copy = x;
		copy_backward(position, finish - 2, finish - 1);
		*position = x_copy;
	}
	else//已无备用空间
	{
		const size_type old_size = size();
		const size_type len = old_size != 0 ? 2 * old_size : 1;
		//如果原大小为0,那么就配置为大小为1
		//如果大小不为0,就配置大小为原大小的两倍

		iterator new_start = data_allocator::allocate(len);
		iterator new_finish = new_start;
		try
		{
			//将原来vector中的内容拷贝到新的vector中
			new_finish = unintialized_copy(start, position, new_start);
			//为新元素设定初始值x
			construct(new_finfish, x);
			//调整finish位置
			++new_finfish;
			new_finfish = uninitalized_copy(position, finish, new_finish);
		}
		catch(...)
		{
			destroy(new_start, new_finish);
			data_allocator::deallocate(new_start, len);
			throw;
		}
		//析构并释放原vector
		destroy(begin(), end());
		deallocate();
		//调整迭代器指向新的vector
		start = new_start;
		finish = new_finish;
		end_of_storage = new_start + len;
	}
}

源码其实没必要全部看懂,我们可以大概了解源码扩容的思路:

如果有空间就进行正常的插入操作,如果空间已满,就进行扩容,然后讲原空间内容复制到新的空间中,再讲新空间中的内容进行回收操作。因此,对vector中的任何操作,一旦引起空间重新配置,就会导致指向原先vector中的所有迭代器都会失效。上面我已经提到过了这里再次说明。

重要的东西基本上都提到了,如果有什么不足还希望大家能在评论区提出宝贵意见,其实在学习STL的过程中,我们发现迭代器在容器中起着很重要的作用,迭代器在容器的访问上提供了一致的接口,在之后的博客中大家可以慢慢体会到迭代器的重要作用。

今天的博客内容就到这里啦,谢谢大家支持!

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++中的vector是一个动态数组,可以在运行时动态地增加或减少其大小。它提供了许多方便的方法来操作数组,例如在末尾添加元素、在任意位置插入元素、删除元素等等。下面是一些常用的vector操作: 1. 创建vector对象:可以使用默认构造函数创建一个空的vector对象,也可以使用带有初始值的构造函数创建一个包含初始值的vector对象。 ```c++ // 创建一个空的vector对象 std::vector<int> v1; // 创建一个包含初始值的vector对象 std::vector<int> v2 = {1, 2, 3}; ``` 2. 访问vector元素:可以使用下标运算符[]或at()方法来访问vector中的元素。 ```c++ std::vector<int> v = {1, 2, 3}; // 使用下标运算符[]访问元素 int x = v[0]; // x = 1 // 使用at()方法访问元素 int y = v.at(1); // y = 2 ``` 3. 在末尾添加元素:可以使用push_back()方法在vector的末尾添加一个元素。 ```c++ std::vector<int> v = {1, 2, 3}; // 在末尾添加一个元素 v.push_back(4); // v = {1,2, 3, 4} ``` 4. 在任意位置插入元素:可以使用insert()方法在vector的任意位置插入一个元素。 ```c++ std::vector<int> v = {1, 2, 3}; // 在第二个位置插入一个元素 v.insert(v.begin() + 1, 4); // v = {1, 4, 2, 3} ``` 5. 删除元素:可以使用erase()方法删除vector中的一个或多个元素。 ```c++ std::vector<int> v = {1, 2, 3}; // 删除第二个元素 v.erase(v.begin() + 1); // v = {1, 3} // 删除第二个到第三个元素 v.erase(v.begin() + 1, v.begin() + 3); // v = {1} ``` 6. 获取vector大小:可以使用size()方法获取vector中元素的数量。 ```c++ std::vector<int> v = {1, 2, 3}; // 获取vector大小 int size = v.size(); // size = 3 ``` 7. 清空vector:可以使用clear()方法清空vector中的所有元素。 ```c++ std::vector<int> v = {1, 2, 3}; // 清空vector v.clear(); // v = {} ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Feng,

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

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

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

打赏作者

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

抵扣说明:

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

余额充值