138

可能重复:
为什么使用迭代器而不是数组索引?

我正在回顾我对 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 网站上拿了这个例子。那么你能告诉我为什么后者更糟吗?有什么大的区别?

4

8 回答 8

220

迭代器的特殊之处在于它们提供了算法和容器之间的粘合剂。对于通用代码,建议使用 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),不适用于list,forward_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));

缺点:需要额外的工作来获取当前元素的索引(对于 list 或 forward_list 可能是 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) range-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 的步幅,那么我会明确地选择索引循环,否则我会选择 range-for 循环。

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

于 2013-01-17T08:04:27.000 回答
10

使用向量迭代器并没有提供任何真正的优势。语法更丑陋,打字时间更长,更难阅读。

使用迭代器对向量进行迭代不是更快也不是更安全(实际上,如果在迭代期间使用迭代器调整向量的大小会给你带来很大的麻烦)。

当您稍后更改容器类型时,拥有一个通用循环的想法在实际情况下也大多是无稽之谈。不幸的是,没有严格类型推断的严格类型语言的阴暗面(不过现在使用 C++11 更好一些)是您需要在每一步中说明所有内容的类型。如果您稍后改变主意,您仍然需要四处走动并改变一切。此外,不同的容器有非常不同的权衡,改变容器类型并不是经常发生的事情。

如果可能的话,应该保持迭代的唯一情况是在编写模板代码时,但这(我希望你)不是最常见的情况。

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

请注意,解决方案不是使用无符号作为索引,因为无符号值之间的算术显然也是不合逻辑的(它是模算术,并且x-1可能大于x)。相反,您应该在使用它之前将大小转换为整数。仅当您正在处理 16 位 C++ 实现( 16 位是在 size 中具有无符号值的原因)时,使用无符号大小和索引(非常注意您编写的每个表达式)可能是有意义的。

作为 unsigned size 可能引入的典型错误,请考虑:

void drawPolyline(const std::vector<P2d>& points)
{
    for (int i=0; i<points.size()-1; i++)
        drawLine(points[i], points[i+1]);
}

此处存在错误,因为如果您传递一个空points向量,则该值points.size()-1将是一个巨大的正数,从而使您循环进入段错误。一个可行的解决方案可能是

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

但我个人更喜欢总是删除unsinged-ness with int(v.size())

PS:如果您真的不想自己思考其中的含义,而只是希望专家告诉您,那么请考虑很多世界公认的 C++ 专家同意并表达了关于无符号值是一个坏主意的观点,除了位操作

在迭代到倒数第二个的情况下发现使用迭代器的丑陋留给读者作为练习。

于 2013-01-17T07:35:39.943 回答
9

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

于 2013-01-17T07:18:47.637 回答
7

迭代器是首选operator[]。C++11 提供std::begin(),std::end()函数。

由于您的代码使用 just std::vector,我不能说这两个代码有很大差异,但是,operator []可能无法按您的意愿运行。例如,如果您使用地图,operator[]如果未找到将插入一个元素。

此外,通过使用iterator您的代码在容器之间变得更加可移植。如果您使用迭代器,您可以自由地将容器从std::vectortostd::list或其他容器切换而不会发生太大变化,这样的规则不适用于operator[].

于 2013-01-17T07:31:32.653 回答
4

它总是取决于你需要什么。

operator[]当您需要直接访问向量中的元素时(当您需要索引向量中的特定元素时),您应该使用。在迭代器上使用它并没有错。但是,您必须自己决定哪个(operator[]或迭代器)最适合您的需求。

使用迭代器可以让您切换到其他容器类型,而无需对代码进行太多更改。换句话说,使用迭代器将使您的代码更加通用,并且不依赖于特定类型的容器。

于 2013-01-17T07:17:50.543 回答
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, " "));
于 2013-01-17T09:36:01.197 回答
0

迭代器的好处是,稍后如果您想将向量切换到另一个 STD 容器。然后 forloop 仍然可以工作。

于 2013-01-17T07:18:41.387 回答
-1

这是速度的问题。使用迭代器可以更快地访问元素。这里回答了一个类似的问题:

用 vector::iterator 或 at() 迭代 STL 向量有什么更快的方法?

编辑:访问速度因每个 cpu 和编译器而异

于 2013-01-17T07:18:26.673 回答