第6章 C++ STL迭代器适配器总结
本章将介绍 5 种迭代器适配器,分别是:
- 反向迭代器适配器、
- 插入型迭代器适配器、
- 流迭代器适配器、
- 流缓冲区迭代器适配器、
- 移动迭代器适配器。
初学者完全可以将迭代器适配器视为普通迭代器。之所以称为迭代器适配器,是因为这些迭代器是在输入迭代器、输出迭代器、前向迭代器、双向迭代器或者随机访问迭代器这些基础迭代器的基础上实现的。也就是说,使用迭代器适配器的过程中,其本质就是在操作某种基础迭代器。
C++ STL迭代器适配器简介
C++ STL 标准库中迭代器大致分为 5 种类型,分别是:
- 输入迭代器、
- 输出迭代器、
- 前向迭代器、
- 双向迭代器
- 以及随机访问迭代器。
值得一提的是,这 5 种迭代器是 STL 标准库提供的最基础的迭代器,很多场景中遍历容器的需求,它们并不适合。
所谓迭代器适配器,其本质也是一个类模板,比较特殊的是,该类模板是借助以上5种基础迭代器实现的。换句话说,迭代器适配器模板类的内部实现,是通过对以上 5 种基础迭代器拥有的成员方法进行整合、修改,甚至为了实现某些功能还会添加一些新的成员方法。由此,将基础迭代器“改头换面”,就变成了本节要讲的迭代器适配器。
本质上讲,迭代器适配器仍属于迭代器,可以理解为是基础迭代器的“翻新版”或者“升级版”。同时,“xxx 迭代器适配器”通常直接称为“xxx迭代器”。
C++ 11标准中,迭代器适配器共有4类,它们各自的名称和功能如表 1 所示。
名称 | 功能 |
---|---|
反向迭代器(reverse_iterator) (个人:也就是说反向迭代器内部重新定义了++和--,所以反向迭代器的 实现所要求的基础迭代器必须能够++和--,所以至少得双向迭代器,前向迭代器只能++,不能--,所以直接pass掉) |
又称“逆向迭代器”,其内部重新定义了递增运算符(++)和递减运算符(--),专门用来实现对容器的逆序遍历。 |
安插型迭代器(inserter或者insert_iterator) | 通常用于在容器的任何位置添加新的元素,需要注意的是,此类迭代器不能被运用到元素个数固定的容器(比如 array)上。 |
流迭代器(istream_iterator / ostream_iterator) 流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator) |
输入流迭代器用于从文件或者键盘读取数据;相反,输出流迭代器用于将数据输出到文件或者屏幕上。输入流缓冲区迭代器用于从输入缓冲区中逐个读取数据;输出流缓冲区迭代器用于将数据逐个写入输出流缓冲区。 |
移动迭代器(move_iterator) | 此类型迭代器是 C++ 11 标准中新添加的,可以将某个范围的类对象移动到目标范围,而不需要通过拷贝去移动。 |
下面样例,演示了用反向迭代器适配器遍历 list 容器的实现过程:
#include <iostream> #include <list> #include <iterator> using namespace std; int main() { std::list<int> values{ 1,2,3,4,5 }; //找到遍历的起点和终点,这里无需纠结定义反向迭代器的语法,后续会详细讲解 std::reverse_iterator<std::list<int>::iterator> begin = values.rbegin(); std::reverse_iterator<std::list<int>::iterator> end = values.rend(); while (begin != end) { cout << *begin << " "; //注意,这里是 ++,因为反向迭代器内部互换了 ++ 和 -- 的含义 ++begin; } return 0; }
C++ STL反向迭代器适配器(reverse_iterator)详解
反向迭代器适配器(reverse_iterator),可简称为反向迭代器或逆向迭代器,常用来对容器进行逆序遍历,即从容器中存储的最后一个元素开始,一直遍历到第一个元素。 值得一提的是,反向迭代器底层可以选用双向迭代器或者随机访问迭代器作为其基础迭代器。不仅如此,通过对 ++(递增)和 --(递减)运算符进行重载,使得:
- 当反向迭代器执行 ++ 运算时,底层的基础迭代器实则在执行 -- 操作,意味着反向迭代器在反向遍历容器;
- 当反向迭代器执行 -- 运算时,底层的基础迭代器实则在执行 ++ 操作,意味着反向迭代器在正向遍历容器。
另外,实现反向迭代器的类模板定义在<iterator>头文件,并位于std命名空间中。因此,在使用反向迭代器时,需包含如下语句:
#include <iterator> using namespace std;
反向迭代器的模板类定义如下:
template <class Iterator> class reverse_iterator;
注意,Iterator模板参数指的是类模板中所用的基础迭代器的类型,只能选择双向迭代器或者随机访问迭代器。 这意味着,如果想使用反向迭代器实现逆序遍历容器,则该容器的迭代器类型必须是双向迭代器或者随机访问迭代器。
reverse_iterator模板类中共提供了3种创建反向迭代器的方法,
- 调用该类的默认构造方法,即可创建一个不指向任何对象的反向迭代器,例如:(个人:注意reverse_iterator是一个类模板,实例化时需要显示的提供模板类型实参,这个实参是这个反向迭代器适配器的底层实现使用的基础迭代器)
std::reverse_iterator<std::vector<int>::iterator> my_reiter;
- 当然,在创建反向迭代器的时候,我们可以直接将一个基础迭代器作为参数传递给新建的反向迭代器。例如:
//创建并初始化一个 myvector 容器 std::vector<int> myvector{1,2,3,4,5}; //创建并初始化 my_reiter 迭代器 std::reverse_iterator<std::vector<int>::iterator> my_reiter(myvector.end());
我们知道,反向迭代器是通过操纵内部的基础迭代器实现逆向遍历的,但是反向迭代器的指向和底层基础迭代器的指向并不相同。以上面创建的 my_reiter 为例,其内部的基础迭代器指向的是 myvector 容器中元素 5 之后的位置,但是 my_reiter 指向的却是元素 5。 也就是说,反向迭代器的指向和其底层基础迭代器的指向具有这样的关系,即反向迭代器的指向总是距离基础迭代器偏左 1 个位置;反之,基础迭代器的指向总是距离反向迭代器偏右 1 个位置处。
它们的关系如图 1 所示。
其中,begin和end表示基础迭代器,r(begin) 和 r(end) 分别表示由begin和end获得的反向迭代器。
- reverse_iterator模板类还提供了一个复制(拷贝)构造函数,可以实现直接将一个反向迭代器复制给新建的反向迭代器。比如:
//创建并初始化一个 vector 容器 std::vector<int> myvector{1,2,3,4,5}; //调用复制构造函数初始化反向迭代器的 2 种方式 std::reverse_iterator<std::vector<int>::iterator> my_reiter(myvector.rbegin()); //std::reverse_iterator<std::vector<int>::iterator> my_reiter = myvector.rbegin();
前面在学习每一种容器时,都提供有大量的成员函数。但迭代器模板类不同,其内部更多的是对运算符的重载。reverse_iterator模板类中,重载了如表 1 所示的这些运算符。
重载运算符 | 功能 |
---|---|
operator* | 以引用的形式返回当前迭代器指向的元素。 |
operator+ | 返回一个反向迭代器,其指向距离当前指向的元素之前 n 个位置的元素。此操作要求基础迭代器为随机访问迭代器。 |
operator++ | 重载前置 ++ 和后置 ++ 运算符。 |
operator+= | 当前反向迭代器前进 n 个位置,此操作要求基础迭代器为随机访问迭代器。 |
operator- | 返回一个反向迭代器,其指向距离当前指向的元素之后 n 个位置的元素。此操作要求基础迭代器为随机访问迭代器。 |
operator-- | 重载前置 -- 和后置 -- 运算符。 |
operator-= | 当前反向迭代器后退 n 个位置,此操作要求基础迭代器为随机访问迭代器。 |
operator-> | 返回一个指针,其指向当前迭代器指向的元素。 |
operator[n] | 访问和当前反向迭代器相距 n 个位置处的元素。此操作要求基础迭代器为随机访问迭代器。 |
下面程序演示了表1中部分运算符的用法:
#include <iostream> #include <iterator> #include <vector> using namespace std; int main() { //创建并初始化一个 vector 容器 std::vector<int> myvector{ 1,2,3,4,5,6,7,8 }; //创建并初始化一个反向迭代器 std::reverse_iterator<std::vector<int>::iterator> my_reiter(myvector.rbegin());//指向 8 cout << *my_reiter << endl;// 8 cout << *(my_reiter + 3) << endl;// 5 cout << *(++my_reiter) << endl;// 7 cout << my_reiter[4] << endl;// 3 return 0; }
除此之外,reverse_iterator 模板类还提供了base() 成员方法,该方法可以返回当前反向迭代器底层所使用的基础迭代器。举个例子:
#include <iostream> #include <iterator> #include <vector> using namespace std; int main() { //创建并初始化一个 vector 容器 std::vector<int> myvector{ 1,2,3,4,5,6,7,8 }; //创建并初始化反向迭代器 begin,其指向元素 1 之前的位置 std::reverse_iterator<std::vector<int>::iterator> begin(myvector.begin()); //创建并初始化反向迭代器 end,其指向元素 8 std::reverse_iterator<std::vector<int>::iterator> end(myvector.end()); //begin底层基础迭代器指向元素 1,end底层基础迭代器指向元素 8 之后的位置 for (auto iter = begin.base(); iter != end.base(); ++iter) { std::cout << *iter << ' '; } return 0; }
C++ STL插入迭代器适配器(insert_iterator)
插入迭代器适配器(insert_iterator),简称插入迭代器或者插入器,其功能就是向指定容器中插入元素。值得一提的是,根据插入位置的不同,C++ STL 标准库提供了 3 种插入迭代器适配器,如表 1 所示。
迭代器适配器 | 功能 |
---|---|
back_insert_iterator | 在指定容器的尾部插入新元素,但前提必须是提供有 push_back() 成员方法的容器(包括 vector、deque 和list)。 |
front_insert_iterator | 在指定容器的头部插入新元素,但前提必须是提供有 push_front() 成员方法的容器(包括 list、deque 和 forward_list)。 |
insert_iterator | 在容器的指定位置之前插入新元素,前提是该容器必须提供有 insert() 成员方法。 |
C++ STL back_insert_iterator迭代器:
back_insert_iterator 迭代器可用于在指定容器的末尾处添加新元素。 需要注意的是,由于此类型迭代器的底层实现需要调用指定容器的 push_back() 成员方法,这就意味着,此类型迭代器并不适用于 STL 标准库中所有的容器,它只适用于包含 push_back() 成员方法的容器。 C++ STL 标准库中,提供有 push_back() 成员方法的容器包括 vector、deque 和 list。
back_insert_iterator 迭代器定义在 <iterator> 头文件,并位于 std 命名空间中,因此在使用该类型迭代器之前,程序应包含如下语句:
#include <iterator> using namespace std;
back_insert_iterator 插入迭代器的定义方式仅有一种,其语法格式如下:
std::back_insert_iterator<Container> back_it (container);
其中,Container 用于指定插入的目标容器的类型;container 用于指定具体的目标容器。
//创建一个 vector 容器 std::vector<int> foo; //创建一个可向 foo 容器尾部添加新元素的迭代器 std::back_insert_iterator< std::vector<int> > back_it(foo);
在此基础上,back_insert_iterator 迭代器模板类中还对赋值运算符(=)进行了重载,借助此运算符,我们可以直接将新元素插入到目标容器的尾部。例如:
#include <iostream> #include <iterator> #include <vector> using namespace std; int main() { //创建一个 vector 容器 std::vector<int> foo; //创建一个可向 foo 容器尾部添加新元素的迭代器 std::back_insert_iterator< std::vector<int> > back_it(foo); //将 5 插入到 foo 的末尾 back_it = 5; //将 4 插入到当前 foo 的末尾 back_it = 4; //将 3 插入到当前 foo 的末尾 back_it = 3; //将 6 插入到当前 foo 的末尾 back_it = 6; //输出 foo 容器中的元素 for (std::vector<int>::iterator it = foo.begin(); it != foo.end(); ++it) std::cout << *it << ' '; return 0; }
除此之外,C++ STL 标准库为了方便用户创建 back_insert_iterator 类型的插入迭代器,提供了 back_inserter() 函数,其语法格式如下:
template <class Container> back_insert_iterator<Container> back_inserter (Container& x);
显然在使用该函数时,只需要为其传递一个具体的容器(vector、deque 或者 list)做参数,此函数即可返回一个 back_insert_iterator 类型的插入迭代器。因此,上面程序中的第 9 行代码,可替换成如下语句:
std::back_insert_iterator< std::vector<int> > back_it = back_inserter(foo);
有关此类型迭代器的底层实现,C++ STL back_insert_iterator官方手册给出了具体的实现代码,有兴趣的读者可自行前往查看。
C++ STL front_insert_iterator迭代器:
和 back_insert_iterator 正好相反,front_insert_iterator 迭代器的功能是向目标容器的头部插入新元素。 并且,由于此类型迭代器的底层实现需要借助目标容器的 push_front() 成员方法,这意味着,只有包含 push_front() 成员方法的容器才能使用该类型迭代器。 C++ STL 标准库中,提供有 push_front() 成员方法的容器,仅有 deque、list 和 forward_list。
另外,front_insert_iterator 迭代器定义在 <iterator> 头文件,并位于 std 命名空间中,因此在使用该类型迭代器之前,程序应包含如下语句:
#include <iterator> using namespace std;
值得一提的是,定义 front_insert_iterator 迭代器的方式和 back_insert_iterator 完全相同,并且 C++ STL 标准库也提供了 front_inserter() 函数来快速创建该类型迭代器。
#include <iostream> #include <iterator> #include <forward_list> using namespace std; int main() { //创建一个 forward_list 容器 std::forward_list<int> foo; //创建一个前插入迭代器 //std::front_insert_iterator< std::forward_list<int> > front_it(foo); std::front_insert_iterator< std::forward_list<int> > front_it = front_inserter(foo); //向 foo 容器的头部插入元素 front_it = 5; front_it = 4; front_it = 3; front_it = 6; for (std::forward_list<int>::iterator it = foo.begin(); it != foo.end(); ++it) std::cout << *it << ' '; return 0; }
同样,C++ STL front_insert_iterator官方手册也给出了此类型迭代器底层实现的参考代码,有兴趣的读者可自行前往查看。
C++ STL insert_iterator迭代器:
当需要向容器的任意位置插入元素时,就可以使用 insert_iterator 类型的迭代器。 需要说明的是,该类型迭代器的底层实现,需要调用目标容器的 insert() 成员方法。但幸运的是,STL 标准库中大多数容器都提供有 insert() 成员方法,因此 insert_iterator 是唯一可用于关联式容器的插入迭代器。
和前 2 种插入迭代器一样,insert_iterator 迭代器也定义在 <iterator> 头文件,并位于 std 命名空间中,因此在使用该类型迭代器之前,程序应包含如下语句:
#include <iterator> using namespace std;
不同之处在于,定义 insert_iterator 类型迭代器的语法格式如下:
std::insert_iterator<Container> insert_it (container,it);
其中,Container 表示目标容器的类型,参数 container 表示目标容器,而 it 是一个基础迭代器,表示新元素的插入位置。
和前 2 种插入迭代器相比,insert_iterator 迭代器除了定义时需要多传入一个参数,它们的用法完全相同。
除此之外,C++ STL 标准库中还提供有 inserter() 函数,可以快速创建insert_iterator类型迭代器。
#include <iostream> #include <iterator> #include <list> using namespace std; int main() { //初始化为 {5,5} std::list<int> foo(2,5); //定义一个基础迭代器,用于指定要插入新元素的位置 std::list<int>::iterator it = ++foo.begin(); //创建一个 insert_iterator 迭代器 //std::insert_iterator< std::list<int> > insert_it(foo, it); std::insert_iterator< std::list<int> > insert_it = inserter(foo, it); //向 foo 容器中插入元素 insert_it = 1; insert_it = 2; insert_it = 3; insert_it = 4; //输出 foo 容器存储的元素 for (std::list<int>::iterator it = foo.begin(); it != foo.end(); ++it) std::cout << *it << ' '; return 0; }
需要注意的是,如果 insert_iterator 迭代器的目标容器为关联式容器,由于该类型容器内部会自行对存储的元素进行排序,因此我们指定的插入位置只起到一个提示的作用,即帮助关联式容器从指定位置开始,搜索正确的插入位置。但是,如果提示位置不正确,会使的插入操作的效率更加糟糕。
C++ STL insert_iterator官方手册中给出了此类型迭代器底层实现的参考代码,有兴趣的读者可自行前往查看。
总结:
本节讲解了 3 种插入迭代器,虽然它们都可以借助重载的赋值运算符,实现向目标容器的指定位置插入新元素,但在实际应用中,它们通常和 copy() 函数连用,即作为 copy() 函数的第 3 个参数。
C++ STL流迭代器(istream_iterator和ostream_iterator)
流迭代器也是一种迭代器适配器,不过和之前讲的迭代器适配器有所差别,它的操作对象不再是某个容器,而是流对象。即通过流迭代器,我们可以读取指定流对象中的数据,也可以将数据写入到流对象中。
鉴于流对象又可细分为输入流对象(istream)和输出流对象(ostream),C++ STL 标准库中,也对应的提供了2类流迭代器:
- 将绑定到输入流对象的迭代器称为输入流迭代器(istream_iterator),其可以用来读取输入流中的数据;
- 将绑定到输出流对象的迭代器称为输出流迭代器(ostream_iterator),其用来将数据写入到输出流中。
C++ STL输入流迭代器(istream_iterator):
===================================================
===========================================
==============================================
输入流迭代器用于直接从指定的输入流中读取元素,该类型迭代器本质上就是一个输入迭代器,这意味着假设 p 是一个输入流迭代器,则其只能进行 ++p、p++、*p 操作,同时输入迭代器之间也只能使用 == 和 != 运算符。
实际上,输入流迭代器的底层是通过重载 ++ 运算符实现的,该运算符内部会调用operator >>读取数据。也就是说,假设 iit 为输入流迭代器,则只需要执行 ++iit 或者 iit++,即可读取一个指定类型的元素。
值得一提的是,istream_iterator 定义在<iterator>头文件,并位于 std 命名空间中,因此使用此迭代器之前,程序中应包含如下语句:
#include <iterator> using namespace std;
创建输入流迭代器的方式有 3 种,分别为:
- 调用 istream_iterator 模板类的默认构造函数,可以创建出一个具有结束标志的输入流迭代器。要知道,当我们从输入流中不断提取数据时,总有将流中数据全部提取完的那一时刻,这一时刻就可以用此方式构建的输入流迭代器表示。
std::istream_iterator<double> eos;
由此,即创建了一个可读取double类型元素,并代表结束标志的输入流迭代器。
- 除此之外,还可以创建一个可用来读取数据的输入流迭代器,比如:
std::istream_iterator<double> iit(std::cin);
这里创建了一个可从标准输入流 cin 读取数据的输入流迭代器。值得注意的一点是,通过此方式创建的输入流迭代器,其调用的构造函数中,会自行尝试去指定流中读取一个指定类型的元素。
- istream_iterator类模板还支持用已创建好的istream_iterator迭代器为新建 istream_iterator 迭代器初始化,例如,在上面 iit 的基础上,再创建一个相同的 iit2 迭代器:
std::istream_iterator<double> iit2(iit1);
下面程序演示了输入流迭代器的用法:
#include <iostream> #include <iterator> using namespace std; int main() { //用于接收输入流中的数据 double value1, value2; cout << "请输入 2 个小数: "; //创建表示结束的输入流迭代器 istream_iterator<double> eos; //创建一个可逐个读取输入流中数据的迭代器,同时这里会让用户输入数据 istream_iterator<double> iit(cin); //判断输入流中是否有数据 if (iit != eos) { //读取一个元素,并赋值给 value1 value1 = *iit; } //如果输入流中此时没有数据,则用户要输入一个;反之,如果流中有数据,iit 迭代器后移一位,做读取下一个元素做准备 iit++; if (iit != eos) { //读取第二个元素,赋值给 value2 value2 = *iit; } //输出读取到的 2 个元素 cout << "value1 = " << value1 << endl; cout << "value2 = " << value2 << endl; return 0; }
注意,只有读取到 EOF 流结束符时,程序中的 iit 才会和 eos 相等。另外,Windows 平台上使用 Ctrl+Z 组合键输入 ^Z 表示 EOF 流结束符,此结束符需要单独输入,或者输入换行符之后再输入才有效。
C++ STL输出流迭代器(ostream_iterator):
==================================================
=================================================
==================================================
=====================================================
和输入流迭代器恰好相反,输出流迭代器用于将数据写到指定的输出流(如 cout)中。另外,该类型迭代器本质上属于输出迭代器,假设 p 为一个输出迭代器,则它能执行 ++p、p++、*p=t 以及 *p++=t 等类似操作。
其次,输出迭代器底层是通过重载赋值(=)运算符实现的,即借助该运算符,每个赋值给输出流迭代器的元素都会被写入到指定的输出流中。
值得一提的是,实现 ostream_iterator 迭代器的模板类也定义在<iterator>头文件,并位于 std 命名空间中,因此在使用此类型迭代器时,程序也应该包含以下 2 行代码:
#include <iterator> using namespace std;
ostream_iterator 模板类中也提供了 3 种创建 ostream_iterator 迭代器的方法。
- 通过调用该模板类的默认构造函数,可以创建了一个指定输出流的迭代器:
std::ostream_iterator<int> out_it(std::cout);
由此,我们就创建了一个可将 int 类型元素写入到输出流(屏幕)中的迭代器。
- 在第一种方式的基础上,还可以为写入的元素之间指定一个分隔符,例如:
std::ostream_iterator<int> out_it(std::cout,",");
和第一种写入方式不同之处在于,此方式在向输出流写入 int 类型元素的同时,还会附带写入一个逗号(,)。
- 另外,在创建输出流迭代器时,可以用已有的同类型的迭代器,为其初始化。例如,利用上面已创建的out_it,再创建一个完全相同的 out_it1:
std::ostream_iterator<int> out_it1(out_it);
下面程序演示了ostream_iterator输出流迭代器的功能:
#include <iostream> #include <iterator> #include <string> using namespace std; int main() { //创建一个输出流迭代器 ostream_iterator<string> out_it(cout); //向 cout 输出流写入 string 字符串 *out_it = "http://www.baidu.com/computer/programme/stl/"; cout << endl<<"===================================="<<endl; //创建一个输出流迭代器,设置分隔符 , ostream_iterator<int> out_it1(cout, ","); //向 cout 输出流依次写入 1、2、3 *out_it1 = 1; *out_it1 = 2; *out_it1 = 3; return 0; }
在实际场景中,输出流迭代器常和 copy() 函数连用,即作为该函数第 3 个参数。比如:
#include <iostream> #include <iterator> #include <vector> #include <algorithm> // std::copy using namespace std; int main() { //创建一个 vector 容器 vector<int> myvector; //初始化 myvector 容器 for (int i = 1; i < 10; ++i) { myvector.push_back(i); } //创建输出流迭代器 std::ostream_iterator<int> out_it(std::cout, ", "); //将 myvector 容器中存储的元素写入到 cout 输出流中 std::copy(myvector.begin(), myvector.end(), out_it); return 0; }
C++ STL流缓冲区迭代器(streambuf_iterator)
我们知道在 C++ STL 标准库中,流迭代器又细分为输入流迭代器和输出流迭代器,流缓冲区迭代器也是如此,其又被细分为输入流缓冲区迭代器和输出流缓冲区迭代器:
- 输入流缓冲区迭代器(istreambuf_iterator):从输入流缓冲区中读取字符元素;
- 输出流缓冲区迭代器(ostreambuf_iterator):将连续的字符元素写入到输出缓冲区中。
流缓冲区迭代器和流迭代器最大的区别在于,前者仅仅会将元素以字符的形式(包括char、wchar_t、char16_t及char32_t等)读或者写到流缓冲区中,由于不会涉及数据类型的转换,读写数据的速度比后者要快。(个人:也就是流缓冲区迭代器的模板元素类型只能是char,wchar_t,char16_t,char32_t,而流迭代器可以是任意的元素类型,例如,int,string,)
C++ STL输入流缓冲区迭代器(istreambuf_iterator):
===================================================================
=====================================================
===================================================
istreambuf_iterator输入流缓冲区迭代器的功能是从指定的流缓冲区中读取字符元素。值得一提的是,该类型迭代器本质是一个输入迭代器,即假设 p 是一个输入流缓冲区迭代器,则其只能进行 ++p、p++、*p操作,同时迭代器之间也只能使用 == 和 != 运算符。
另外,实现该类型迭代器的模板类也定义在<iterator>头文件,并位于 std 命名空间中。因此,在创建并使用该类型迭代器之前,程序中应包含如下代码:
#include <iterator> using namespace std;
创建输入流缓冲区迭代器的常用方式,有以下 2 种:
- 通过调用 istreambuf_iterator 模板类中的默认构造函数,可以创建一个表示结尾的输入流缓冲区迭代器。要知道,当我们从流缓冲区中不断读取数据时,总有读取完成的那一刻,这一刻就可以用此方式构建的流缓冲区迭代器表示。
std::istreambuf_iterator<char> end_in;
其中,<> 尖括号中用于指定从流缓冲区中读取的字符类型。
- 当然,我们还可以指定要读取的流,比如:
std::istreambuf_iterator<char> in{ std::cin };
除此之外,还可以传入流缓冲区的地址,比如:
std::istreambuf_iterator<char> in {std::cin.rdbuf()};
其中,rdbuf() 函数的功能是获取指定流缓冲区的地址。
#include <iostream> // std::cin, std::cout #include <iterator> // std::istreambuf_iterator #include <string> // std::string using namespace std; int main() { //创建结束流缓冲区迭代器 istreambuf_iterator<char> eos; //创建一个从输入缓冲区读取字符元素的迭代器 istreambuf_iterator<char> iit(cin); string mystring; cout << "向缓冲区输入元素:\n"; //不断从缓冲区读取数据,直到读取到 EOF 流结束符 while (iit != eos) { mystring += *iit++; } cout << "string:" << mystring; return 0; }
(个人:根据程序的运行结果,如果流缓冲区为空,会要求我们输入数据,输入的数据会放到缓冲区中,再执行后面的操作)
注意,只有读取到 EOF 流结束符时,程序中的 iit 才会和 eos 相等。在 Windows 平台上,使用 Ctrl+Z 组合键输入 ^Z 表示 EOF 流结束符,此结束符需要单独输入,或者输入换行符之后再输入才有效。
C++ STL输出流缓冲区迭代器(ostreambuf_iterator):
和 istreambuf_iterator 输入流缓冲区迭代器恰恰相反,ostreambuf_iterator 输出流缓冲区迭代器用于将字符元素写入到指定的流缓冲区中。 实际上,该类型迭代器本质上是一个输出迭代器,这意味着假设 p 为一个输出迭代器,则它仅能执行 ++p、p++、*p=t 以及 *p++=t 操作。
另外,和 ostream_iterator 输出流迭代器一样,istreambuf_iterator 迭代器底层也是通过重载赋值(=)运算符实现的。换句话说,即通过赋值运算符,每个赋值给输出流缓冲区迭代器的字符元素,都会被写入到指定的流缓冲区中。
需要指出的是,istreambuf_iterator 类模板也定义在<iterator>头文件,并位于 std 命名空间中,因此使用该类型迭代器,程序中需要包含以下代码:
#include <iterator> using namespace std;
在此基础上,创建输出流缓冲区迭代器的常用方式有以下 2 种:
- 通过传递一个输出流对象,即可创建一个输出流缓冲区迭代器,比如:
std::ostreambuf_iterator<char> out_it (std::cout);
同样,尖括号 <> 中用于指定要写入字符的类型,可以是 char、wchar_t、char16_t 以及 char32_t 等。
- 还可以借助 rdbuf(),传递一个流缓冲区的地址,也可以成功创建输出流缓冲区迭代器:
std::ostreambuf_iterator<char> out_it (std::cout.rdbuf());
下面程序演示了输出流缓冲区迭代器的用法:
#include <iostream> // std::cin, std::cout #include <iterator> // std::ostreambuf_iterator #include <string> // std::string #include <algorithm> // std::copy int main() { //创建一个和输出流缓冲区相关联的迭代器 std::ostreambuf_iterator<char> out_it(std::cout); // stdout iterator //向输出流缓冲区中写入字符元素 *out_it = 'S'; *out_it = 'T'; *out_it = 'L'; //和 copy() 函数连用 std::string mystring("\nhttp://www.baidu.com/stl/"); //将 mystring 中的字符串全部写入到输出流缓冲区中 std::copy(mystring.begin(), mystring.end(), out_it); return 0; }
C++ STL move_iterator移动迭代器
C++ 11 还为 STL 标准库增添了一种迭代器适配器,即本节要讲的 move_iterator 移动迭代器适配器。 move_iterator 迭代器适配器,又可简称为移动迭代器,其可以实现移动而非复制的方式,将某个区域空间中的元素移动至另一个指定的空间。举个例子,
#include <iostream> #include <vector> #include <list> #include <string> using namespace std; int main() { //创建并初始化一个 vector 容器 vector<string> myvec{ "STL","Python","Java" }; //再次创建一个 vector 容器,利用 myvec 为其初始化 vector<string>othvec(myvec.begin(), myvec.end()); cout << "myvec:" << endl; //输出 myvec 容器中的元素 for (auto ch : myvec) { cout << ch << " "; } cout << endl << "othvec:" << endl; //输出 othvec 容器中的元素 for (auto ch : othvec) { cout << ch << " "; } return 0; }
注意程序第 11 行,初始化 othvec 容器是通过复制myvec容器中的元素实现的。也就是说,othvec 容器从 myvec 容器中复制了一份 "STL"、"Python"、"Java" 并存储起来,此过程不会影响 myvec 容器。 那么,如果不想采用复制的方式,而就是想 myvec 容器中存储的元素全部移动到 othvec 容器中,该怎么办呢?没错,就是采用移动迭代器适配器。
值得一提的是,实现移动迭代器的模板类定义在 <iterator> 头文件,并位于 std 命名空间中。因此,在使用该类型迭代器时,程序中应包含如下代码:
#include <iterator> using namespace std;
实现move_iterator移动迭代器的模板类定义如下:
template <class Iterator> class move_iterator;
可以看到,在使用此迭代器时,需要传入一个基础迭代器 Iterator。注意,此基础迭代器的类型虽然没有明确要求,但该类模板中某些成员方法的底层实现,需要此基础迭代器为双向迭代器或者随机访问迭代器。也就是说,如果指定的 Iterator 类型仅仅是输入迭代器,则某些成员方法将无法使用。
实际上,在 move_iterator 类模板中就包含有指定 Iterator 类型的基础迭代器,整个类模板也是借助此基础迭代器实现的。关于 move_iterator 的底层实现,C++ STL move_iterator手册给出了详细的参考代码,有兴趣的读者可自行研究。
- 通过调用该模板类的默认构造函数,可以创建一个不指向任何对象的移动迭代器。比如:
//将 vector 容器的随机访问迭代器作为新建移动迭代器底层使用的基础迭代器 typedef std::vector<std::string>::iterator Iter; //调用默认构造函数,创建移动迭代器 std::move_iterator<Iter>mIter;
- 当然,在创建 move_iterator 迭代器的同时,也可以为其初始化。比如:
//创建一个 vector 容器 std::vector<std::string> myvec{ "one","two","three" }; //将 vector 容器的随机访问迭代器作为新建移动迭代器底层使用的基础迭代器 typedef std::vector<std::string>::iterator Iter; //创建并初始化移动迭代器 std::move_iterator<Iter>mIter(myvec.begin());
这里,我们创建了一个 mIter 移动迭代器,同时还为底层使用的随机访问迭代器做了初始化,即令其指向 myvec 容器的第一个元素。
- move_iterator模板类还支持用已有的移动迭代器初始化新建的同类型迭代器,比如,在上面创建好 mIter 迭代器的基础上,还可以向如下这样为新建的移动迭代器初始化:
std::move_iterator<Iter>mIter2(mIter); //还可以使用 = 运算符,它们是等价的 //std::move_iterator<Iter>mIter2 = mIter;
- 以上 3 种创建 move_iterator 迭代器的方式,其本质都是直接调用 move_iterator 模板类中的构造方法实现的。除此之外,C++ STL 标准库还提供了一个 make_move_iterator() 函数,通过调用此函数可以快速创建一个 move_iterator 迭代器。 C++ STL 标准库中,make_move_iterator() 是以函数模板的形式提供的,其语法格式如下:
template <class Iterator> move_iterator<Iterator> make_move_iterator (const Iterator& it);
其中,参数it为基础迭代器,用于初始化新建迭代器。同时,该函数会返回一个创建好的移动迭代器。举个例子:
typedef std::vector<std::string>::iterator Iter; std::vector<std::string> myvec{ "one","two","three" }; //将 make_move_iterator() 的返回值赋值给同类型的 mIter 迭代器 std::move_iterator<Iter>mIter = make_move_iterator(myvec.begin());
下面程序运用移动迭代器为othvec容器初始化:
#include <iostream> #include <vector> #include <vector> #include <string> using namespace std; int main() { //创建并初始化一个 vector 容器 vector<string> myvec{ "STL","Python","Java" }; //再次创建一个 vector 容器,利用 myvec 为其初始化 vector<string>othvec(make_move_iterator(myvec.begin()), make_move_iterator(myvec.end())); cout << "myvec:" << endl; //输出 myvec 容器中的元素 for (auto ch : myvec) { cout << ch << " "; } cout << endl << "othvec:" << endl; //输出 othvec 容器中的元素 for (auto ch : othvec) { cout << ch << " "; } return 0; }
由于程序第11行为othvec容器初始化时,使用的是移动迭代器,其会将 myvec 容器中的元素直接移动到 othvec 容器中。 注意,即便通过移动迭代器将容器中某区域的元素移动到了其他容器中,该区域内仍可能残留有之前存储的元素,但这些元素是不能再被使用的,否则极有可能使程序产生各种其他错误。
和其他迭代器适配器一样,move_iterator 类模板中也提供有 base() 成员方法,通过该方法,我们可以获取到当前移动迭代器底层所使用的基础迭代器。
#include <iostream> #include <vector> #include <iterator> #include <string> using namespace std; int main() { typedef std::vector<std::string>::iterator Iter; //创建并初始化一个 vector 容器 vector<std::string> myvec{ "STL","Java","Python" }; //创建 2 个移动迭代器 std::move_iterator<Iter>begin = make_move_iterator(myvec.begin()); std::move_iterator<Iter>end = make_move_iterator(myvec.end()); //以复制的方式初始化 othvec 容器 vector <std::string> othvec(begin.base(), end.base()); cout << "myvec:" << endl; //输出 myvec 容器中的元素 for (auto ch : myvec) { cout << ch << " "; } cout << endl << "othvec:" << endl; //输出 othvec 容器中的元素 for (auto ch : othvec) { cout << ch << " "; } return 0; }
显然,通过调用 base() 成员方法,初始化 othvec 容器的方式转变为以复制而非移动的方式,因此 myvec 容器不会受到影响。
C++ STL迭代器辅助函数
为了方便用户操作这些迭代器,C++ STL 标准库中还提供有一些辅助函数,如表 1 所示。
迭代器辅助函数 | 功能 |
---|---|
advance(it, n) | it 表示某个迭代器,n 为整数。该函数的功能是将 it 迭代器前进或后退 n 个位置。 |
distance(first, last) | first 和 last 都是迭代器,该函数的功能是计算 first 和 last 之间的距离。 |
begin(cont) | cont 表示某个容器,该函数可以返回一个指向 cont 容器中第一个元素的迭代器。 |
end(cont) | cont 表示某个容器,该函数可以返回一个指向 cont 容器中最后一个元素之后位置的迭代器。 |
prev(it) | it 为指定的迭代器,该函数默认可以返回一个指向上一个位置处的迭代器。注意,it 至少为双向迭代器。 |
next(it) | it 为指定的迭代器,该函数默认可以返回一个指向下一个位置处的迭代器。注意,it 最少为前向迭代器。 |
C++ STL advance()函数
advance() 函数用于将迭代器前进(或者后退)指定长度的距离,其语法格式如下:
template <class InputIterator, class Distance> void advance (InputIterator& it, Distance n);
其中 it 指的是目标迭代器,n 通常为一个整数。
需要注意的是,
- 如果 it 为输入迭代器或者前向迭代器,则 n 必须为一个正数,即表示将 it 右移(前进) n 个位置;
- 反之,如果 it 为双向迭代器或者随机访问迭代器,则 n 为正数时表示将 it 右移(前进) n 个位置,n 为负数时表示将 it 左移(后退) n 个位置。
另外,根据it类型是否为随机访问迭代器,advance() 函数底层采用了不同的实现机制:
- 当 it 为随机访问迭代器时,由于该类型迭代器支持 p+n 或者 p-n(其中 p 就是一个随机访问迭代器)运算,advance() 函数底层采用的就是 it+n 操作实现的;
- 当 it 为其他类型迭代器时,它们仅支持进行 ++ 或者 -- 运算,这种情况下,advance() 函数底层是通过重复执行 n 个 ++ 或者 -- 操作实现的。
值得一提的是,advance() 函数定义在<iterator>头文件,并位于 std 命名空间中。因此,程序在使用该函数之前,应包含如下 2 行代码:
#include <iterator> using namespace std;
首先以 forward_list 容器(仅支持使用前向迭代器)为例,下面程序演示了 advance() 函数的功能:
#include <iostream> // std::cout #include <iterator> // std::advance #include <forward_list> using namespace std; int main() { //创建一个 forward_list 容器 forward_list<int> mylist{1,2,3,4}; //it为前向迭代器,其指向 mylist 容器中第一个元素 forward_list<int>::iterator it = mylist.begin(); //借助 advance() 函数将 it 迭代器前进 2 个位置 advance(it, 2); cout << "*it = " << *it; return 0; }
此程序中,由于 it 为前向迭代器,其只能进行 ++ 操作,即只能前进(右移),所以 advance() 函数的第 2 个参数只能为正数。
下面程序以 vector 容器为例,演示了 advance() 函数的功能:
#include <iostream> // std::cout #include <iterator> // std::advance #include <vector> using namespace std; int main() { //创建一个 vector 容器 vector<int> myvector{1,2,3,4}; //it为随机访问迭代器,其指向 myvector 容器中第一个元素 vector<int>::iterator it = myvector.begin(); //借助 advance() 函数将 it 迭代器前进 2 个位置 advance(it, 2); cout << "1、*it = " << *it << endl; //继续使用it,其指向 myvector 容器中最后一个元素之后的位置 it = myvector.end(); //借助 advance() 函数将 it 迭代器后退 3 个位置 advance(it, -3); cout << "2、*it = " << *it; return 0; }
注意,advance() 函数本身不会检测 it 迭代器移动 n 个位置的可行性,如果 it 迭代器的移动位置超出了合理范围,it 迭代器的指向将无法保证,此时使用 *it 将会导致程序崩溃。
C++ STL distance()函数
我们知道,作用于同一容器的 2 个同类型迭代器可以有效指定一个区间范围。在此基础上,如果想获取该指定范围内包含元素的个数,就可以借助本节要讲的 distance() 函数。distance() 函数用于计算两个迭代器表示的范围内包含元素的个数,其语法格式如下:
template<class InputIterator> typename iterator_traits<InputIterator>::difference_type distance (InputIterator first, InputIterator last);
其中,first 和 last 都为迭代器,其类型可以是输入迭代器、前向迭代器、双向迭代器以及随机访问迭代器;该函数会返回[first, last)范围内包含的元素的个数。注意,first和last的迭代器类型,直接决定了 distance() 函数底层的实现机制:
- 当 first 和 last 为随机访问迭代器时,distance() 底层直接采用 last - first 求得 [first, last) 范围内包含元素的个数,其时间复杂度为O(1)常数阶;
- 当 first 和 last 为非随机访问迭代器时,distance() 底层通过不断执行 ++first(或者 first++)直到 first==last,由此来获取 [first, last) 范围内包含元素的个数,其时间复杂度为O(n)线性阶。
另外,distance() 函数定义在<iterator>头文件,并位于 std 命名空间中。因此在使用此函数前,程序中应包含如下代码:
#include <iterator> using namespace std;
下面程序以 list 容器(其迭代器类型为双向迭代器)为例,演示了 distance() 函数的用法:
#include <iostream> // std::cout #include <iterator> // std::distance #include <list> // std::list using namespace std; int main() { //创建一个空 list 容器 list<int> mylist; //向空 list 容器中添加元素 0~9 for (int i = 0; i < 10; i++) { mylist.push_back(i); } //指定 2 个双向迭代器,用于执行某个区间 list<int>::iterator first = mylist.begin();//指向元素 0 list<int>::iterator last = mylist.end();//指向元素 9 之后的位置 //获取 [first,last) 范围内包含元素的个数 cout << "distance() = " << distance(first, last); return 0; }
C++ STL begin()和end()函数
首先需要说明的是,begin() 和 end() 是以函数模板的形式定义的,但它们的模板并没有位于某一个头文件中,而是很多头文件中都有它们的定义。C++ STL 标准库中,包含 begin() 和 end() 函数模板的头文件包括:<iterator>, <array>, <deque>, <forward_list>, <list>, <map>, <regex>(正则表达式的头文件), <set>, <string>, <unordered_map>, <unordered_set> 以及 <vector>。不仅如此,begin() 和 end() 都位于 std 命名空间中。因此,在使用这 2 个函数之前,程序中应引入容纳它们函数模板的头文件以及 std 命名空间。
- 当将某个具体容器(比如 cont)作为参数分别传给 begin() 和 end() 函数时,其中 begin() 底层会执行 cont.begin() 语句,而 end() 底层会执行 cont.end() 语句,它们最终会将得到的迭代器作为函数的返回值反馈回来。
//① 非 const 修改的容器作为参数,begin() 函数返回的为非 const 类型的迭代器 template <class Container> auto begin (Container& cont) //② 传入 const 修饰的容器,begin() 函数返回的为 const 类型的迭代器 template <class Container> auto begin (const Container& cont)
- 将指定数组传给 begin() 函数,其会返回一个指向该数组首个元素的指针;将指定数组传给 end() 函数,其会返回一个指向数组中最后一个元素之后位置的指针。
template <class T, size_t N> T* begin (T(&arr)[N]);
其中 T 为数组中存储元素的类型,N 为数组的长度;(&arr)[N] 表示以引用的方式传递数组作为参数。
C++ STL prev()和next()函数
prev 原意为“上一个”,但 prev() 的功能远比它的本意大得多,该函数可用来获取一个距离指定迭代器 n 个元素的迭代器。prev() 函数的语法格式如下:
template <class BidirectionalIterator> BidirectionalIterator prev (BidirectionalIterator it, typename iterator_traits<BidirectionalIterator>::difference_type n = 1);
其中,it 为源迭代器,其类型只能为双向迭代器或者随机访问迭代器;n 为指定新迭代器距离 it 的距离,默认值为 1。该函数会返回一个距离 it 迭代器 n 个元素的新迭代器。注意,当 n 为正数时,其返回的迭代器将位于 it 左侧;反之,当 n 为负数时,其返回的迭代器位于 it 右侧。
#include <iostream> // std::cout #include <iterator> // std::next #include <list> // std::list using namespace std; int main() { //创建并初始化一个 list 容器 std::list<int> mylist{ 1,2,3,4,5 }; std::list<int>::iterator it = mylist.end(); //获取一个距离 it 迭代器 2 个元素的迭代器,由于 2 为正数,newit 位于 it 左侧 auto newit = prev(it, 2); cout << "prev(it, 2) = " << *newit << endl; //n为负数,newit 位于 it 右侧 it = mylist.begin(); newit = prev(it, -2); cout << "prev(it, -2) = " << *newit; return 0; }
注意,prev() 函数自身不会检验新迭代器的指向是否合理,需要我们自己来保证其合理性。
和 prev 相反,next 原意为“下一个”,但其功能和 prev() 函数类似,即用来获取一个距离指定迭代器 n 个元素的迭代器。next() 函数的语法格式如下:
template <class ForwardIterator> ForwardIterator next (ForwardIterator it, typename iterator_traits<ForwardIterator>::difference_type n = 1);
其中 it 为源迭代器,其类似可以为前向迭代器、双向迭代器以及随机访问迭代器;n 为指定新迭代器距离 it 的距离,默认值为 1。该函数会返回一个距离 it 迭代器 n 个元素的新迭代器。
需要注意的是,
- 当 it 为前向迭代器时,n 只能为正数,该函数最终得到的新迭代器位于 it 右侧;
- 当 it 为双向迭代器或者随机访问迭代器时,若 n 为正数,则得到的新迭代器位于 it 右侧,反之位于 it 左侧。
#include <iostream> // std::cout #include <iterator> // std::next #include <list> // std::list using namespace std; int main() { //创建并初始化一个 list 容器 std::list<int> mylist{ 1,2,3,4,5 }; std::list<int>::iterator it = mylist.begin(); //获取一个距离 it 迭代器 2 个元素的迭代器,由于 2 为正数,newit 位于 it 右侧 auto newit = next(it, 2); cout << "next(it, 2) = " << *newit << endl; //n为负数,newit 位于 it 左侧 it = mylist.end(); newit = next(it, -2); cout << "next(it, -2) = " << *newit; return 0; }
注意,和 prev() 函数一样,next() 函数自身也不会检查新迭代器指向的有效性,需要我们自己来保证。
如何将const_iterator转换为iterator类型迭代器?
对于 C++ STL 标准库中的这 4 种基础迭代器来说,C++ 编译器的隐式转换仅支持以下 2 种情况:
- iterator 类型的迭代器隐式转换为 const_iterator 类型的迭代器;
- 将 reverse_iterator 类型的迭代器隐式转换为 const_reverse_iterator 类型的迭代器。
注意,以上 2 种隐式转换是单向的,即编译器只支持从 iterator 转换为 const_iterator,从 reverse_iterator 转换为 const_reverse_iterator,但不支持逆向转换。
有些读者可能会好奇,既然隐式转换无法做到,还有其他方式可以实现从const_iterator到 iterator、从const_reverse_iterator 到reverse_iterator 的转换吗?
很多读者可能会想到使用强制类型转换(const_cast)的方式。但可以明确的是,强制类型转换并不适用于迭代器,因为 const_cast 的功能仅是去掉某个类型的 const 修饰符,但 const_iterator 和iterator 是完全不同的 2 个类,同样 const_reverse_iterator 和 reverse_iterator 也是完全不同的 2 个类,它们仅仅是类名有 const 的差别,但并不是 const T 和 T 的关系。
这里给读者推荐一种实现方式,就是使用 advance() 和 distance() 这 2 个函数,其语法格式如下:
//将 const_iterator 转换为 iterator advance(iter, distance<cont<T>::const_iterator>(iter,citer)); //将 const_reverse_iterator 转换为 reverse_iterator advance(iter, distance<cont<T>::const_reverse_iterator>(iter,citer));
其中,citer 为指向某个容器(比如 cont)任意位置的 const_iterator(或者 const_reverse_iterator)类型迭代器,而 iter 通常初始为指向 cont 容器中第一个元素的 iterator(或者 reverse_iterator)类型迭代器。通过套用此格式,最终 iter 会变成一个指向和 citer 一样的 iterator(或者 reverse_iterator)类型迭代器。
(个人:也就是现在citer是一个const迭代器,我们现在需要得到一个非const迭代器,这个非const迭代器指向的位置和citer相同,我们可以通过上面的方法得到这个非const迭代器,注意,这个方法不是把citer这个变量的类型直接由const_iterator变为iterator,而是获得另一个非const迭代器变量,这个非const迭代器指向的位置和citer相同)
注意,在使用 distance() 函数时,必须额外指明 2 个参数为 const 迭代器类型,否则会因为传入的 iter 和 citer 类型不一致导致 distance() 函数编译出错。
#include <iostream> #include <vector> #include <iterator> using namespace std; int main() { vector<int>value{ 1,2,3,4,5 }; //定义一个 const_iterator 类型的迭代器,其指向最后一个元素 vector<int>::const_iterator citer = --value.cend(); //初始化一个非 const 迭代器,另其指向 vector<int>::iterator iter = value.begin(); //将 iter 变成和 citer 同样指向的迭代器 advance(iter, distance<vector<int>::const_iterator>(iter, citer)); cout <<"*citer = " << *citer << endl; cout << "*iter = " << *iter << endl; return 0; }
可以看到,通过使用advance()和distance()函数的组合格式,最终可以得到一个和 citer 指向相同但类型为 iterator 的迭代器。
注意,此方法的实现效率仍取决于目标容器的迭代器类型,如果是随机访问迭代器,则该方法的执行效率为 O(1);反之,则执行效率为 O(n)。