接触 STL 的容器有一段时间了,现在来聊一聊迭代器的问题。
我第一个接触的容器是 vector,当时遍历容器的写法就是简单的迭代循环。

代码是这样写的:

std::vector<int> v;
for (int i = 0; i < v.size(); i++)
{
//TODO
}

后来看到别人的写法是这样的:

std::vector<int> v;
for (std::vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
//TODO
}

觉得既然 STL 既然内置了 iterator,应该有它自己的优势,虽然这两种写法的时间复杂度都是 O(n)。
那么它们的具体区别是什么呢?在我闲逛 stackoverflow 时看到了相关的讨论,这两种方式在遍历 vector 时并无区别,但是第一种方法却不一定适用于其它容器,例如 map,而且后者也易于理解,从维护性,复用性等角度来看都是后者更具备优势。
在 STL 中还有反向迭代器,可以反序遍历容器,像这样:

std::vector<int> v;
for (std::vector<int>::reverse_iterator it = v.rbegin(); it != v.rend(); it++)
{
//TODO
}

这里反向迭代器将 ++-- 的含义反了过来,++ 访问前一个元素,而 -- 访问后一个元素。
但是为什么不像下面这样遍历呢?

std::vector<int> v;
for (std::vector<int>::iterator rit = v.end(); rit != v.begin(); rit++)
{
//TODO
}

有这种想法的人(比如我)运行一下就知道了,结果并不是想象中的那样,因为 v.end() 返回指向当前对象中 末尾之后( Past-the-end) 的元素的迭代器。一图胜千言:

begin end
| |
v v
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
rbegin rend

为什么不将 end() 指向最后一个元素呢?这个问题有点类似为什么数组标号是从 0 开始的,之所以指向末尾之后的元素原因有很多,我举几个例子:

size() = end() - begin()
size() = end() - begin() + 1

这样不仅不优雅,假设容器是空的,begin()end() 的顺序也会令人费解。

sort(begin(), end())
sort(begin(), end()+1)

所有参数与迭代器相关的函数都要补上 +1,代码变得丑陋。
再比如,如果在容器内未找到元素之后返回的结果要改为 end()+1 而不是原来的 end()
还想了解更多原因的话,请看 Dijkstra 爷爷的原文:Why numbering should start at zero
如果你非常希望通过 begin()end() 进行反向迭代也未尝不可,不过我不会告诉你写法,放弃这种方法吧。
最后,强制将正向迭代器反向不一定能得到想要的结果:

int a[] = {6, 7, 8, 7, 9};
std::vector<int> v(a, a + sizeof(a) / sizeof(int));
std::vector<int>::iterator it;
std::vector<int>::reverse_iterator rit;
it = find(v.end(), v.begin(), 7);
std::cout << *(it-1) << " " << *it << " " << *(it+1) << std::endl;
it = find(v.begin(), v.end(), 7);
std::cout << *(it-1) << " " << *it << " " << *(it+1) << std::endl;
rit = find(v.rbegin(), v.rend(), 7);
std::cout << *(rit+1) << " " << *rit << " " << *(rit-1) << std::endl;

第一个输出的结果是错的,第二个与第三个的结果也不一样,所以请不要乱使用迭代器。
参考链接:

文档信息

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/09/STL-iterator.html