为什么要使用迭代器而不是数组索引?

281

考虑以下两行代码:

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
}

我被告知第二种方式更受欢迎。为什么呢?


77
若你将 some_iterator++ 修改为 ++some_iterator,则更倾向于使用第二种方式。因为后置自增会创建一个不必要的临时迭代器。 - jason
6
你还应该将 end() 加入声明子句。 - Lightness Races in Orbit
5
使用效率低下的vector::end(向量的结尾) 的C++实现,可能存在比它是否被提取出循环更值得担心的问题。就我个人而言,我更喜欢清晰易懂的代码——如果终止条件中涉及到find操作,那么我会感到有点担忧。 - Steve Jessop
13
@Tomalak: 这段代码并不邋遢(除了后置递增可能有点),就C++迭代器的允许范围而言,它是简洁明了的。增加更多的变量只是为了过早优化而增加认知负担,这才是邋遢的做法。 - Steve Jessop
8
如果它不是瓶颈,那么现在就进行还为时过早。你的第二点对我来说似乎很荒谬,因为正确的比较不是 it != vec.end()it != end,而是 (vector<T>::iterator it = vec.begin(); it != vec.end(); ++it)(vector<T>::iterator it = vec.begin(), end = vec.end(); it != end; ++it)。我不需要计算字符数。当然可以更喜欢其中一个,但别人对你偏好的异议并不是“粗心”,而是更喜欢变量更少、阅读时更易考虑的简化代码。 - Steve Jessop
显示剩余5条评论
27个回答

1
  • 如果你喜欢接近底层 / 不信任它们的实现细节,不要使用迭代器。
  • 如果你经常在开发过程中切换一种集合类型到另一个,使用迭代器。
  • 如果你发现难以记住如何迭代不同类型的集合(也许你有几种来自多个外部来源的类型在使用),使用迭代器来统一遍历元素的方式。这适用于将链表切换为数组列表等情况。

实际上,就是这样。无论哪种方式,在平均水平上都不会更加简洁,如果简洁确实是你的目标,你总是可以退而求其次使用宏。


1

已经有几个好观点了。我有一些额外的评论:

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

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

    const size_t numElems = some_vector.size();
    for (size_t i = 0; i 
  3. 始终预增迭代器,并将后增操作视为异常情况。


因此,假设有一个可索引的std::vector<>之类的容器,没有理由更喜欢顺序遍历容器中的一个而不是另一个。如果您需要频繁引用旧的或新的元素索引,则索引版本更合适。

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

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 数组的数据结构时,会衰变为简单高效的指针算术运算,从而实现真正的零成本抽象。


0
比起“告诉CPU该做什么”(命令式),更好的方法是“告诉库你想要什么”(函数式)。
因此,你应该学习STL中存在的算法,而不是使用循环。

0

为了实现容器独立性


0

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


2
“在向量中使用迭代器会失去对象连续内存块的好处,这有助于它们的访问。” [需要引证]。为什么?您认为将迭代器递增到连续容器中无法实现简单的加法吗? - underscore_d

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

}

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