使用'for'循环迭代C++向量

286

我刚接触C++语言。我开始使用向量,并注意到在所有遍历向量的代码中,通过索引迭代时for循环的第一个参数总是基于这个向量的。在Java中,我可能会像这样使用ArrayList:

for(int i=0; i < vector.size(); i++){
   vector[i].doSomething();
}

我不明白为什么在C++中找不到这个东西?这是一种不好的实践吗?


1
for循环不是函数,因此它没有参数(或参数是您传递的内容)。你是指像这样std::vector<int>::size_type i = 0;吗?或者可能是std::vector<int>::iterator it = vector.begin(); - chris
5
在Java中,我更喜欢使用for-each循环或迭代器。与C++基本相同,尽管语法略有不同。 - Jesse Good
2
可能是为什么使用迭代器而不是数组索引?的重复问题。 - johnsyweb
17
这里大多数答案错误地假设问题为:“在迭代std::vector时,什么是最好/最短的方法?”实际上,这里所问的问题是:“我为什么看不到这种用法在C ++中?它是不好的编程习惯吗?”也就是说,“为什么我总是看到在迭代std::vector时使用迭代器的C++代码?” - Alok Save
1
我认为这个问题在这里有一个更好的解释:https://dev59.com/SHRC5IYBdhLWcg3wG9Nb - rabin
显示剩余4条评论
14个回答

228

为什么你没有看到这样的做法,原因非常主观,不能有明确的答案,因为我见过很多代码都使用了您提到的方法而不是迭代器(iterator)风格的代码。

以下可能是人们不考虑使用 vector.size() 循环的原因:

  1. 对于在循环条件中每次调用 size() 的情况感到担忧。但是这要么不是问题,要么可以轻松解决。
  2. 更喜欢使用 std::for_each() 而不是 for 循环本身。
  3. 稍后将容器从 std::vector 更改为其他容器(例如 maplist)也会要求更改循环机制,因为并非每个容器都支持 size() 风格的循环。

C++11 提供了一种很好的遍历容器的方法,称为“范围 for 循环”(或 Java 中的 “增强型 for 循环”)。

只需少量代码,即可遍历整个(必须的!)std::vector

vector<int> vi;
...
for(int i : vi) 
  cout << "i = " << i << endl;

33
请注意"range based for loop"的一个小缺点:你无法将其与#pragma omp parallel for一起使用。 - liborm
2
我喜欢紧凑版本,因为需要阅读的代码更少。一旦你进行了心理调整,就会更容易理解,并且错误也更加明显。这还使得非标准迭代发生时更加明显,因为代码块更大。 - Code Abominator
2
@liborm 注意,这已经不再是真实情况。OpenMP 5.0支持基于范围的迭代。(来源:https://dev59.com/HmMm5IYBdhLWcg3wIsQ2#51390846) - djsavvy
1
基于范围的for循环语法简洁明了。然而,调试可能需要显式地知道索引。例如:循环写入文件。发现第125行对应第125次迭代产生了不正确的值。如何在调试时在第125次迭代处设置断点?(您不知道失败迭代的循环变量的值,除非更改代码并显式打印它。) - Matyas

164

使用迭代器是遍历向量最清晰的方法:

for (auto it = begin (vector); it != end (vector); ++it) {
    it->doSomething ();
}

或者(与上面的等价)

for (auto & element : vector) {
    element.doSomething ();
}
在C++0x之前,你必须用迭代器类型来替换auto,并使用成员函数而不是全局函数begin和end。这可能就是你所见过的。与你提到的方法相比,优点是你不会过度依赖于vector的类型。如果你将vector更改为不同的“集合类型”类,则代码可能仍然有效。但是,在Java中也可以做类似的事情。在概念上没有太大差别;但是,C ++使用模板来实现这一点(与Java中的泛型相比);因此,该方法适用于所有定义了begin和end函数的类型,甚至适用于静态数组等非类类型。请参见此处:How does the range-based for work for plain arrays?

7
auto、free begin/end也是C++11。此外,在许多情况下应该使用++it而不是it++。 - ForEveR
是的,你说得对。然而,实现 beginend 只需要一行代码。 - JohnB
如果你只需要它用于向量,那么这只需要一行代码。 - JohnB
如果你使用一个简单的迭代器切换到另一个集合,那么当然可以。但是如果切换到其中一种映射类,迭代器就会变成一对。在我看来,这些更改更常见,有人发现他们的数组或向量是稀疏的,于是一切都改变了。 - Code Abominator
关于 beginend,还有一个细节,它们需要 std:: 前缀,除非你添加了 using namespace std;。因此,许多人更喜欢使用 v.begin()v.end() 而不是 begin(v) 等等。 - Max
显示剩余2条评论

136

我在C++中为什么看不到这个方法?这是不好的编程习惯吗?

不是。这并不是不好的编程习惯,但以下方法可以使您的代码更加灵活。

通常,在C++11之前,迭代容器元素的代码使用迭代器,类似于:

std::vector<int>::iterator it = vector.begin();

这是因为它使代码更加灵活。

所有标准库容器都支持并提供迭代器。如果在开发的后期需要切换到另一个容器,那么这段代码无需更改。

注意:编写适用于所有可能的标准库容器的代码并不像看似那样简单。


35
在这个特定的案例/代码片段中,为什么您建议使用迭代器而不是索引?你所说的“灵活性”是什么?我个人不喜欢迭代器,它们会使代码变得臃肿 - 相同效果需要键入更多字符。尤其是如果您无法使用auto的情况下,请问能否解释一下?请问是否可以用迭代器来遍历容器,而不是通过数字索引来访问每个元素。这样做的好处是,避免了越界错误,并且在某些情况下可以提高代码的可读性和可维护性。另外,使用迭代器可以更轻松地处理不同类型的容器。您提到的“灵活性”指的是使用迭代器时,您可以以各种方式更改容器的内容,例如添加或删除元素,而无需更改循环本身。相比之下,如果使用索引,则必须小心计算边界和位置,以确保不会发生越界或其他错误。即使您无法使用auto关键字,也可以使用C++11或更高版本中的范围for循环语法,以更简洁的方式使用迭代器。 - Violet Giraffe
8
使用迭代器时,在某些情况下,如空范围,很难犯错,但代码会更加冗长。当然这是一种感知和选择的问题,因此可以无休止地辩论。 - Alok Save
28
为什么你只展示如何声明迭代器,而不展示如何使用它来进行循环呢? - underscore_d

50

正确的做法是:

for(std::vector<T>::iterator it = v.begin(); it != v.end(); ++it) {
    it->doSomething();
 }

T代表vector中的类的类型。例如,如果类是CActivity,则只需写CActivity而不是T。

这种类型的方法适用于每个STL(不仅仅是向量,这有点更好)。

如果您仍然想使用索引,方法是:

for(std::vector<T>::size_type i = 0; i != v.size(); i++) {
    v[i].doSomething();
}

1
std::vector<T>::size_type 不总是 size_t 吗?这就是我一直使用的类型。 - Violet Giraffe
1
@VioletGiraffe 我相信你是对的(没有真正检查过),但更好的做法是使用std::vector<T>::size_type。 - Dig

19

使用auto操作符确实使得使用变得容易,因为不需要担心向量或任何其他数据结构的数据类型和大小

使用自动和for循环迭代向量

vector<int> vec = {1,2,3,4,5}

for(auto itr : vec)
    cout << itr << " ";

输出:

1 2 3 4 5

你也可以使用这种方法迭代集合和列表。使用auto会自动检测模板中使用的数据类型并让您使用它。 因此,即使我们有一个vectorstringchar组成,相同的语法也将正常工作。


12

正确遍历向量并打印其值的方法如下:

#include<vector>

// declare the vector of type int
vector<int> v;

// insert elements in the vector
for (unsigned int i = 0; i < 5; ++i){
    v.push_back(i);
}

// print those elements
for (auto it = v.begin(); it != v.end(); ++it){
    std::cout << *it << std::endl;
}

但至少在目前的情况下,使用基于范围的for循环更好:

for (auto x: v) std::cout << x << "\n";

(您还可以在auto后添加&,使x成为元素的引用而不是它们的副本。然后它与上述基于迭代器的方法非常相似,但更易于阅读和编写。)


1
为什么要使用auto而不是int - user15072974
@Harith 变量it是整数向量v的迭代器,而不是整数,因此我们不能使用int来声明it,但是我们可以使用auto让编译器自动推断it的类型。您还可以在for循环中显式声明it的类型,如vector<int>::iterator it,例如for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)。至于x,您可以在技术上使用int来声明它,但是auto限定符通常用于基于范围的for循环以进行自动类型推断。 - YuanLinTech

8

使用迭代器有一些强大的理由,其中一些在此处提到:

稍后切换容器不会使您的代码失效。

即,如果您从std::vector切换到std::list或std::set,则无法使用数字索引访问包含的值。仍然可以使用迭代器。

运行时捕获无效迭代

如果您在循环中间修改了容器,则下次使用迭代器时它将抛出无效迭代器异常。


1
你能指出一些解释上述观点并附有示例代码的网络文章/帖子吗?那将非常棒!如果您能添加一个就更好了 :) - Anu
如果我没记错的话,使用(实际上是解引用)无效的迭代器是未定义行为(糟糕!)。虽然一些实现可能会抛出异常,但如果我没记错,这并不普遍。 - No-Bugs Hare
  1. 在循环过程中对某些容器进行一定的修改是完全可以的(例如,在list<>、map<>等容器中)。
- No-Bugs Hare

7

这里有一种更简单的方法来迭代并打印向量中的值。

for(int x: A) // for integer x in vector A
    cout<< x <<" "; 

5

不要忘记使用const修饰符的示例 - 循环能否修改元素。这里有很多例子,它们不能修改元素,并且应该使用const迭代器。在这里我们假设

class T {
  public:
    T (double d) : _d { d } {}
    void doSomething () const { cout << _d << endl; return; }
    void changeSomething ()   { ++_d; return; }
  private:
    double _d;
};

vector<T> v;
// ...
for (const auto iter = v.cbegin(); iter != v.cend(); ++iter) {
    iter->doSomething();
}

同时需要注意的是,在C++11标准下,默认情况下会复制元素。使用引用可以避免这种情况,也可以允许修改原始元素:

vector<T> v;
// ...
for (auto t : v) {
    t.changeSomething(); // changes local t, but not element of v
    t.doSomething();
}
for (auto& t : v) {      // reference avoids copying element
    t.changeSomething(); // changes element of v
    t.doSomething();
}
for (const auto& t : v) { // reference avoids copying element
    t.doSomething();      // element can not be changed
}

5

使用STL,程序员使用迭代器来遍历容器,因为迭代器是一个抽象概念,在所有标准容器中都有实现。例如,std::list根本没有operator []


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