266

取以下两行代码:

for (int i = 0; i < some_vector.size(); i++)
{
    //do stuff
}

还有这个:

for (some_iterator = some_vector.begin(); some_iterator != some_vector.end();
    some_iterator++)
{
    //do stuff
}

我被告知第二种方式是首选。为什么会这样?

4

27 回答 27

230

只有当 vector.size() 是快速操作时,第一种形式才有效。例如,这对向量是正确的,但对于列表则不然。另外,你打算在循环体内做什么?如果您计划访问元素,如

T elem = some_vector[i];

那么您假设容器已operator[](std::size_t)定义。同样,这适用于矢量,但不适用于其他容器。

迭代器的使用使您更接近容器独立性。您没有对随机访问能力或快速size()操作做出假设,只是容器具有迭代器功能。

您可以使用标准算法进一步增强您的代码。根据您要实现的目标,您可以选择使用std::for_each()std::transform()依此类推。通过使用标准算法而不是显式循环,您可以避免重新发明轮子。您的代码可能更高效(如果选择了正确的算法)、正确且可重用。

于 2008-09-25T03:10:27.047 回答
56

它是现代 C++ 灌输过程的一部分。迭代器是迭代大多数容器的唯一方法,因此即使与向量一起使用它也是为了让自己进入正确的心态。说真的,这是我这样做的唯一原因——我认为我从来没有用不同类型的容器替换过向量。


哇,这在三周后仍然被否决。我想有点半开玩笑是不值得的。

我认为数组索引更具可读性。它与其他语言中使用的语法以及用于老式 C 数组的语法相匹配。它也不太冗长。如果你的编译器好的话,效率应该是一种洗涤,而且几乎没有任何情况下它很重要。

即便如此,我仍然发现自己经常将迭代器与向量一起使用。我相信迭代器是一个重要的概念,所以我会尽可能地推广它。

于 2008-09-25T03:35:42.867 回答
53

因为您没有将代码绑定到 some_vector 列表的特定实现。如果你使用数组索引,它必须是某种形式的数组;如果您使用迭代器,您可以在任何列表实现中使用该代码。

于 2008-09-25T03:02:54.537 回答
35

想象 some_vector 是用链表实现的。然后在第 i 个位置请求一个项目需要执行 i 个操作来遍历节点列表。现在,如果你使用迭代器,一般来说,它会尽最大努力尽可能高效(在链表的情况下,它会维护一个指向当前节点的指针并在每次迭代中推进它,只需要一个单次操作)。

所以它提供了两件事:

  • 使用抽象:你只是想迭代一些元素,你不关心怎么做
  • 表现
于 2008-09-25T03:08:40.200 回答
27

我将在这里成为魔鬼的拥护者,而不是推荐迭代器。主要原因是我从桌面应用程序开发到游戏开发的所有源代码都不需要使用迭代器。一直以来它们都不是必需的,其次是隐藏的假设、代码混乱和使用迭代器进行的调试噩梦,这使它们成为不要在任何需要速度的应用程序中使用它的主要示例。

即使从维护的角度来看,它们也是一团糟。这不是因为他们,而是因为幕后发生的所有混叠。我怎么知道您没有实现自己的虚拟向量或数组列表,这些虚拟向量或数组列表与标准完全不同。我知道现在在运行时是什么类型吗?你是否重载了一个运算符,我没有时间检查你所有的源代码。我什至知道您使用的是什么版本的 STL?

迭代器遇到的下一个问题是抽象泄漏,尽管有许多网站与他们详细讨论了这个问题。

抱歉,我还没有看到迭代器的任何意义。如果他们从您那里抽象出列表或向量,而实际上您应该已经知道要处理的向量或列表,如果您不知道,那么您将为将来的一些出色的调试会话做好准备。

于 2008-09-25T04:53:04.583 回答
24

如果要在迭代向量时向向量添加/删除项目,则可能需要使用迭代器。

some_iterator = some_vector.begin(); 
while (some_iterator != some_vector.end())
{
    if (/* some condition */)
    {
        some_iterator = some_vector.erase(some_iterator);
        // some_iterator now positioned at the element after the deleted element
    }
    else
    {
        if (/* some other condition */)
        {
            some_iterator = some_vector.insert(some_iterator, some_new_value);
            // some_iterator now positioned at new element
        }
        ++some_iterator;
    }
}

如果您使用索引,则必须在数组中向上/向下移动项目以处理插入和删除。

于 2008-09-25T03:23:12.833 回答
16

关注点分离

将迭代代码与循环的“核心”关注点分开是非常好的。这几乎是一个设计决定。

实际上,按索引进行迭代将您与容器的实现联系在一起。向容器询问开始和结束迭代器,使循环代码能够与其他容器类型一起使用。

另外,std::for_each顺便说一下,你告诉集合要做什么,而不是询问它的内部结构

0x 标准将引入闭包,这将使这种方法更易于使用 - 看看例如 Ruby 的表达能力[1..6].each { |i| print i; }......

表现

但也许一个备受关注的问题是,使用该for_each方法产生了使迭代并行化的机会——英特尔线程块可以将代码块分布在系统中的处理器数量上!

注意:在发现这个algorithms库之后,尤其是foreach.,我经历了两三个月的时间,编写了可笑的小“助手”操作符结构,这会让你的开发人员发疯。这段时间之后,我又回到了一种务实的方法——小循环体不再值得foreach:)

关于迭代器的必读参考书是“Extended STL”一书。

GoF 在 Iterator 模式的末尾有一个很小的段落,它谈到了这个品牌的迭代;它被称为“内部迭代器”。也看看这里

于 2008-09-27T20:05:00.090 回答
15

因为它更面向对象。如果您正在使用您假设的索引进行迭代:

a) 这些对象是有序的
b) 这些对象可以通过索引获得
c) 索引增量将命中每个项目
d) 该索引从零开始

使用迭代器,您是在说“给我一切,以便我可以使用它”而不知道底层实现是什么。(在Java中,存在无法通过索引访问的集合)

此外,使用迭代器,无需担心超出数组范围。

于 2008-09-25T03:04:54.920 回答
15

除了所有其他出色的答案...int对于您的矢量来说可能不够大。相反,如果您想使用索引,请size_type为您的容器使用:

for (std::vector<Foo>::size_type i = 0; i < myvector.size(); ++i)
{
    Foo& this_foo = myvector[i];
    // Do stuff with this_foo
}
于 2008-09-25T03:19:21.487 回答
15

迭代器的另一个好处是它们更好地允许您表达(并强制执行)您的 const 偏好。此示例确保您不会在循环过程中更改向量:


for(std::vector<Foo>::const_iterator pos=foos.begin(); pos != foos.end(); ++pos)
{
    // Foo & foo = *pos; // this won't compile
    const Foo & foo = *pos; // this will compile
}
于 2008-09-25T03:24:32.613 回答
12

我可能应该指出你也可以打电话

std::for_each(some_vector.begin(), some_vector.end(), &do_stuff);

于 2008-09-26T11:44:35.730 回答
8

STL 迭代器大部分都在那里,因此像排序这样的 STL 算法可以独立于容器。

如果您只想遍历向量中的所有条目,只需使用索引循环样式。

对于大多数人来说,它的输入更少并且更容易解析。如果 C++ 有一个简单的 foreach 循环而不过度使用模板魔法,那就太好了。

for( size_t i = 0; i < some_vector.size(); ++i )
{
   T& rT = some_vector[i];
   // now do something with rT
}
'
于 2008-09-25T13:36:12.267 回答
5

我认为它对向量没有太大影响。我更喜欢自己使用索引,因为我认为它更具可读性,并且您可以进行随机访问,例如向前跳转 6 个项目或在需要时向后跳转。

我也喜欢像这样引用循环内的项目,所以这个地方没有很多方括号:

for(size_t i = 0; i < myvector.size(); i++)
{
    MyClass &item = myvector[i];

    // Do stuff to "item".
}

如果您认为将来可能需要将向量替换为列表,那么使用迭代器可能会很好,而且对于 STL 怪胎来说它看起来也更时尚,但我想不出任何其他原因。

于 2008-09-25T03:03:05.410 回答
3

第二种形式更准确地代表了您正在做的事情。在您的示例中,您实际上并不关心 i 的值-您想要的只是迭代器中的下一个元素。

于 2008-09-25T03:20:39.283 回答
3

在对这个答案的主题有了更多了解之后,我意识到这有点过于简单化了。这个循环的区别:

for (some_iterator = some_vector.begin(); some_iterator != some_vector.end();
    some_iterator++)
{
    //do stuff
}

这个循环:

for (int i = 0; i < some_vector.size(); i++)
{
    //do stuff
}

相当小。事实上,以这种方式执行循环的语法似乎在我身上越来越多:

while (it != end){
    //do stuff
    ++it;
}

迭代器确实解锁了一些相当强大的声明性功能,当与 STL 算法库结合使用时,您可以做一些非常酷的事情,这些事情超出了数组索引管理的范围。

于 2008-12-16T22:46:17.620 回答
3

索引需要额外的mul操作。例如,对于vector<int> v,编译器将转换v[i]&v + sizeof(int) * i.

于 2009-05-08T04:47:42.890 回答
2

在迭代期间,您不需要知道要处理的项目数。您只需要项目和迭代器就可以很好地完成这些事情。

于 2008-09-25T04:25:11.937 回答
2

还没有人提到索引的一个优点是,当您附加到类似的连续容器时它们不会变得无效std::vector,因此您可以在迭代期间将项目添加到容器中。

使用迭代器也可以做到这一点,但您必须调用reserve(),因此需要知道要追加多少项。

于 2017-09-11T17:32:20.133 回答
2

如果您可以访问C++11功能,那么您还可以使用基于范围的for循环来迭代您的向量(或任何其他容器),如下所示:

for (auto &item : some_vector)
{
     //do stuff
}

此循环的好处是您可以直接通过item变量访问向量的元素,而不会冒弄乱索引或在取消引用迭代器时出错的风险。此外,占位符使auto您不必重复容器元素的类型,这使您更接近独立于容器的解决方案。

笔记:

  • 如果您需要循环中的元素索引并且operator[]容器存在(并且对您来说足够快),那么最好选择第一种方式。
  • 基于范围的for循环不能用于在容器中添加/删除元素。如果你想这样做,那么最好坚持Brian Matthews 给出的解决方案。
  • 如果您不想更改容器中的元素,则应使用以下关键字constfor (auto const &item : some_vector) { ... }.
于 2020-02-21T12:29:15.493 回答
1

已经有几个优点了。我还有几点补充意见:

  1. 假设我们谈论的是 C++ 标准库,“向量”意味着一个随机访问容器,它具有 C 数组的保证(随机访问、连续内存布局等)。如果您说“some_container”,则上述许多答案会更准确(容器独立性等)。

  2. 为了消除对编译器优化的任何依赖,您可以将 some_vector.size() 移出索引代码中的循环,如下所示:

    const size_t numElems = some_vector.size();
    for (size_t i = 0; 我
  3. 始终预增量迭代器并将后增量视为例外情况。

for (some_iterator = some_vector.begin(); some_iterator != some_vector.end(); ++some_iterator){ //做事 }

因此,假设容器和可索引std::vector<>容器一样,没有充分的理由优先选择一个,依次通过容器。如果您必须经常引用较旧或较新的元素索引,那么索引版本更合适。

一般来说,使用迭代器是首选,因为算法会使用它们,并且可以通过更改迭代器的类型来控制(并隐式记录)行为。数组位置可以用来代替迭代器,但语法上的差异会很突出。

于 2008-09-25T08:30:47.667 回答
1

我不使用迭代器的原因与我不喜欢 foreach 语句的原因相同。当有多个内部循环时,很难跟踪全局/成员变量,而不必记住所有的局部值和迭代器名称。我发现有用的是在不同场合使用两组索引:

for(int i=0;i<anims.size();i++)
  for(int j=0;j<bones.size();j++)
  {
     int animIndex = i;
     int boneIndex = j;


     // in relatively short code I use indices i and j
     ... animation_matrices[i][j] ...

     // in long and complicated code I use indices animIndex and boneIndex
     ... animation_matrices[animIndex][boneIndex] ...


  }

例如,我什至不想将诸如“animation_matrices[i]”之类的东西缩写为一些随机的“anim_matrix”-named-iterator,因为这样您就无法清楚地看到该值来自哪个数组。

于 2009-07-13T07:27:58.570 回答
1
  • 如果您喜欢接近金属/不相信他们的实现细节,请不要使用迭代器。
  • 如果您在开发过程中经常将一种集合类型切换为另一种,请使用迭代器。
  • 如果您发现难以记住如何迭代不同类型的集合(可能您使用了来自多个不同外部源的多种类型),请使用迭代器来统一遍历元素的方式。这适用于说用数组列表切换链表。

真的,仅此而已。平均而言,这并不是说你会获得更多的简洁,如果简洁真的是你的目标,你总是可以依靠宏。

于 2015-11-15T12:58:07.240 回答
0

比“告诉 CPU 做什么”(命令式)更好的是“告诉库你想要什么”(功能性)。

因此,您应该学习 stl 中存在的算法,而不是使用循环。

于 2008-09-25T04:58:46.410 回答
0

我总是使用数组索引,因为我的许多应用程序都需要“显示缩略图”之类的东西。所以我写了这样的东西:

some_vector[0].left=0;
some_vector[0].top =0;<br>

for (int i = 1; i < some_vector.size(); i++)
{

    some_vector[i].left = some_vector[i-1].width +  some_vector[i-1].left;
    if(i % 6 ==0)
    {
        some_vector[i].top = some_vector[i].top.height + some_vector[i].top;
        some_vector[i].left = 0;
    }

}
于 2008-09-25T06:30:54.053 回答
0

容器独立性

于 2008-09-25T11:14:58.830 回答
0

两种实现都是正确的,但我更喜欢“for”循环。由于我们决定使用 Vector 而不是任何其他容器,因此使用索引将是最佳选择。使用带有向量的迭代器将失去将对象放在连续内存块中的好处,这有助于简化它们的访问。

于 2012-07-02T11:04:57.043 回答
0

我觉得这里没有一个答案可以解释为什么我喜欢迭代器作为索引容器的一般概念。请注意,我使用迭代器的大部分经验实际上并非来自 C++,而是来自 Python 等高级编程语言。

迭代器接口对函数使用者的要求更少,这允许使用者用它做更多的事情。

如果您只需要能够进行前向迭代,那么开发人员不仅限于使用可索引容器——他们可以使用任何实现operator++(T&),operator*(T)operator!=(const &T, const &T).

#include <iostream>
template <class InputIterator>
void printAll(InputIterator& begin, InputIterator& end)
{
    for (auto current = begin; current != end; ++current) {
        std::cout << *current << "\n";
    }
}

// elsewhere...

printAll(myVector.begin(), myVector.end());

您的算法适用于您需要它的情况 - 迭代向量 - 但它对于您不一定预期的应用程序也很有用:

#include <random>

class RandomIterator
{
private:
    std::mt19937 random;
    std::uint_fast32_t current;
    std::uint_fast32_t floor;
    std::uint_fast32_t ceil;

public:
    RandomIterator(
        std::uint_fast32_t floor = 0,
        std::uint_fast32_t ceil = UINT_FAST32_MAX,
        std::uint_fast32_t seed = std::mt19937::default_seed
    ) :
        floor(floor),
        ceil(ceil)
    {
        random.seed(seed);
        ++(*this);
    }

    RandomIterator& operator++()
    {
        current = floor + (random() % (ceil - floor));
    }

    std::uint_fast32_t operator*() const
    {
        return current;
    }

    bool operator!=(const RandomIterator &that) const
    {
        return current != that.current;
    }
};

int main()
{
    // roll a 1d6 until we get a 6 and print the results
    RandomIterator firstRandom(1, 7, std::random_device()());
    RandomIterator secondRandom(6, 7);
    printAll(firstRandom, secondRandom);

    return 0;
}

尝试实现一个与该迭代器类似的方括号运算符将是人为的,而迭代器的实现相对简单。方括号运算符还会对您的类的功能产生影响——您可以索引到任意点——这可能难以实现或效率低下。

迭代器也适合装饰。人们可以编写迭代器,在构造函数中使用迭代器并扩展其功能:

template<class InputIterator, typename T>
class FilterIterator
{
private:
    InputIterator internalIterator;

public:
    FilterIterator(const InputIterator &iterator):
        internalIterator(iterator)
    {
    }

    virtual bool condition(T) = 0;

    FilterIterator<InputIterator, T>& operator++()
    {
        do {
            ++(internalIterator);
        } while (!condition(*internalIterator));

        return *this;
    }

    T operator*()
    {
        // Needed for the first result
        if (!condition(*internalIterator))
            ++(*this);
        return *internalIterator;
    }

    virtual bool operator!=(const FilterIterator& that) const
    {
        return internalIterator != that.internalIterator;
    }
};

template <class InputIterator>
class EvenIterator : public FilterIterator<InputIterator, std::uint_fast32_t>
{
public:
    EvenIterator(const InputIterator &internalIterator) :
        FilterIterator<InputIterator, std::uint_fast32_t>(internalIterator)
    {
    }

    bool condition(std::uint_fast32_t n)
    {
        return !(n % 2);
    }
};


int main()
{
    // Rolls a d20 until a 20 is rolled and discards odd rolls
    EvenIterator<RandomIterator> firstRandom(RandomIterator(1, 21, std::random_device()()));
    EvenIterator<RandomIterator> secondRandom(RandomIterator(20, 21));
    printAll(firstRandom, secondRandom);

    return 0;
}

虽然这些玩具可能看起来很普通,但不难想象使用迭代器和迭代器装饰器通过一个简单的接口来做强大的事情——例如,用一个从单个结果构造模型对象的迭代器来装饰数据库结果的只进迭代器. 这些模式使无限集的内存有效迭代成为可能,并且使用我上面写的过滤器,可能对结果进行惰性评估。

C++ 模板的部分强大之处在于您的迭代器接口,当应用于固定长度的 C 数组之类时,会衰减为简单高效的指针算法,使其成为真正的零成本抽象。

于 2019-11-27T22:25:43.327 回答