迭代器提供了对一系列事物的逐个元素的访问。这些东西可以是数字、字符或几乎任何类型的对象。标准容器,比如vector,提供了对容器内容的迭代器访问,其他标准迭代器允许您访问输入流和输出流。考虑范围的一种方式是一对迭代器。标准算法需要迭代器来对事物序列进行操作。
到目前为止,您对迭代器的看法和使用还有些局限。当然,你用过它们,但是你真的了解它们吗?这种探索有助于理解迭代器到底发生了什么。
到目前为止,您已经看到迭代器有多种形式,特别是读和写。您看到了可以从一对迭代器中构造一个vector,比如std::istream_iterator。std::ranges::copy函数需要一个写迭代器作为复制目的地。
然而,一直以来,我都通过引用“读”和“写”迭代器来简化情况。事实上,C++ 有六种不同类别的迭代器:输入、输出、正向、双向、随机访问和连续。每个类别都有额外的特征来进一步描述特定迭代器能做什么或不能做什么。输入和输出迭代器的功能最少,contiguous 的功能最多。您可以在任何需要迭代器功能较少的地方用功能较多的迭代器来代替。图 46-1 说明了迭代器的可替换性。然而,不要被这个数字误导了。它不显示类继承。使一个对象成为迭代器的是它的行为。例如,如果它满足了一个输入迭代器的所有要求,它就是一个输入迭代器,不管它是什么类型。
图 46-1。
迭代器的替换树
所有迭代器都可以自由复制和赋值。复制或赋值的结果是一个新的迭代器,它引用与原始迭代器相同的项。其他特征取决于迭代器类别,如下面几节所述。
不出所料,输入迭代器只支持输入。每次迭代只能从迭代器读取一次(使用一元*操作符)。您不能修改迭代器引用的项。++操作者前进到下一个输入项目。你可以比较迭代器的相等和不相等,但是唯一有意义的比较是比较一个迭代器和一个末端迭代器。一般来说,您不能比较两个输入迭代器来查看它们是否引用同一个项。
大概就是这样。输入迭代器非常有限,但也非常有用。许多标准算法用输入迭代器来表示它们的输入。istream_iterator类型是输入迭代器的一个例子。还可以将任何容器的迭代器视为输入迭代器,例如,vector 的begin()成员函数返回的迭代器。
输出迭代器只支持输出。您可以通过将*操作符应用到赋值左边的迭代器来给迭代器项赋值,但是您不能从迭代器中读取。每次迭代只能修改一次迭代器值。++运算符前进到下一个输出项目。
您不能比较输出迭代器是否相等。
尽管输出迭代器有局限性,但它们也被标准算法广泛使用。每个将数据复制到输出范围的算法都需要一个输出迭代器来指定范围的开始。
处理输出迭代器时需要注意的一点是,必须确保迭代器实际写入的地方有足够的空间来存储整个输出。任何错误都会导致未定义的行为。一些实现提供了可以检查这种错误的调试迭代器,当这些工具可用时,您当然应该利用它们。然而,不要仅仅依赖于调试库。在使用输出(和其他)迭代器时,仔细的代码设计、仔细的代码实现和仔细的代码审查对于确保安全是绝对必要的。
ostream_iterator类型是输出迭代器的一个例子。您也可以将许多容器的迭代器视为输出迭代器,例如,vector 的begin()成员函数返回的迭代器。
前向迭代器具有输入迭代器和输出迭代器的所有功能,还有更多的功能。您可以自由地读取和写入一个迭代器项(仍然使用一元操作符*),并且您可以根据需要经常这样做。++操作符前进到下一项,==和!=操作符可以比较迭代器,看它们是指同一项还是指结束位置。
一些算法需要前向迭代器,而不是输入迭代器。在之前的探索中,我忽略了这个细节,因为它很少影响到你。例如,二分搜索法算法需要前向迭代器来指定输入范围,因为它们可能需要不止一次地引用一个特定的项。这意味着你不能直接使用一个istream_iterator作为一个参数,比如说,lower_bound,但是你不太可能在一个真实的程序中尝试这样做。所有容器的迭代器都满足前向迭代器的要求,所以实际上,这个限制影响很小。
双向迭代器具有正向迭代器的所有功能,但是它还支持--操作符,该操作符将迭代器向后移动一个位置,到达前一项。与任何迭代器一样,您有责任确保迭代器不会超出范围的结尾或开头。
reverse和reverse_copy算法(以及其他一些算法)需要双向迭代器。大多数容器的迭代器至少满足双向迭代器的要求,所以您很少需要担心这个限制。
一个随机访问迭代器拥有所有其他迭代器的所有功能,另外你可以通过增加或减少一个整数来移动迭代器任意的数量。
你可以减去两个迭代器(假设它们引用相同的对象序列)来获得它们之间的距离。回想一下 Exploration 45 中的distance函数返回两个迭代器之间的距离。如果向函数传递正向或双向迭代器,它会一次向前推进起始迭代器一步,直到到达结束迭代器。只有这样它才会知道距离。如果你传递随机访问迭代器,它只是减去两个迭代器,并立即返回它们之间的距离。
可以比较随机访问迭代器是否相等。如果两个迭代器引用相同的对象序列,也可以使用任何关系运算符。对于随机访问迭代器,a < b意味着a引用序列中比b更早的一个项目。
像sort这样的算法需要随机访问迭代器。vector类型提供了随机访问迭代器,但并不是所有的容器都提供。例如,list容器实现了一个双向链表,所以它只有双向迭代器。因为不能使用sort算法,list容器有自己的sort成员函数。在探索 56 中了解更多关于list的信息。
连续迭代器具有所有其他迭代器的所有功能,而且它适用于存储在相邻内存位置的元素。向量或数组有连续的迭代器,但其他容器没有。
现在你知道了向量提供了连续的迭代器,并且你可以使用关系操作符来比较连续的(和随机访问的)迭代器,重新看看清单 10-4。你能想出一个更简单的方法来写这个程序吗?(提示:考虑一个循环条件start < end。)参见我在清单 46-1 中的重写。
import <algorithm>;
import <iostream>;
import <iterator>;
import <vector>;
int main()
{
std::vector<int> data{
std::istream_iterator<int>(std::cin),
std::istream_iterator<int>()
};
for (auto start{data.begin()}, end{data.end()}; start < end; ++start)
{
--end; // now end points to a real position, possibly start
std::iter_swap(start, end); // swap contents of two iterators
}
std::copy(data.begin(), data.end(),
std::ostream_iterator<int>(std::cout, "\n"));
}
Listing 46-1.Comparing Iterators by Using the < Operator
这次我使用了std::copy(),它使用了一对迭代器而不是一个范围,只是为了展示迭代器对是如何进行输入和输出的。
因此,输入、正向、双向、随机访问和连续迭代器都可以称为“读”迭代器,输出、正向、双向、随机访问和连续迭代器都可以称为“写”迭代器。一个算法,比如copy,可能只需要输入和输出迭代器。也就是说,输入范围需要两个输入迭代器。您可以使用任何满足输入迭代器要求的迭代器:输入迭代器、正向迭代器、双向迭代器、随机访问迭代器或连续迭代器。对于输出范围的开始,使用任何满足输出迭代器要求的迭代器:输出、前向、双向、随机访问或连续。
迭代器最常见的来源是所有容器(如map和vector)提供的begin()和end()成员函数。begin()成员函数返回一个引用容器第一个元素的迭代器,end()返回一个引用容器最后一个元素位置的迭代器。
begin()空集装箱返回什么?
**_____________________________________________________________
如果容器为空,begin()返回与end()相同的值,即表示“超过结尾”的特殊值,不能解引用。测试容器是否空的一种方法是测试begin() == end()。(更好的是,尤其是当你正在编写一个真正的程序,而不是试图说明迭代器的本质时,调用每个容器都提供的empty()成员函数。)
每个容器以不同的方式实现其迭代器。对你来说最重要的是迭代器满足一个标准类别的要求。
迭代器的确切类别取决于容器。返回连续的迭代器。一个map返回双向迭代器。任何库参考都会告诉你每个容器支持哪种迭代器。
许多算法和容器成员函数也返回迭代器。例如,几乎每个执行搜索的函数都返回一个指向所需项的迭代器。如果函数找不到该项,它将返回结束迭代器。返回值的类型通常与输入范围中迭代器的类型相同。将元素复制到输出范围的算法返回结果迭代器。
一旦有了迭代器,就可以用*去引用它,以获得它所引用的值(除了输出迭代器和结束迭代器,输出迭代器的去引用只是为了分配一个新值,而结束迭代器永远不能去引用)。如果迭代器引用一个对象,而你想访问该对象的一个成员,你可以使用简写的- >符号。
std::vector<std::string> lines(2, "hello");
std::string first{*lines.begin()}; // dereference the first item
std::size_t size{lines.begin()->size()}; // dereference and call a member function
您可以通过调用next或advance函数(在<iterator>中声明)将迭代器推进到新的位置。advance函数修改作为第一个参数传递的迭代器。next函数接受迭代器的值,并返回一个新的迭代器值。第二个参数是推进迭代器的整数距离。第二个参数对于next是可选的;默认为 1。如果迭代器是随机访问的,那么这个函数会把这个距离加到迭代器上。任何其他类型的迭代器都必须多次应用它的 increment ( ++)操作符来前进所需的距离,例如:
std::vector<int> data{ 1, 2, 3, 4, 5 };
auto iter{ data.begin() };
std::cout << "4 == " << *std::next(iter, 3) << '\n';
对于一个 vector 来说,std::next()就像加法一样,但是对于其他容器,比如std::map,它多次应用 increment 运算符,以到达期望的目的地。如果你有一个反向迭代器,你可以传递一个负的距离或者调用std::prev()。如果迭代器是双向的,第二个参数可以是负的,这样就可以返回。您可以推进输入迭代器,但不能推进输出迭代器。重用 Exploration 44 中的sequence仿函数,读取清单 46-2 中的程序。
import <algorithm>;
import <iostream>;
import <iterator>;
import <vector>;
import data; // see Listing 45-2.
import sequence; // see Listing 44-6.
int main()
{
intvector data(10);
// fill with even numbers
std::generate(data.begin(), data.end(), sequence{0, 2});
auto iter{data.begin()};
std::advance(iter, 4);
std::cout << *iter << ", ";
iter = std::prev(iter, 2);
std::cout << *iter << '\n';
}
Listing 46-2.Advancing an Iterator
这个程序打印什么?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
data向量用偶数填充,从 0 开始。迭代器iter最初指的是向量的第一个元素,即 0。迭代器前进四个位置,值为 8,然后后退两个位置,值为 4。所以输出是
8, 4
声明变量来存储迭代器是笨拙的。类型名又长又麻烦。所以我经常用auto来定义一个变量。有时你需要明确地命名一个迭代器类型。这通常是通过成员类型名来完成的。清单 46-3 展示了迭代器成员类型的一些用法。
import <iostream>;
import <string>;
import <vector>;
int main()
{
std::vector<std::string> lines{2, "hello"};
std::vector<std::string>::iterator iter{lines.begin()};
*iter = "good-bye"; // dereference and modify the first item
std::size_t size{iter->size()}; // dereference and call a member function
std::vector<std::string>::const_iterator citer{lines.cbegin()};
std::cout << *citer << '\n';
std::cout << size << '\n';
}
Listing 46-3.Demonstrating Iterator Member Types
成员类型const_iterator产生容器的const元素。iterator类型产生可修改的成员。下一节将更仔细地研究const_iterator。
混淆的一个小来源是const_iterator和const iterator之间的区别。输出迭代器(以及任何满足输出迭代器要求的迭代器,即正向迭代器、双向迭代器、随机访问迭代器和连续迭代器)允许您修改它引用的项。对于一些前向迭代器(双向的、随机访问的和连续的),您希望将该范围内的数据视为只读。即使迭代器本身满足前向迭代器的要求,您的直接需求可能只是输入迭代器。
您可能认为声明迭代器const会有所帮助。毕竟,这就是你要求编译器帮助你的方式,通过防止意外修改变量:用const说明符声明变量。你怎么想呢?行得通吗?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
如果你不确定,试一试。阅读清单 46-4 和预测其产量。使用与 Exploration 45 相同的data模块。
import <iostream>;
import <iterator>;
import data;
int main()
{
intvector data{};
read_data(data);
const intvector::iterator iter{data.begin()};
std::advance(iter, data.size() / 2); // move to middle of vector
if (not data.empty())
std::cout << "middle item = " << *iter << '\n';
}
Listing 46-4.Printing the Middle Item of a Series of Integers
你能看出为什么编译器拒绝编译程序吗?可能你看不到确切的原因,埋在编译器的错误输出里。(下一节将更详细地讨论这个问题。)错误在于变量iter是const。你不能修改迭代器,所以你不能把它推进到向量的中间。
你不必将迭代器本身声明为const,你必须告诉编译器你希望迭代器引用const数据。如果向量本身是const,那么begin()函数将返回这样一个迭代器。您可以自由地修改迭代器的位置,但是不能修改迭代器引用的值。这个函数返回的迭代器的名字是const_iterator(带下划线)。
换句话说,每个容器实际上都有两个不同的begin()函数。一个是const成员函数,返回const_iterator。另一个不是const成员函数;它返回一个普通的iterator。与任何const或非const成员函数一样,编译器根据容器本身是否为const来选择其中之一。如果容器不是const,则得到begin()的非const版本,返回一个普通的iterator,可以通过迭代器修改容器内容。如果容器是const,您将得到begin()的const版本,它返回一个const_iterator,这将阻止您修改容器的内容。您还可以通过调用cbegin()来强制发布,它总是返回const_iterator,即使对于非const对象也是如此。
重写清单 46-4 以使用一个const_iterator。你的程序应该看起来类似于清单 46-5 。
import <iostream>;
import <iterator>;
import data;
int main()
{
intvector data{};
read_data(data);
intvector::const_iterator iter{data.begin()};
std::advance(iter, data.size() / 2); // move to middle of vector
if (not data.empty())
std::cout << "middle item = " << *iter << '\n';
}
Listing 46-5.Really Printing the Middle Item of a Series of Integers
向自己证明,有了const_iterator就不能修改数据。**对你的程序做进一步修改,取中间值。**现在你的程序应该如清单 46-6 所示。
import <iostream>;
import <iterator>;
import data;
int main()
{
intvector data{};
read_data(data);
intvector::const_iterator iter{data.begin()};
std::advance(iter, data.size() / 2); // move to middle of vector
if (not data.empty())
*iter = -*iter;
write_data(data);
}
Listing 46-6.Negating the Middle Value in a Series of Integers
如果你把const_iterator改成iterator,程序就工作了。做吧。
当你编译清单 46-4 时,编译器发出一条错误消息,或者 C++ 标准编写者称之为诊断。例如,我每天使用的编译器 g++ 会打印以下内容:
In file included from /usr/include/c++/10/bits/stl_algobase.h:66,
from /usr/include/c++/10/bits/char_traits.h:39,
from /usr/include/c++/10/ios:40,
from /usr/include/c++/10/ostream:38,
from /usr/include/c++/10/iostream:39,
from list4604.cc:3:
/usr/include/c++/10/bits/stl_iterator_base_funcs.h: In instantiation of 'constexpr void std::__advance(_RandomAccessIterator&, _Distance, std::random_access_iterator_tag) [with _RandomAccessIterator = const __gnu_cxx::__normal_iterator<int*, std::vector<int> >; _Distance = long int]':
/usr/include/c++/10/bits/stl_iterator_base_funcs.h:206:21: required from 'constexpr void std::advance(_InputIterator&, _Distance) [with _InputIterator = const __gnu_cxx::__normal_iterator<int*, std::vector<int> >; _Distance = long unsigned int]'
list4604.cc:11:37: required from here
/usr/include/c++/10/bits/stl_iterator_base_funcs.h:181:2: error: passing 'const __gnu_cxx::__normal_iterator<int*, std::vector<int> >' as 'this' argument discards qualifiers [-fpermissive]
181 | ++__i;
| ^~~~~
In file included from /usr/include/c++/10/bits/stl_algobase.h:67,
from /usr/include/c++/10/bits/char_traits.h:39,
from /usr/include/c++/10/ios:40,
from /usr/include/c++/10/ostream:38,
from /usr/include/c++/10/iostream:39,
from list4604.cc:3:
/usr/include/c++/10/bits/stl_iterator.h:975:7: note: in call to 'constexpr __gnu_cxx::__normal_iterator<_Iterator, _Container>& __gnu_cxx::__normal_iterator<_Iterator, _Container>::operator++() [with _Iterator = int*; _Container = std::vector<int>]'
975 | operator++() _GLIBCXX_NOEXCEPT
| ^~~~~~~~
In file included from /usr/include/c++/10/bits/stl_algobase.h:66,
from /usr/include/c++/10/bits/char_traits.h:39,
from /usr/include/c++/10/ios:40,
from /usr/include/c++/10/ostream:38,
from /usr/include/c++/10/iostream:39,
from list4604.cc:3:
/usr/include/c++/10/bits/stl_iterator_base_funcs.h:183:2: error: passing 'const __gnu_cxx::__normal_iterator<int*, std::vector<int> >' as 'this' argument discards qualifiers [-fpermissive]
那么这些官样文章是什么意思呢?虽然一个 C++ 高手也能搞清楚,但对你的帮助可能不大。隐藏在中间的是行号和源文件,它们标识了错误的来源。这就是你要开始寻找的地方。编译器直到开始处理各种模块时才发现错误。文件名取决于标准库的实现,所以您不能总是从这些文件名中判断出实际的错误是什么。
在这种情况下,当std::advance函数试图将++操作符应用到迭代器时,就会出现错误。这时编译器检测到它有一个const迭代器,但是它没有任何与const迭代器一起工作的函数。关于“丢弃限定词”的消息意味着编译器能够继续的唯一方法是去掉iter对象上的const限定词。
不要放弃理解 C++ 编译器错误信息的希望。到本书结束时,你将获得更多的知识,这将帮助你理解编译器和库是如何工作的,这种理解将帮助你理解这些错误信息。
我的建议是,处理大量令人困惑的错误信息时,首先要找到你的源文件。这应该会告诉您引起问题的行号。检查源文件。你可能会发现一个明显的错误。如果没有,请检查错误消息文本。忽略“从此处实例化”和类似的消息。尝试找到真正的错误消息,它通常以error:开始,而不是以warning:或note:开始。
<iterator>头定义了许多有用的、专门的迭代器,比如back_inserter,你已经见过几次了。严格来说,back_inserter是一个返回迭代器的函数,但是你很少需要知道确切的迭代器类型。
除了back_inserter,还可以使用front_inserter,它也是以容器为参数,返回一个输出迭代器。每次给解引用迭代器赋值时,它都会调用容器的push_front成员函数,将值插入容器的开头。
inserter函数接受一个容器和一个迭代器作为参数。它返回一个调用容器的insert函数的输出迭代器。insert成员函数需要一个iterator参数,指定插入值的位置。inserter迭代器最初传递第二个参数作为插入位置。在每次插入之后,它更新它的内部迭代器,所以后续的插入会进入后续的位置。换句话说,inserter只是做正确的事情。
其他专门的迭代器包括istream_iterator和ostream_iterator,您也已经看到了。一个istream_iterator是一个输入迭代器,当你解引用迭代器时,它从流中提取值。在没有参数的情况下,istream_iterator构造器创建一个流尾迭代器。当输入操作失败时,迭代器相当于流尾迭代器。
一个ostream_iterator是一个输出迭代器。构造器将输出流和可选字符串作为参数。赋值给被解引用的迭代器会将一个值写入输出流,可选地后跟字符串(来自构造器)。
另一个专门的迭代器是reverse_iterator类。它采用了一个现有的迭代器(称为基础迭代器),这个迭代器必须是双向的(或者是随机访问的或者是连续的)。当反向迭代器前进时(++),基迭代器后退(--)。支持双向迭代器的容器有rbegin()和rend()成员函数,它们返回反向迭代器。rbegin()函数返回一个反向迭代器,指向容器的最后一个元素,rend()返回一个特殊的反向迭代器值,表示容器开头之前的一个位置。因此,您将范围[ rbegin(),rend()]视为普通的迭代器范围,以逆序表示容器的值。
C++ 不允许迭代器指向开头之前的一个位置,所以反向迭代器的实现有点古怪。通常,实现细节并不重要,但是reverse_iterator在其返回基本迭代器的base()成员函数中公开了这个特殊的细节。
我可以告诉你基本迭代器实际上是什么,但那会让你失去乐趣。写一个程序来揭示 reverse_iterator 的基本迭代器的本质。(提示:用整数序列填充一个向量。使用反向迭代器获得中间值。与迭代器的base()迭代器的值进行比较。)
如果一个 reverse_iterator 指向一个容器的位置 x,那么它的 base() 迭代器指向什么?
如果您没有回答 x + 1,请尝试运行清单 46-7 中的程序。
import <algorithm>;
import <iostream>;
import data;
import sequence;
int main()
{
intvector data(10);
std::generate(data.begin(), data.end(), sequence(1));
write_data(data); // prints { 1 2 3 4 5 6 7 8 9 10 }
intvector::iterator iter{data.begin()};
iter = iter + 4; // iter is contiguous
std::cout << *iter << '\n'; // prints 5
intvector::reverse_iterator rev{data.rbegin()};
std::cout << *rev << '\n'; // prints 10
rev = rev + 4; // rev is also contiguous
std::cout << *rev << '\n'; // prints 6
std::cout << *rev.base() << '\n'; // prints 7
std::cout << *data.rend().base() << '\n'; // prints 1
}
Listing 46-7.Revealing the Implementation of reverse_iterator
现在你明白了吗?基本迭代器总是指向反向迭代器位置之后的一个位置*。这就是允许rend()指向“开始之前”的位置的诀窍,尽管这是不允许的。在幕后,rend()迭代器实际上有一个指向容器中第一项的基迭代器,reverse_iterator对*操作符的实现执行了获取基迭代器,后退一个位置,然后解引用基迭代器的魔术。*
如您所见,迭代器比最初看起来要复杂一些。然而,一旦你理解了它们是如何工作的,你会发现它们实际上非常简单,功能强大,并且易于使用。迭代器是范围库的基础。您可以将一个范围看作一对迭代器,但它们比这稍微复杂一些,下一篇文章将对此进行解释。**
