迭代循环与索引循环

Iterator Loop vs index loop

本文关键字:循环 索引 迭代      更新时间:2023-10-16

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

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

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

缺点:仅适用于顺序随机访问容器(vectorarraydeque(,不适用于listforward_list或关联容器。循环控制也有点冗长(init、check、increment(。人们需要注意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)(;

缺点:需要额外的工作来获得当前元素的索引(可以是列表或forward_list的O(N((。同样,循环控制有点冗长(init、check、increment(。

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 (auto& elem: v) {
     // if the current index is needed:
     auto i = &elem - &v[0];
    // any code including continue, break, return
}

优点:非常紧凑的回路控制,可直接访问当前元件。

缺点:获取索引的额外语句。不能使用不同的步幅。

使用什么

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

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

使用向量迭代器不会提供任何真正的优势。语法更难看,打字时间更长,阅读起来更难。

使用迭代器对向量进行迭代既不快也不安全(实际上,如果在迭代过程中使用迭代程序可能调整向量的大小,会给您带来很大的麻烦(。

当您稍后更改容器类型时,使用一个通用循环的想法在实际情况下也是无稽之谈。不幸的是,一种没有严格类型推理的严格类型语言的黑暗面(不过,现在使用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]);
}

这里存在错误,因为如果您传递一个空的points向量,则值points.size()-1将是一个巨大的正数,使您循环到segfault中。一个可行的解决方案可能是

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

但我个人更喜欢总是用CCD_ 26去除CCD_。

附言:如果你真的不想自己思考其中的含义,只是想让专家告诉你,那么考虑一下,许多世界公认的C++专家都同意并表达了意见,即除了位操作之外,无符号值是个坏主意。

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

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

迭代程序是operator[]的首选。C++11提供了std::begin()std::end()功能。

由于您的代码只使用了std::vector,所以我不能说这两个代码有多大区别,但是operator []可能无法按您的意愿运行。例如,如果您使用map,operator[]将在找不到元素的情况下插入一个元素。

此外,通过使用iterator,您的代码在容器之间变得更加可移植。如果您使用迭代器,则可以自由地将容器从std::vector切换到std::list或其他容器,而无需进行太多更改。这种规则不适用于operator[]

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

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

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

通过用迭代器编写客户端代码,可以完全抽象容器。

考虑这个代码:

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, " "));

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

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

什么';s更快,用vector::迭代器还是用at((迭代STL向量?

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