为什么使用迭代器而不是数组下标?

以下面两行代码为例:

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
}

有人告诉我第二种方法更可取。为什么会这样呢?

81617 次浏览

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

我不认为这对向量有多大区别。我更喜欢自己使用索引,因为我认为它更具可读性,你可以做随机访问,如向前跳转6个项目或向后跳转,如果需要的话。

我还喜欢像这样引用循环内的项目,这样在位置周围就不会有很多方括号:

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


// Do stuff to "item".
}

使用迭代器可以很好,如果你认为你可能需要在未来的某个时候用一个列表替换向量,它也看起来更时尚的STL怪胎,但我想不出任何其他原因。

因为它更面向对象。如果你用一个索引迭代,你假设:

a)这些对象的顺序
B)这些对象可以通过索引
获得 C)索引增量将触及
的每一项 D)该指数从0开始

使用迭代器,你是在说“给我所有东西,这样我就可以使用它”,而不知道底层实现是什么。(在Java中,有些集合不能通过索引访问)

此外,使用迭代器,无需担心超出数组的边界。

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

所以它提供了两件事:

  • 使用的抽象:你只想迭代一些元素,你不关心如何去做
  • 性能

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

T elem = some_vector[i];

那么你就假设容器已经定义了operator[](std::size_t)。同样,这适用于vector容器,但不适用于其他容器。

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

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

除了所有其他优秀的答案之外……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
}

第二种形式更准确地表示您正在做什么。在你的例子中,你并不关心i的值,你所需要的只是迭代器中的下一个元素。

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

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;
}
}

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

迭代器的另一个好处是,它们更好地允许你表达(和执行)你的const-preference。这个例子确保你不会在循环中改变向量:


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
}
这是现代c++灌输过程的一部分。迭代器是迭代大多数容器的唯一方法,所以即使对向量也使用迭代器,只是为了让自己进入正确的心态。说真的,这是我这么做的唯一原因——我不认为我曾经用不同类型的容器替换过一个向量。
哇,三周之后,还是有人投反对票。我想开玩笑是没有用的。

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

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

在迭代过程中,您不需要知道要处理的项目的数量。你只需要item和迭代器就能很好地完成这些事情。

我在这里是魔鬼的倡导者,不推荐使用迭代器。主要原因是,我从桌面应用程序开发到游戏开发的所有源代码都没有我也不需要使用迭代器。一直以来,迭代器都不是必需的,其次,迭代器所带来的隐藏假设、代码混乱和调试噩梦,使其成为任何要求速度的应用程序都不要使用的典型例子。

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

迭代器的下一个问题是抽象漏洞,尽管有许多网站对此进行了详细讨论。

对不起,我没有,现在也没有看到迭代器有任何意义。如果他们抽象了列表或向量,而实际上你应该已经知道你要处理什么向量或列表,如果你不知道,那么你只会为将来的一些伟大的调试会话做好准备。

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

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

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

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;
}


}

已经有几个好观点了。我还有一些补充意见:

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

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

    const size_t numElems = some_vector.size();
    for (size_t i = 0; i 
  3. Always pre-increment iterators and treat post-increments as exceptional cases.

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

因此,假设std::vector<>和容器一样是可索引的,那么没有充分的理由优先选择一个而不是另一个,按顺序遍历容器。如果必须频繁引用较旧或较新的元素索引,则使用索引版本更合适。

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

容器独立性

STL迭代器大部分都在那里,所以STL算法,比如sort,可以是容器独立的。

如果你只是想循环遍历一个向量中的所有项,只需使用索引循环样式。

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

for( size_t i = 0; i < some_vector.size(); ++i )
{
T& rT = some_vector[i];
// now do something with rT
}
'

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

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

分离关注点

将迭代代码从循环的“核心”关注点中分离出来是非常好的。这几乎是一个设计决策。

实际上,通过索引迭代将您与容器的实现联系在一起。向容器请求开始和结束迭代器,使循环代码可用于其他容器类型。

同样,在std::for_each方式中,你告诉集合要做什么,而不是询问它的一些内部内容

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

性能

但可能有一个更容易被监督的问题是,使用for_each方法产生了一个并行迭代的机会——英特尔螺纹块可以将代码块分布到系统中的多个处理器上!

注意:在发现algorithms库,特别是foreach之后,我花了两三个月的时间编写了非常小的“helper”操作符结构,这会让你的开发人员同事们发疯。在此之后,我回到了一个实用的方法-小循环体不应该再foreach:)

关于迭代器的必读参考是“扩展STL”这本书。

GoF在迭代器模式的末尾有一小段话,讲的是这种迭代;它被称为“内部迭代器”。也来看看在这里

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

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算法库结合使用时,您可以做一些非常酷的事情,这些事情超出了数组索引管理的范围。

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

我不使用迭代器的原因与我不喜欢foreach-statements的原因相同。当有多个内部循环时,如果不记住所有的局部值和迭代器名称,就很难跟踪全局/成员变量。我发现有用的是在不同的情况下使用两组指标:

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”-name -iterator,因为这样你就不能清楚地看到这个值来自哪个数组。

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

  • 如果你喜欢接近金属/不相信他们的实现细节,不要使用迭代器。
  • 如果在开发过程中经常将一种集合类型切换为另一种,则使用迭代器。
  • 如果你发现很难记住如何迭代不同类型的集合(也许你使用了来自不同外部源的几种类型),使用使用迭代器来统一遍历元素的方法。这适用于切换一个链表和一个数组列表。

真的,就是这样。这并不是说您要获得更多的简洁,如果简洁确实是您的目标,您总是可以求助于宏。

还没有人提到,索引的一个优点是,当你向std::vector这样的连续容器添加索引时,它们不会失效,所以你可以在迭代过程中向容器添加项。

这也可以用迭代器实现,但必须调用reserve(),因此需要知道要追加多少项。

我觉得这里的答案没有一个能解释为什么我喜欢把迭代器作为一个通用概念,而不是索引到容器中。请注意,我使用迭代器的大部分经验实际上并不是来自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数组衰减为简单有效的指针算术时,使其成为真正的零成本抽象。

如果你可以访问c++ 11特性,那么你也可以使用基于range的for循环来迭代你的向量(或任何其他容器),如下所示:

for (auto &item : some_vector)
{
//do stuff
}
这个循环的好处是,你可以直接通过item变量访问vector的元素,而不会有弄乱索引或在解引用迭代器时出错的风险。另外,占位符auto可以防止你重复容器元素的类型, 这让你离容器无关的解决方案更近了

注:

  • 如果你需要循环中的元素索引,并且容器中存在operator[](并且对你来说足够快),那么最好采用第一种方法。
  • 基于范围的for循环不能用于在容器中添加/删除元素。如果你想这样做,那么最好坚持布莱恩马修斯给出的解决方案
  • 如果你不想改变容器中的元素,那么你应该使用关键字const,如下所示: