【C++】探讨迭代器的秘密 和 迭代器失效问题(学精、学好必知)

🏠大家好,我是 兔7 ,一位努力学习C++的博主~💬

🍑如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

🚀如有不懂,可以随时向我提问,我会全力讲解~

🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

🚀本文章CSDN首发

目录

0.前言

1. 迭代器是什么

2. 迭代器的用法

3. 迭代器那层迷雾

3.1 while (it != v.end()) 问题

3.2 范围for 问题

3.3 迭代器失效 问题(重点)

3.3.1 迭代器失效的概念:

问题一:意义变了

问题二:变成野指针了

3.3.2 小试牛刀进行检测:

3.3.3 如何解决迭代器失效:

4. 实现


0.前言

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

        感谢大家的喜欢,感谢大家的支持~!

1. 迭代器是什么

        迭代器(iterators)是一个超级接口! 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。

        迭代器是类似指针一样的东西,因为它的用法就是模拟指针,但是它不是指针。

        迭代器是行为型 设计模式 ,提供了一种方法来遍历一个聚合的容器(集合)中的各个元素,而不用暴露其内部的表示。

        对于容器的访问而不需要关注容器内部的实现细节,可以使用迭代器,

        也就是说,不管你是什么容器,都可以用迭代器进行遍历操作和底层实现。

2. 迭代器的用法

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	return 0;
}

        这就是迭代器的使用方法,有的小伙伴直接将这段代码当作迭代器的公式进行记忆,这样其实是错误的!屏幕前的小伙伴如果也是将这段代码当作迭代器的公式进行记忆的话,可能会反驳我:你也这么写,我也这么写,而且都是对的,你凭什么说我这么记下来是错的?

        那我问问大家,迭代器的循环条件可以写成 while (it < v.end()) 么?可以的话,为什么可以?不可以的话,又是为什么呢?

        如果不是很清楚,那么继续向下看~

3. 迭代器那层迷雾

3.1 while (it != v.end()) 问题

        其实迭代器的循环条件有时可以写成while (it < v.end()),有时就不行。我说这个答案,大家可能会有点迷惑,为什么有时就可以有时就不可以呢,要明白这个问题,就先要清楚迭代器。

         前面说过迭代器是类似指针一样的东西,因为它的用法就是模拟指针,但是它不是指针。那么大家先来想想,指针可以比较大小么?答案是肯定的,当然可以比较大小啊,但是有一个限定条件就是用指针比较大小的时候,这两个指针在物理空间上是要连续的,也就是像数组那样比较指针的大小。

        当然,这里的再前提条件就是比较大小要有意义。如果没有意义,为了比较大小而比较大小,那就随意了。所以是在有意义的前提下的。

        现在我先来公布上面问题的答案:

        在vector这个容器是可以的,可以的原因是:vector的底层实现就是用指针来实现它的迭代器,而且它的物理空间也是在空间上连续的,也就是下图所示:

        在string这个容器当然也是可以的,因为string在底层的实现也是用的类似这种方式(string的实现和讲解我已经写过一篇博客了,很清楚,大家可以去学习一下)

        【C++】string类(学习并实现)(学习复习兼顾)_兔7的博客-CSDN博客

        大家可能会疑惑,既然string和vector可以,那你说说哪个不可以啊。

        这两种容器之所以可以是因为它们是连续的内存进行对数据存储的,但是list、map等链式结构就不可以。因为链式结构也就是用链表进行关联各个数据的

         所以我之所以说不对,是因为你可能只知道怎么写,但不知道自己为什么这么写,也就是说,如果问你可不可以写成 while (it < v.end()),你可能因为不清楚而答错。

3.2 范围for 问题

        大家可能认为范围for是很高级的一个形式,其实如果了解它就知道,它不过如此,范围for其实是根据其他简洁的语言进行引入的。

        C++在底层实现范围for其实就是用迭代器进行实现的(这里其实就是傻瓜似的替换,将范围for换成迭代器,我们可以试试将我们实现的begin()或者end()中的任意一个的名字改一下,比方说改begin() -> Begin(),这里就调不动了),所以这里只不过看起来很高级而已,所以只要可以用迭代器遍历,就一定可以用范围for遍历,范围for的实现是依赖于迭代器的

3.3 迭代器失效 问题(重点)

3.3.1 迭代器失效的概念:

        迭代器失效有两种情况:一是pos的意义变了(指向的位置不是想要指向位置),二是pos变成了野指针。

        大家看了概念可能还不是很理解,不用担心,大家继续向下走。

        如果向在一个位置插入数据,我们要做的就是先找到要插入的那个位置,然后再插入,也就是下图所示:

        那么如果我想要将pos位置 ' 3 ' 删掉,我们应该像下图所示写:

问题一:意义变了

         但是这样就会有问题,我给大家画图来展示为什么会有问题:

        pos失效的原因:意义变了。

        其实这里虽然pos的意义变了,但是运行起来还是会出错:

         我在别人博客中看见过有人说这里出错的原因是:pos这个迭代器在遍历一遍后就销毁了,所以会出错。

        这个说法是错误的!大错特错!!!

        没有被销毁,只是 pos 的意义变了,其实这里出错的原因是VS编译器对它进行了检查,因为pos已经失效了,所以编译器检查出来了,所以才报错的,并不是销毁了!!!

问题二:变成野指针了

         当我们添加数据,比如说如果到8个数据数组就满了,再增加就要增容了。这时也会有问题,我还是用画图的方式给大家展示:

        pos失效的原因:pos变成了野指针。

        这时肯定有小伙伴会感到奇怪,为什么pos在旧地址处, ' 30 ' 还是可以加上去,其实这里就要懂底层的实现就可以理解,我先给大家一段我写的对 insert 的实现吧,最后我会把所有的实现给大家发出来~

void insert(iterator pos, const T& x)
		{
			assert(pos < capacity());
			// 这里要注意的是,增容后空间改变,pos会失效。
			if(_finish == _endofstorage)
			{
				size_t len = pos - _start; // 记录下位置,要不就找不到pos位置了

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				// 更新pos,解决增容后pos失效的问题
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}

         所以我这么讲也就解决了大家的问题了吧~大家如果想更清楚的了解,可以去像我一样去实现一下各个容器(最后会给大家)。

        前面的两个问题最后都会出现下面的报错:

        所以大家在使用的时候一定要注意避免此事情的发生,接下来我会给大家说解决这个问题的方法。

3.3.2 小试牛刀进行检测:

        问题:找出偶数,并对其删除,输出奇数。

        大家看看我下面写的代码对不对:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	v.push_back(7);
	vector<int>::iterator it = v.begin();
	while(it != v.end())
	{
		if(*it % 2 == 0)
			v.erase(it);
		it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	return 0;
}

         但是还是那个问题,我继续用图来表示:

        这里可能看不出问题,那么如果都是偶数呢?

         发现问题了吧,如果是偶数的话,删掉再++直接跳过去了,这里就是pos失效的结果,所以大家在写的时候一定要注意这个问题,当然也有对应的解决方案,大家继续向下看。

3.3.3 如何解决迭代器失效:

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	v.push_back(7);
	vector<int>::iterator it = v.begin();
	while(it != v.end())
	{
		if (*it % 2 == 0)
			it = v.erase(it);
		else
			it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	return 0;
}

        所以我们只要在里面进行对 it 的位置进行控制就可以了,这样就解决的 pos 失效的问题,当然其他的情况也有可能失效,按照这个方法对你面对的问题进行控制即可,都是换汤不换药的方式~!

补充:

        这是后来补充来的(是对vector的实现的有问题),开始实现的还是有点问题的,这里还要说的是在insert那里也需要增加个返回值,增加返回值的原因是为了修正find()返回的那个迭代器。

        第一个原因是因为编译器会认为只要进行了依次insert或者erase后,这个迭代器就失效了,所以有可能编译器就会强行检测这里,如果没修正,就直接报错。

        第二个原因是重要的,是因为如果在我们insert的时候,有一种可能是我们在insert的时候,vector的end_of_storage 和 finish相同,也就是满了,然后insert时刚好进行扩容,在insert的实现里面我们是对pos进行了修正,我上面也写了,但是对于外面的pos没有进行修正,因为我们传进去的是pos的值,也就是进行了拷贝传参,所以外卖呢的pos不会被影响,这时也就体现了我们insert的返回值了,也就是在这时我们进行对pos的修正。

        有的人可能会有疑问,我们在用insert的时候,还要手动进行修正pos,那多麻烦,我们可不可以在函数接收的时候,就接收pos 的引用呢?也就是pos的别名,虽然这个方法听起来还是很靠谱的,但是其实还是有问题!

        因为pos是一个迭代器,然后调用insert的时候是 -> insert(对象.begin(), n) ,然后begin()的返回值是一个拷贝,具有常性(但是不是返回const)。

         但是因为pos这里是引用,无法接收具有常量的值(因为指针和引用会考虑到权限扩大和缩小的问题,这里将const的值给了一个non-const,也就是权限的放大,所以是不可以的!!!当然也有可能有时也可能是在隐式类型转换的时候需要创建临时变量,而临时变量具有常性,所以需要用const & 这种方式进行接收),所以这时pos就又要加上const,但是我们如果讲pos加上const,又对这个函数实现有影响(下面的pos不能更改了)

        所以这样的话就会引起一系列的问题,所以我们不能将pos改成引用传参。

        然后当时我有这样的问题:既然函数的返回值是临时拷贝(在这里说的是不引用返回)具有常性,那么和返回const类型有什么区别?

        后来我才反应过来,这里的问题是引用和指针会涉及到权限放大和缩小的问题,并不单单只是返回值是不是const的问题(当然肯定有关系,但这里不是重点,至少我是这样认为的)。

        接下来我给大家两个接口函数进行正确使用insert 和 erase,大家使用的时候要注意要这样使用哦~否则就会出现迭代器失效的问题。

void test_vector1()
{
	兔7::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(4);
	v.push_back(3);
	v.push_back(4);
	//v.push_back(5);

	// 要求删除所有的偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		else
		{
			++it;

		}
	}

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_vector2()
{
	//std::vector<int> v;
	兔7::vector<int> v;
	//v.reserve(10);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	// 在所有的偶数前面插入一个偶数2倍
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.insert(it, *it * 2);
			//it = v.insert(it, *it * 2);
			++it;
			++it;
		}
		else
		{
			++it;
		}
	}

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

4. 实现

#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;


namespace 兔7
{
	template <class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{}

		 //类模板的成员函数,还可以再是函数模板
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		/*vector(T* first, T* last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}*/


		~vector()
		{
			if (_start)
			{
				delete[] _start;
			}

			_start = _finish = _endofstorage = nullptr;
		}

		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const iterator begin() const
		{
			return _start;
		}
		const iterator end() const
		{
			return _finish;
		}


		size_t capacity() const
		{
			return _endofstorage - _start;
		}
		size_t size() const
		{
			return _finish - _start;
		}
		bool empty() const
		{
			return _start == _finish;
		}



		T& operator[](size_t i) // 可读可写
		{
			assert(i < size());
			return _start[i];
		}


		const T& operator[](size_t i) const // 只读
		{
			assert(i < size());
			return _start[i];
		}


		// v2(v1)
		//vector(const vector& v)  // 也可以,但是不推荐
		vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			_start = new T[v.capacity()];
			// memcpy(_start, v._start, sizeof(T)*v.size()); // vector<string>不可以,对string相当于浅拷贝了
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];  // 然而这里是用的赋值拷贝,是深拷贝
			}
			_finish = _start + v.size();
			_endofstorage = _start + v.capacity();
		}


		// v1 = v3

		//vector<T>& operator=(const vector<T>& v)
		//{
		//	if (this != &v)
		//	{
		//		delete[] _start;
		//		_start = new T[v.capacity()];
		//		memcpy(_start, v._start, sizeof(T) * v.size()); // 不可以使用memcpy
		//		_finish = _start + v.size();
		//		_endofstorage = _start + v.capacity();
		//	}
		//	return *this;
		//}

		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}


		void swap(vector<T>& v)
		{
			::swap(_start, v._start);
			::swap(_finish, v._finish);
			::swap(_endofstorage, v._endofstorage);
		}


		void resize(size_t n, T val = T()) // C++支持 int() \ char()
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}


		void reserve(size_t n)
		{

			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start) // 如果是nullptr就会崩
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}


		void push_back(const T& x)
		{
			if (_finish == _endofstorage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = x;
			_finish++;
		}

		void pop_back()
		{
			assert(!empty());
			_finish--;
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			// 这里要注意的是,增容后空间改变,pos会失效。
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start; // 记录下位置,要不就找不到pos位置了

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				// 更新pos,解决增容后pos失效的问题
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;

			return pos;
		}


		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}


		void PrintVector() const
		{
			const_iterator it = begin();
			while (it != end())
			{
				// *it += 1;  只能读,不能写
				cout << *it << " ";
				++it;
			}
			cout << endl;
		}

	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

        如上就是 迭代器 的所有知识,如果大家喜欢看此文章并且有收获,可不可以给 兔7 三连关注~

        感谢大家观看,感谢大家支持!

  • 43
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 77
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NPC Online

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

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

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

打赏作者

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

抵扣说明:

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

余额充值