迭代器循环 vs 索引循环

156

可能重复:
为什么要使用迭代器而不是数组下标?

我正在回顾我的C++知识,遇到了迭代器这个概念。我想知道它们有什么特殊之处,以及为什么要使用它们:

using namespace std;

vector<int> myIntVector;
vector<int>::iterator myIntVectorIterator;

// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(myIntVectorIterator = myIntVector.begin(); 
        myIntVectorIterator != myIntVector.end();
        myIntVectorIterator++)
{
    cout<<*myIntVectorIterator<<" ";
    //Should output 1 4 8
}

比这个更好:

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(int y=0; y<myIntVector.size(); y++)
{
    cout<<myIntVector[y]<<" ";
    //Should output 1 4 8
}

是的,我知道我不应该使用std命名空间。我只是从cprogramming网站上拿了这个例子。那么你能告诉我后者为什么更糟糕吗?有什么大的区别吗?


2
请阅读维基百科上的与索引对比 - Jesse Good
8个回答

247
迭代器的特殊之处在于它们提供了算法和容器之间的粘合剂。对于通用代码,建议使用STL算法(例如findsortremovecopy)等来执行您想要在数据结构(vectorlistmap等)上进行的计算,并向该算法提供指向容器的迭代器。
您的特定示例可以编写为for_each算法和vector容器的组合(请参见下面的选项3),但这只是迭代std::vector的四种不同方法之一:

1) 基于索引的迭代

for (std::size_t i = 0; i != v.size(); ++i) {
    // access element as v[i]

    // any code including continue, break, return
}

优点: 对于熟悉C风格代码的人来说非常熟悉,可以使用不同步长进行循环(例如i += 2)。

缺点: 只适用于顺序随机访问容器 (vector, array, deque),不能用于listforward_list或关联式容器。此外,循环控制有点冗长(初始化、检查、增量)。人们需要意识到C++中的基于0的索引。

2) 基于迭代器的迭代

for (auto it = v.begin(); it != v.end(); ++it) {
    // if the current index is needed:
    auto i = std::distance(v.begin(), it); 

    // access element as *it

    // any code including continue, break, return
}

优点: 更加通用,适用于所有容器(即使是新的无序关联容器),可以使用不同的步长(例如 std::advance(it, 2));

缺点: 需要额外的工作来获取当前元素的索引(对于链表或前向列表可能是 O(N))。同样,循环控制有点冗长(初始化、检查、增量)。

3) STL for_each 算法 + lambda 表达式

std::for_each(v.begin(), v.end(), [](T const& elem) {
     // if the current index is needed:
     auto i = &elem - &v[0];

     // cannot continue, break or return out of the loop
});

优点:与第2种方法相同,另外还可以减少循环控制(无需检查和增量),这可以大大降低错误率(错误的初始化、检查或增量、超出一个等错误)。

缺点:与显式迭代器循环相同,加上循环中流程控制的受限(不能使用continue、break或return),并且没有不同步长的选项(除非您使用重载operator++的迭代器适配器)。

4)范围for循环

for (auto& elem: v) {
     // if the current index is needed:
     auto i = &elem - &v[0];

    // any code including continue, break, return
}

优点:循环控制非常紧凑,可以直接访问当前元素。

缺点:需要额外的语句来获取索引。不能使用不同的步长。

什么时候使用?

对于您特定的迭代std::vector的示例:如果您真的需要索引(例如访问前一个或下一个元素,在循环中打印/记录索引等),或者您需要与1不同的步幅,则我会选择显式索引循环,否则我会选择范围-for循环。

对于通用容器上的通用算法,我会选择显式迭代循环,除非代码在循环内没有流程控制并且需要步幅1,这种情况下我会选择STL for_each+ lambda。


1
如果仅对单个容器进行迭代,我想即使需要访问先前/下一个元素和/或不同的步长,使用nextprevadvance函数的迭代器也足够好,并且可能会更易读。但是,同时使用多个迭代器迭代多个容器看起来不太优雅,最好使用索引。 - Predelnik
3
这是一个非常有用的答案!感谢您列出这四种不同方法的优缺点。有一个问题:索引迭代使用i != v.size()进行测试。在这里使用!=而不是<是否有原因?我的C语言本能告诉我应该使用i < v.size()。我认为两者应该都可以达到相同的效果,只是我更习惯在数字for循环中看到< - Michael Geary
1
使用范围循环,这是否需要容器按照数组的顺序存储元素?如果容器不按顺序存储项目,这仍然可以用来获取索引吗? - Devolus
1
问题是在数组索引的上下文中,因此连续排列的序列(如vectorarray)可以使用。所以不,它不能用于list甚至deque - TemplateRex
1
@MichaelGeary < 的优点是它适用于不同的增量,比如 i += 3。然而,当增量为一时,使用 != 是相对标准的做法。 - user904963
显示剩余5条评论

14

使用向量迭代器没有任何真正的优势。语法更丑陋,输入更长,阅读更困难。

使用迭代器遍历向量既不更快,也不更安全(实际上,如果在迭代过程中可能调整向量大小,则使用迭代器会带来很大的麻烦)。

在实际情况下,具有可以在以后更改容器类型的通用循环的想法也大多是无意义的。不幸的是,严格类型的语言没有严格的类型推断(C++11稍微好一些),这意味着您需要在每个步骤中说明一切的类型。如果您稍后改变主意,则仍然需要四处更改一切。此外,不同的容器具有非常不同的权衡,更改容器类型并不是经常发生的事情。

唯一应该尽可能保持通用迭代的情况是编写模板代码,但那(我为您)不是最常见的情况。

您明确的索引循环中唯一存在的问题是 size 返回无符号值(C++的设计错误),有符号和无符号之间的比较是危险和令人惊讶的,因此最好避免。如果您使用启用了警告的体面编译器,则应该有诊断。

请注意,解决方法不是使用无符号数作为索引,因为无符号值之间的算术运算也显然是不合逻辑的(它是模算术,而x-1可能比x还要大)。相反,在使用之前应将大小转换为整数。 只有当您使用16位C++实现时( 16位是使用大小无符号值的原因)时,使用无符号大小和索引才会有一些意义(在编写每个表达式时非常注意)。
作为使用无符号大小可能带来的典型错误,请考虑:
void drawPolyline(const std::vector<P2d>& points)
{
    for (int i=0; i<points.size()-1; i++)
        drawLine(points[i], points[i+1]);
}

这里存在一个bug,因为如果你传递一个空的points向量,那么points.size()-1的值将会是一个巨大的正数,使你陷入了段错误的循环中。 一个可行的解决方案是:

for (int i=1; i<points.size(); i++)
    drawLine(points[i - 1], points[i]);

我个人更喜欢使用 int(v.size()) 去除 unsinged,但不强求。PS:如果你不想思考其影响,只是想让专家告诉你,那么请注意,很多世界公认的C++专家都认为 unsigned values are a bad idea except for bit manipulations。在迭代到倒数第二个元素的情况下使用迭代器的丑陋性留给读者作为练习。

3
请问您能详细解释一下,为什么 size() 作为无符号整数类型是一个设计缺陷?我看不出来在使用 for(size_t i; ...) 的情况下,为什么还会有人更喜欢使用 for(int i = 0; ...)。我曾经在 64 位系统上使用 32 位索引遇到过问题。 - Angew is no longer proud of SO
9
虚拟-1:丑陋,输入更长,阅读更困难 -> a) 这是一种观点,b) for(auto x : container) - Sebastian Mach
2
@6502:关于size_t的无符号性:不,这只是意味着我还没有听说过。而且对于不同的搜索,谷歌相对沉默,将我(像你一样)指向Alf的答案之一,这很有道理,听起来很可信,但本身并没有得到支持的引用。我不确定为什么“从未听说过”对你来说等同于“我不同意”; 这是很多猜测。而且,纯粹的推理和深入的C ++知识是不够的; C++标准也不包含这样的轶事,逻辑也不包括。 - Sebastian Mach
3
我基本上同意无符号类型不太理想,但由于它们已经内置在标准库中,我也看不到避免使用它们的好方法。对我来说,“其值永远不会超过INT_MAX的无符号类型”与另一方提出的“其值永远不会小于0的带符号类型”本质上并没有更可靠的区别。如果容器的大小大于INT_MAX,那么显然就不能将其转换为 int,代码将失败。long long会更安全(尤其是因为它已经被纳入标准)。我永远不会创建一个拥有2^63个元素的向量,但我可能会创建一个拥有2^31个元素的向量。 - Steve Jessop
2
@6502:对我来说,这只是意味着处理它的一种方式(使用无符号类型并冒险在0处溢出)有一个更明显的问题,而另一种方式(将大小转换为“int”)有一个更微妙的问题。实际上,我更喜欢在常见情况下发生错误,而不是逃避测试的错误。将大小转换为int的问题并不是因为我认为数字2^31-1“不够用”。问题在于,如果我正在编写一些操作向量的代码,那么我希望接受调用者可以创建的所有类型的值,我不想在我的API中引入额外的混淆限制。 - Steve Jessop
显示剩余28条评论

9

迭代器使您的代码更加通用。
每个标准库容器都提供了一个迭代器,因此如果将来更改容器类,循环不会受到影响。


但是并非所有的容器类都有 size 函数吧?如果我改变原始容器,后者仍然可以工作,因为 size 方法并没有改变。 - CodingMadeEasy
1
@CodingMadeEasy:但是内置数组没有大小函数。 - Sebastian Mach
4
但并非所有容器都支持随机访问。也就是说,std::list 没有(也不能够以任何高效的方式)提供 operator[] - Angew is no longer proud of SO
@phresnel 我不知道你可以遍历数组。我认为它们只适用于容器类。 - CodingMadeEasy
@phresnel提到了内置数组的好处,但编写一个模板size函数来获取其大小也很容易。 - juanchopanza
显示剩余4条评论

7
迭代器是优先于operator[]的选择。C++11 提供了 std::begin()和std::end()函数。
如果您的代码仅使用std::vector,我不能说两个代码有多大区别,但是,operator []可能无法按照您的意图操作。例如,如果使用map,则operator []将在未找到元素时插入一个元素。
此外,通过使用迭代器,您的代码变得更易于在容器之间移植。您可以自由地将容器从std::vector切换到std::list或其他容器,而不需要进行太多更改,如果使用operator[]这样的规则不适用。

谢谢你的回答。一旦你提到了std::map,我就更明白了。由于映射不必具有数字键,因此如果我要更改容器类,则必须修改循环以适应映射容器。使用迭代器,无论我将其更改为哪个容器,都适合循环。感谢您的答案 :) - CodingMadeEasy

4

这取决于您的需求。

当您需要索引向量中的特定元素时,应使用operator[]以直接访问元素。使用它而不是迭代器没有任何问题。然而,您必须自行决定哪个(operator[]或迭代器)最适合您的需求。

使用迭代器可以使您在不更改代码太多的情况下切换到其他容器类型。换句话说,使用迭代器可以使您的代码更通用,并且不依赖于特定类型的容器。


那么你的意思是我应该使用[]运算符而不是迭代器? - CodingMadeEasy
1
@CodingMadeEasy 这总是取决于你想要什么和需要什么。 - Mark Garcia
好的,我会继续努力,并看看哪种方法适用于每种情况。 - CodingMadeEasy
但是 operator[] 和迭代器一样直接。它们都只是提供元素的引用。你是不是指当你需要手动索引容器时,例如 cont[x] < cont[x-1] - Sebastian Mach
@phresnel 是的,接受你的观点。 - Mark Garcia
迭代器的另一个优点是,即使您永远不会更改容器,您在那里和其他地方编写相同的代码模式,即使您使用不同的容器。模式减少了错误的数量。此外,迭代器通常立即记录操作将在容器中的每个元素上运行。std::for_each更好地记录了这一点,同时消除了breakcontinuereturn的可能性。 - user904963

1
通过使用迭代器编写客户端代码,您可以完全抽象掉容器。
考虑以下代码:
class ExpressionParser // some generic arbitrary expression parser
{
public:
    template<typename It>
    void parse(It begin, const It end)
    {
        using namespace std;
        using namespace std::placeholders;
        for_each(begin, end, 
            bind(&ExpressionParser::process_next, this, _1);
    }
    // process next char in a stream (defined elsewhere)
    void process_next(char c);
};

客户端代码:

ExpressionParser p;

std::string expression("SUM(A) FOR A in [1, 2, 3, 4]");
p.parse(expression.begin(), expression.end());

std::istringstream file("expression.txt");
p.parse(std::istringstream<char>(file), std::istringstream<char>());

char expr[] = "[12a^2 + 13a - 5] with a=108";
p.parse(std::begin(expr), std::end(expr));

编辑:考虑使用以下代码示例实现:

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

copy(myIntVector.begin(), myIntVector.end(), 
    std::ostream_iterator<int>(cout, " "));

不错的例子,但是 istringstream 的客户端调用可能不会达到你想要的效果,因为 operator>>(istream&, char&) 会丢弃所有空格(虽然通常可以关闭此功能,但我在 cplusplus.com 上的粗略浏览表明,在这种情况下无法关闭,因为会创建一个特殊的 sentry 对象来保持它... 唉)。例如,如果你的 expr 在文件 expression.txt 中,第二次调用 p.parse() 将(也许是不可避免的)将 witha 作为单个标记从中读取。 - j_random_hacker

0
迭代器的好处在于,如果以后您想将向量切换到另一个STD容器,则for循环仍将起作用。

-1

但是在你刚才给我展示的那篇文章中,它说索引速度要快得多:/ - CodingMadeEasy
我的错,我是从那个基准测试的结果下面读取的。我在其他地方读到过,使用迭代器比索引更快。我打算亲自试一试。 - Nicolas Brown
好的,谢谢。请告诉我你得到的结果。 - CodingMadeEasy
3
at() 之所以不同是因为它进行了范围检查并有条件地抛出异常。在使用迭代器或索引时,并不存在一致的性能优势,无论哪种方式你测量的结果都更多或少取决于编译器/优化器的随机因素,并且不一定稳定跨越构建、优化器标志、目标架构等。 - Tony Delroy
1
我同意@TonyD的观点。在我发布的链接中,一个人说索引更快,而另一个人则说使用迭代器更快。我尝试了发布的代码;使用迭代器的循环花费了40秒,而使用索引的只花费了4秒。虽然速度上只有轻微的差别。 - Nicolas Brown
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接