C++ | STL 迭代器

目录

一.什么是迭代器

二.迭代器的划分

1.常量迭代器的简单使用

2.反转型迭代器的简单使用

三.迭代器的插入和修改

1.迭代器的修改操作

2.迭代器的插入操作

四.流式迭代器的类型

五.迭代器失效


一.什么是迭代器

迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器。但按严格的意义来讲,迭代器又不是我们所谓普通的指针,我们可以称之为广义指针,只不过我们可以对迭代器执行类似指针的操作。

我们知道在32位的操作系统中,指针的大小是4个字节,那么迭代器的大小又是多少呢?

#include<iostream>
#include<vector>
#include<list>
#include<deque>

int main()
{
	std::vector<int> vec(10,20);
	std::list<int> lst; (10, 20);
	std::deque<int> dqu(10,20);

	std::vector<int>::iterator it1 = vec.begin();
	std::list<int>::iterator it2 = lst.begin();
	std::deque<int>::iterator it3 = dqu.begin();

	std::cout << sizeof(it1) << std::endl;
	std::cout << sizeof(it2) << std::endl;
	std::cout << sizeof(it3) << std::endl;

        int* p = NULL;
	std::cout <<sizeof(p) << std::endl;


	return 0;
}

我们看到,在32位操作系统下,vector,list和deque容器中的迭代器的大小都是12个字节。 在64位操作系统下,vector,list和deque容器中的迭代器的大小都是24个字节。

二.迭代器的划分

在容器类中一般都会提供四种的迭代器

  1. iterator:普通迭代器。
  2. const_itrerator :常量迭代器,常量迭代器 迭代 常容器对象。
  3. reverse_iterator :反转型迭代器。
  4. const_reverse_iterator:常量反转型迭代器,迭代常量容器对象。

1.常量迭代器的简单使用

#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>

template<typename Iterator>
void Show1(Iterator first, Iterator last)
{
	for (first; first != last; first++)
	{
		std::cout << *first << " ";
	}
	std::cout << std::endl;
}

template<typename Container>//容器 Container需要一个容器
void Show2(const Container con)//con 常量容器对象 
{
	typename Container::const_iterator it = con.begin();//面向对象的指针
	while (it != con.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
}

int main()
{
	int arr[] = { 123, 2, 5, 564, 7, 3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);
	Show1(vec.begin(), vec.end());
	Show2(vec);

	return 0;
}

上述代码的功能是对一个容器的所有数据进行打印,Show1()函数的形参是一个迭代器区间,Show2()函数的形参是一个常量容器对象,而之所以将容器对象声明为常量容器对象,是为了避免在打印过程中修改容器中的数据,毕竟Show()函数的功能只是访问容器。

我们知道遍历一个容器是用迭代器进行的,在Show1()函数中我们已经给定了迭代器区间,而在Show2()函数中我们只给的了容器对象,所以我们还需要这个容器对象的迭代器,而这个容器对象又是一个常量容器对象,所以,为了避免在打印过程中通过迭代器来修改容器中的数据,我们要将迭代器也声明为常量迭代器,即Show()中的it。

2.反转型迭代器的简单使用

#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<list>

template<typename Container>
void ReverseShow1(Container con)
{
	typename Container::iterator rit = --con.end();
	while (rit != con.begin())
	{
		std::cout << *rit << " ";
		rit--;
	}
        std::cout << *(con.begin());
	std::cout << std::endl;
}

template<typename Container>
void ReverseShow2(const Container con)
{
	typename Container::const_reverse_iterator rit = con.rbegin();
	while (rit != con.rend())
	{
		std::cout << *rit << " ";
		rit++;
	}
	std::cout << std::endl;
}
int main()
{
	int arr[] = { 1, 3, 24, 3, 5, 46 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);
	ReverseShow1(vec);
        ReverseShow2(vec);
	return 0;
}

上述代码的功能是将一个容器中的数据倒着打印出来,我们看到在ReverseShow1函数中,我们定义了一个普通类型的迭代器rit,并让它指向容器的end()-1的位置,也就是最后一个有效元素的位置,接着每打印一个数据,就让rit向前迭代。但我们发现这样并不能将容器中的所有元素都打印出来,因为循环的条件是 rit != con.begin(),因此为了将容器中的第一个数据也打印出来,我们还需要额外的语句来实现,为了避免这一麻烦,我们又实现了ReverseShow2这个函数。

在ReverseShow2中我们定义了一个反转型迭代器rit,我们注意到一开始rit迭代的位置是con.rbegin(),而con.rbegin()代表的位置是con这个容器的最后一个有效元素的位置,而con.rend()的代表的位置是第一个有限元素的前面的位置,正好与con.begin()和con.end()代表的意思相反。

三.迭代器的插入和修改

1.迭代器的修改操作

#include<iostream>
#include<vector>
#include<algorithm>

template<typename Container>//需要一个容器类型
void Show(const Container con)
{
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
}

int main()
{
	int arr[] = { 13, 12, 43, 6, 45, 77 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);
	Show(vec);

	std::vector<int>::iterator fit = std::find(vec.begin(), vec.end(), 12);
	if (fit != vec.end())
	{
		*fit = 100;//插入  修改?
	}
	Show(vec);
        
        return 0;
}

std域中的find函数可以查看某一迭代器区间中的某个值是否存在,如果存在则将这个元素所在的位置返还给调用方,如果不存在则将这个容器的end()返回给调用方。

在上述代码中,我们在main函数中调用std域中的find函数,查找vec容器中是否存在值为12的元素,并将最终的结果返还给迭代器fit,我们只做vec容器中是存在值为12的元素的,因此此时fit接收到的值就是这个值为12的元素的位置,接下来 我们进行了    *fit =100; 的操作,就是将那个元素的值从12改为100。这里迭代器进行的就是修改操作。

2.迭代器的插入操作

 在简绍迭代器的插入操作前,我们先来看一下插入型迭代器。

插入迭代器的类型:

  1. back_insert_iterator     后插型迭代器
  2. front_insert_iterator     前插迭代器,vector容器没有前插型迭代器。
  3. insert_iterator              插入迭代器

插入型迭代器所需要的头文件 #include<iterator>。

#include<iostream>
#include<vector>
#include<algorithm>
#include<iterator>

template<typename Container>//需要一个容器类型
void Show(const Container con)
{
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
}

int main()
{
	int arr[] = { 13, 12, 43, 6, 45, 77 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);
	Show(vec);

	std::back_insert_iterator<std::vector<int>> bii(vec);//后插型迭代器
	*bii = 100;//也可写为 bii =100;
	Show(vec);

	std::insert_iterator<std::vector<int>> ii(vec, vec.begin() + 4);//插入型迭代器
	ii = 200;//插入 ,也可写为*ii = 200; 
	Show(vec);

	std::list<int> lst(arr, arr + len);
	Show(lst);

	std::front_insert_iterator<std::list<int>> fii(lst);//前插型迭代器
	fii = 300;//也可写为 *fii = 300;
	Show(lst);

	return 0;
}

上述代码包含了后插型迭代器,前插型迭代器以及插入型迭代器的使用方法。为了使大家对以上三种迭代器的工作原理了解的更加清楚,我们来着重讲解后插型迭代器,并且自己实现一个类似于库中的后插型迭代器。

template<typename Container>//后插   容器类型
class Back_Insert_Iterator
{
public:
	typedef Back_Insert_Iterator<Container> _Myt;//重命名

	typedef typename Container::value_type _Valty;//重命名

	Back_Insert_Iterator(Container& con)//形参把容器对象的地址传给pc,构造函数
	{
		pc = &con;
	}

	_Myt& operator*()//这个运算符重载函数没有任何意义
	{
		return *this;
	}

	_Myt& operator++()//这个运算符重载函数没有任何意义
	{
		return *this;
	}

	_Myt& operator++(int)//这个运算符重载函数没有任何意义
	{
		return *this;
	}

	_Myt& operator=(const _Valty& val)
	{
		pc->push_back(val);
		return *this;
	}
private:
	Container* pc;//容器类型的指针   指向一个容器对象
};

私有数据成员 pc 是一个容器类型的指针,其指向一个容器对象。我们在构造函数中对这个pc进行指向。期间我们还编写了多个运算符重载函数,这些运算符重载函数的返回值都是Back_Insert_Iterator<Container>即Back_Insert_Iterator这个类本身,为了编写时的方便,我们将Back_Insert_Iterator<Container>重命名为_Myt,而Container::value_type指的是当前迭代器迭代的容器中元素的类型,为了编写方便,我们将它重命名为_Valty。

我们发现赋值运算符重载的内部其实调动了容器内部尾插这个函数,例如前面代码中的

std::back_insert_iterator<std::vector<int>> bii(vec);//后插型迭代器
*bii = 100;

首先,调用构造函数让 bii中的pc指针指向vec,即

接着 *bii = 100;会调用运算符重载符函数,而运算符重载函数中会调用容器中的push_back()函数,进行尾部插入

其实这里的三个运算符重载函数没有任何意义,但是因为迭代器本身就是一个指针,而只要是指针就要可以进行解引用和++操作,所以我们将这些接口预留下来,哪怕它是一个无效的函数。

类似的,前插型迭代器其实是调用了容器中的push_front函数,插入型迭代器调用的是容器中的insert函数。

四.流式迭代器的类型

流式迭代器的分类

  1. ostream_iterator,输出流迭代器
  2. istream_iterator,输入流迭代器

在简绍这两个迭代器之前,我们先来看一个copy泛型算法。

template <typename InputIterator, typename OutputIterator >
OutputIterator Copy(InputIterator first1, InputIterator last,OutputIterator first2)
//first1 - last   放到  first2 插入型迭代器
{
	for (first1; first1 != last; first1++)
	{
		first2 = *first1;
	}
	return first2;
}

这个泛型算法的形参为两个输入型迭代器和一个输出型迭代器,返回值为一个输出型迭代器。

这里的两个输入型迭代器 first1 和 last 构成了元素区间,然后将 first1 ~ last 之间的元素全部放到输出型迭代器first2中,也就是说这里的first2其实是一个插入型迭代器,也就是将 first1 ~ last 之间的元素全部放到输出缓冲区当中,接着系统就会将输出缓冲区中的数据打印到屏幕上。

具体的使用方法如下:

#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<list>

template <typename InputIterator, typename OutputIterator >
OutputIterator Copy(InputIterator first1, InputIterator last,OutputIterator first2)
//first1 - last   放到  first2 插入型迭代器
{
	for (first1; first1 != last; first1++)
	{
		first2 = *first1;
	}
	return first2;
}

int main()
{
	int arr[] = { 1, 32, 4, 3, 45, 45 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);
	Copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "));
	return 0;
}

上述代码中,输入流迭代器first1绑定到了vec.begin()的位置,输入流迭代器last迭代器绑定到了vec.end()的位置。而这时由 first和last构成的区间就是vec容器中的所有数据,接着将输出流迭代器绑定到了输出流缓冲区上,这样Copy就会将vec中的所有数据进行打印了。

或者

#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<list>

template <typename InputIterator, typename OutputIterator >
OutputIterator Copy(InputIterator first1, InputIterator last,OutputIterator first2)
//first1 - last   放到  first2 插入型迭代器
{
	for (first1; first1 != last; first1++)
	{
		first2 = *first1;
	}
	return first2;
}

int main()
{
	std::vector<int> vec;
	Copy(std::istream_iterator<int>(std::cin),std::istream_iterator<int>(),std::back_insert_iterator<std::vector<int>>(vec));
	std::cout << "-------------------------" << std::endl;
	Copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "));

	return 0;
}

在上述代码中,我们第一次调用Copy时,将输入流迭代器first1与输入缓冲区绑定,现在first就代表输入缓存区的起始位置,接着将输入流迭代器last与输入缓冲区的异常绑定(这个异常是由系统给定的),现在last就代表输入缓冲区的末尾位置,最后将插入型迭代器first2与vec容器绑定,这样我们就可以向vec中插入元素了。我们第二次调用Copy就会将此时vec中的数据打印到屏幕上了。

此时,当我们输入a时,系统就会判断为异常,因为我们将输入流迭代器first1与输入缓冲区绑定时,规定了输入的数据为int类型,接着就会结束对输入缓冲区的输入了。

五.迭代器失效

#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<list>

template<typename Container>//需要一个容器类型
void Show(const Container con)
{
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
}
int main()
{
	int arr[] = { 1, 2, 1, 1, 1, 2, 1, 1 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);

	std::vector<int>::iterator it = vec.begin();
	while (it != vec.end())
	{
		if (*it == 1)
		{
		    vec.erase(it);//erase(it) 返回值 重新拿到有效位置
		}
                it++;
	}
	Show(vec);
	return 0;
}

我们看到,上述代码的功能是删除vec容器中值为1的元素,我们让迭代器it迭代到vec.begin()的位置,接着进入while循环,如果it所迭代位置的值为1,就将它删除,并且每进行一次就将it向后迭代一个位置。但是,这段代码的逻辑虽然没有错,但在实现上有很大的问题。最终执行的结果是系统崩溃。

上述代码之所以是错误的,是因为在执行上述语句时引起了迭代器失效。

一开始我们让it迭代到vec.begin()的位置,如下图

进入while循环以后,迭代器it会判断当前迭代位置的值是否为1,我们看到vec容器中第一个元素的值恰好为1,那么就要进行删除操作,而在vector容器中,没当删除当前位置的元素后,都要将后面所有的元素向前挪动,而所有容器内部提供的begin()和end()接口每次都是重新获取的,所以就变成了下图

我们看到在进行了一次删除操作后,vec容器中的所有元素都向前挪动了一个,而这时就造成了混乱。例如,假如现在有两个线程,分别是线程A和线程B,这两个线程中分别有两个迭代器迭代这个容器,如下图

假如此时线程A中的迭代器itA进行了删除操作,就会变成下图

我们发现,在线程A没有进行删除操作前,迭代器itB所迭代位置的值为2,但在线程A进行了删除操作后,迭代器itB所迭代位置的值就变为了1。这样就会引起很大问题,而为了避免这种情况,就引进了迭代器失效这一观念。

在vector容器中引起迭代器失效的情况

  1. insert,按位置插入会导致插入点到尾部的所有迭代器失效,具体原因我们上面的例子已经讲过了。
  2. erase,按位置删除会导致删除点到尾部的迭代器都失效。
  3. push_back,尾插会导致尾部迭代器失效,因为每次在尾部插入一个元素后,end()都会重新获取,从而导致原来指向end()位置的迭代器失效,因为此时end()已经向后移动了一格。
  4. pop_back,尾删会导致尾部迭代器失效。
  5. 当vector容器进行扩容操作会导致所有迭代器都失效,因为此时的地址已经发生变化。

在list容器中引起迭代器失效的情况

  1. erase,按位置删除会导致指向被删除元素的那个迭代器失效,其他迭代器不受影响(list目前只发现这一种迭代器失效的情况)。

 在deque容器中引起迭代器失效的情况

  1. push_front和push_back,即头插和尾插会导致插入点的迭代器失效。
  2. pop_front和pop_back,即头删和尾删会导致删除点的迭代器失效。
  3. 在除头部个尾部以外的任意位置进行insert或erase会导致所有迭代器失效。

那么为了解决以上问题,使得能够顺利的将vec中值为1的元素全部删除,我们可以将以上代码改写成下面的代码

#include<iostream>
#include<vector>
#include<algorithm>

template<typename Container>//需要一个容器类型
void Show(const Container con)
{
	typename Container::const_iterator it = con.begin();
	while (it != con.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
}
int main()
{
	int arr[] = { 1, 2, 1, 1, 1, 2, 1, 1 };
	int len = sizeof(arr) / sizeof(arr[0]);
	std::vector<int> vec(arr, arr + len);

	std::vector<int>::iterator it = vec.begin();
	while (it != vec.end())
	{
		if (*it == 1)
		{
			it=vec.erase(it);//erase(it) 返回值 重新拿到有效位置
		}
		else
		{
			it++;
		}
	}
	Show(vec);
	return 0;
}

 erase()会重新返回有效位置给迭代器it,让迭代器it变得重新有效,这样就可以解决迭代器失效的问题了。

发布了88 篇原创文章 · 获赞 40 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ThinPikachu/article/details/104950471