【C++】vector和list的迭代器

目录

前言

一.迭代器的使用

1.vector迭代器

2.list迭代器的使用

二.迭代器失效问题

1.vector迭代器失效问题

2.list迭代器失效问题

三.vector和list的对比


前言

我们在学习C++STL部分的时候,在vector和list的部分初步认识了迭代器,以及在初学阶段,会觉得迭代器失效是一个很头痛的问题...

所以接下来我们就先从迭代器的使用开始,对迭代器进行一个浅浅的梳理,希望对大家有所帮助~

好的,废话不多说,直接开始今天的内容

一.迭代器的使用

1.vector迭代器

首先,我们需要先对其 建立一个整体的印象。在cpluslpus这个网站,我们可以先看一下它对vector容器的归纳:

因为今天的主题是迭代器,所以我们着重对上面框起来的部分进行讲解,剩下的模块大家有兴趣下来可以自行去cplusplus该网站去探索~

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//遍历
	auto it = v1.begin();
	while (it != v1.end())
	{
		cout << *it << " ";
        ++it;
        
	}
	cout << endl;

	return 0;
}

上面我们创建了一个int类型的容器,用它来完成了一个遍历的操作。使用vector其实使用的就是一段连续的数组空间

我们暂且可以将迭代器理解成一个指针

v1.begin()指向的就是第一个元素的位置,end()就是最后一个元素的下一个位置

这么看的话我们是否觉得使用迭代器还不如直接使用指针呢?那么接下来我们来看下面这段代码:

int main()
{
	vector<string> s1;
	s1.push_back("hello");
	s1.push_back("world");
	s1.push_back("hello");
	s1.push_back("iterator");

    //遍历 
	auto it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
        ++it;
	}
	cout << endl;

	return 0;
}

这次我们vector用的是string自定义类型,而不是内置类型int,这在使用以前的数组是无法办到的,这就体现了迭代器的价值,将其封装后可以使得底层可能截然不同的结构在使用上保持形式的统一~

这一点在之后的list中大家会有更深的体会,我们先继续

其实说了正向迭代器begin() 和end()之后,反向迭代器的使用rbegin()和 rend()就非常简单了,因为它们完全是一样的

请看下面代码:

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//遍历
	auto rit = v1.rbegin();
	while (rit != v1.rend())
	{
		cout << *rit << " ";
		++rit;

	}
	cout << endl;

	return 0;
}

反向迭代器实现的效果只是将本来的内容反过来遍历了,使用起来也是一样的,是不是?

2.list迭代器的使用

对于list迭代器,同样的,我们需要先对其 建立一个整体的印象。在cpluslpus这个网站,我们可以先看一下它对list容器的归纳:

接下来是使用,代码如下:

int main()
{
    list<int> ls;
	ls.push_back(1);
	ls.push_back(2);
	ls.push_back(3);
	ls.push_back(4);
	ls.push_back(5);

	auto LT = ls.begin();
	while (LT != ls.end())
	{
		cout << *LT << " ";
        LT++;
	}
	cout << endl;

}

这里我们在使用上发现玩的跟上面的vector是一样的,这也就体现了迭代器的一个巨大的好处

因为其实在底层上,两者是完全不同的

我们知道,list是一种带头双向循环的链表,那么每个结点就有三个区域:

数据域data
指向下一个结点的next指针
指向前一个结点的prev指针

这时如果我们还将迭代器简单的理解为指针的话,那么不妨想一下,如果我们使用++运算符的话,还能找到下一个结点的位置吗?

答案是不能的,因为list不像vector一样是一段连续的内存空间,++以后,可能对应是下一个结点的位置,也可能不是,我们就没法确定了

那又为什么我们却能像vector一样的去使用list的迭代器呢?
 

这都归功于类的封装,在对迭代器封装的时候,我们重载了这些操作符的意义(例如:++,  * 等)

这才使得我们能就像使用指针一样去使用迭代器

至于迭代器的封装具体是怎么做到这样的封装,这里博主自己实现了一个list的迭代器,大家可以看一下思路,相当于是STL关于 list迭代器部分的一个简化版~:

//list的模拟实现分成3个类,这里就不细说了下面给出了大致的框架,并非完整的代码哦~

//节点的结构体
template<class T>
struct ListNode
{
	T _data;
	ListNode<T>* _next;
	ListNode<T>* _prev;

    //...
};



//list迭代器的模拟实现
template<class T, class Ref, class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;

	Node* _node;//变量

	ListIterator(Node* node)//构造
		:_node(node)
	{}


	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}


	Ref operator*()//解引用
	{
		return _node->_data;
	}

	Ptr operator->()//指针
	{
		return &_node->_data;
	}

	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

};

//list--->链表
template<class T>
class list
{
	typedef ListNode<T> Node;
public:
	typedef ListIterator<T, T&, T*>  iterator;
	typedef ListIterator<T, const T&, const T*>  const_iterator;


	iterator begin()
	{
		return iterator(_head->_next);
	}

	iterator end()
	{
		return iterator(_head);
	}
	 
	const_iterator begin() const
	{
		return const_iterator(_head->_next);
	}

	const_iterator end() const
	{
		return const_iterator(_head);
	}
        
     //...
     //...
	}

private:
	Node* _head;//头节点
};

为了更能体现出迭代器并非简单的指针,所以就用list的迭代器作为实例啦(上面的vector的迭代器我们自己实现的话其实跟指针差不多,不具备代表性,所以就没有提啦)

好,接下来我们来看一下关于迭代器失效的问题,这也是一个重点问题

二.迭代器失效问题

1.vector迭代器失效问题

vector的迭代器失效,我们是在实现vector的insert和erase这两个接口时发现的

请看下面的代码:

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);


     //遍历
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = find(v1.begin(), v1.end(), 3);//查找3的位置
	v1.erase(it);//删除3
	v1.insert(it, 0);//插入0

    //遍历
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;


    return 0;
}

上面代码的意思就是找到3并删除掉,然后再在该位置插入0
按照理解这段代码应该是没有问题的,就是很简单的一段删除和插入嘛

然后,我们运行一下代码:
 

???代码崩溃了

其实这就是因为erase函数使得迭代器失效导致的

迭代器it原本指向的是3的位置,之后我们将他删除后,it指向的数据是4,不再是原数据了

而VS在实现函数时是assert处理的,所以才导致的上面的结果

解决办法就是it接收一下erase的返回值,erase的返回值是该位置的下一个位置的迭代器。即对it进行修正,这样就能有效的避免上面的问题

请看修正后的代码:


int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//遍历
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = find(v1.begin(), v1.end(), 3);//查找3的位置
	it = v1.erase(it);//删除3
	v1.insert(it, 0);//插入0

	//遍历
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;


	return 0;
}

还有一种迭代器失效是在insert时导致的

假如我们一开始的容器大小是5个,且这个容器已经满了,此时我再进行insert的时候,就会发生扩容,而C++的扩容是异地扩容的

异地扩容就必定会导致一个问题,我们原来it指向的地址发生了改变,此时如果不做处理的话,it指向的就是一块未知的空间,it就变成了一个野指针,这也是一种迭代器的失效

2.list迭代器失效问题

list的迭代器在删除时也会导致和上面的vector一样的问题,但与vector不同的是,vector在erase时,会导致当前迭代器失效,而list只会导致当前迭代器失效,其他迭代器不受影响

解决办法也是接收一下erase的返回值,同样list的erase的返回值也是该位置的下一个结点位置

但是list在insert插入的时候并不会导致迭代器失效,因为list不存在扩容的概念

三.vector和list的对比

最后我们将vector和list这2个容器进行一个对比:

在底层结构上:

vector是动态顺序表,开辟的是一段连续空间

list     是带头双向循环链表

在随机访问上:

vector支持随机访问,访问某个元素的效率为O(1)  ([ ]运算符访问)

list不支持随机访问,访问某个元素的效率为O(N) 

在插入和删除上:

vector在任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要扩容,开辟新空间,拷贝元素,释放旧空间,导致效率更低

list在任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)

迭代器上:

vector的迭代器是原生态指针,在插入和删除元素均会导致迭代器失效

list的迭代器是在原生态指针上进行了封装,插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响

在使用场景上:

vector在需要高效存储,支持随机访问,不关心插入和删除效率的场景下更适合

list在需要大量插入和删除操作,不关心访问的场景下更适合

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在这段代码中,我们使用了vector容器,并且通过push_back函数将元素1、2、3、4依次添加到容器中。接下来,我们声明了一个迭代器it,并将其初始化为容器的起始位置。然后,在while循环中,我们遍历容器中的元素,并输出每个元素的值。如果当前元素的值等于3,我们使用insert函数在当前位置之前插入一个值为10的元素。最后,我们输出换行符,并结束程序。 关于vector迭代器,它是用来访问和操作vector容器中元素的一种方式。迭代器可以指向容器中的任意一个元素,并可以通过解引用操作符(*)来获取元素的值。在这段代码中,迭代器it用于遍历vector容器中的元素,并输出它们的值。具体来说,通过cout << *it << " ";这行代码,我们输出了当前迭代器指向的元素值。 引用提供了vector的构造函数和赋值运算符的重载函数的介绍。其中,构造函数用于创建一个新的vector对象,并可以指定初始大小和初始值。赋值运算符的重载函数用于将一个vector对象赋值给另一个vector对象。这些函数可以用来初始化和操作vector容器。 引用介绍了assign函数,它可以用来替代vector容器中指定范围的元素。具体来说,assign函数的第一个参数是希望替代的元素个数,第二个参数是用于替代的值。在这个例子中,我们使用assign函数将容器v中的数据全部赋给了容器v1,并输出了v1中的元素。接着,我们又使用assign函数将v1中的前1个元素替代成了字符串"c",并输出了v1中的元素。这些操作展示了assign函数的用法和效果。 综上所述,c是一个vector迭代器,用于遍历vector容器中的元素,并输出其值。vector迭代器可以通过解引用操作符(*)来获取元素的值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C++中的vector结构解读、迭代器失效问题以及模拟实现](https://blog.csdn.net/weixin_59093392/article/details/131864618)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [C++之动态数组(Vector)中运用各类迭代器](https://blog.csdn.net/hold_the_key/article/details/126799998)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不吃肉的Humble

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

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

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

打赏作者

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

抵扣说明:

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

余额充值