Range-based 循环语法
2026/7/2 11:08:36 网站建设 项目流程

大多数语言都支持 for-each 语法遍历一个数组或集合中的元素,C++ 11 中才支持这种语法,可谓姗姗来迟。在 C++ 98/03 规范中,对于一个数组 int arr[10],如果我们想要遍历这个数组,只能使用递增的计数去引用数组中每个元素:

int arr[10] = {0}; for (int i = 0; i < 10; ++i) { std::cout << arr[i] << std::endl; }

在 C++ 11 规范中有了for-each语法,我们可以这么写:

int arr[10] = {0}; for (int i : arr) { std::cout << i << std::endl; }

对于上面auto关键字章节遍历 std::map,我们也可以使用这种语法:

std::map<std::string, std::string> seasons; seasons["spring"] = "123"; seasons["summer"] = "456"; seasons["autumn"] = "789"; seasons["winter"] = "101112"; for (auto iter : seasons) { std::cout << iter.second << std::endl; }

for-each 语法虽然很强大,但是有两个需要注意的地方

  • for-each 中的迭代器类型与数组或集合中的元素的类型完全一致,而原来使用老式语法迭代 stl 容器(如 std::map)时,迭代器是类型的取地址类型。因此,在上面的例子中,老式语法中,iter是一个指针类型(std::pair<std::string, std::string>*),使用iter->second 去引用键值;而在 for-each 语法中,iter是数据类型(std::pair<std::string, std::string>),使用 iter.second 直接引用键值。

  • for-each 语法中对于复杂数据类型,迭代器是原始数据的拷贝,而不是原始数据的引用。什么意思呢?我们来看一个例子:

    std::vector<std::string> v; v.push_back("zhangsan"); v.push_back("lisi"); v.push_back("maowu"); v.push_back("maliu"); for (auto iter : v) { iter = "hello"; }

    我们遍历容器 v,意图将 v 中的元素的值都修改成“hello”,但是实际执行时我们却达不到我们想要的效果。这就是上文说的 for-each 中的迭代器是元素的拷贝,所以这里只是将每次拷贝修改成“hello”,原始数据并不会被修改。我们可以将迭代器修改成原始数据的引用:

    std::vector<std::string> v; v.push_back("zhangsan"); v.push_back("lisi"); v.push_back("maowu"); v.push_back("maliu"); for (auto& iter : v) { iter = "hello"; }

    这样我们就达到修改原始数据的目的了。这一点在使用 for-each 比较容易出错,对于容器中是复杂数据类型,我们尽量使用这种引用原始数据的方式,以避免复杂数据类型不必要的调用构造函数的开销。

    class A { public: A() { } ~A() = default; A(const A& rhs) { } public: int m; }; int main() { A a1; A a2; std::vector<A> v; v.push_back(a1); v.push_back(a2); for (auto iter : v) { //由于iter是v中的元素的拷贝,所以每一次循环,iter都会调用A的拷贝构造函数生成一份 //实际使用for-each循环时应该尽量使用v中元素的引用,减少不必要的拷贝函数的调用开销 iter.m = 9; } return 0; }

自定义对象如何支持 Range-based 循环

介绍了这么多,如何让我们自定义的对象支持 Range-based 循环语法呢?为了让一个对象支持这种语法,这个对象至少需要实现如下两个方法:

//需要返回第一个迭代子的位置 Iterator begin(); //需要返回最后一个迭代子的下一个位置 Iterator end();

上面的 Iterator 是自定义数据类型的迭代子类型,这里的 Iterator 类型必须支持如下三种操作(原因下文会解释):

  • operator++ (即自增)操作,即可以自增之后返回下一个迭代子的位置;

  • operator != (即判不等操作)操作;

  • operator* 即解引用(dereference)操作。

下面是一个自定义对象支持 for-each 循环的例子:

#include <iostream> #include <string> template<typename T, size_t N> class A { public: A() { for (size_t i = 0; i < N; ++i) { m_elements[i] = i; } } ~A() { } T* begin() { return m_elements + 0; } T* end() { return m_elements + N; } private: T m_elements[N]; }; int main() { A<int, 10> a; for (auto iter : a) { std::cout << iter << std::endl; } return 0; }

注意:上述代码中,迭代子 Iterator 是 T*,这是指针类型,本身就支持 operator ++ 和 operator != 操作,所以这里并没有提供这两个方法的实现。那么为什么迭代子要支持 operator ++ 和 operator != 操作呢?我们来看一下编译器是如何实现这种 for-each 循环的。

for-each 循环的实现原理

上述 for-each 循环可抽象成如下公式:

for (for-range-declaration : for-range-initializer) statement;

C++ 14 标准是这样解释上面的公式的:

auto && __range = for-range-initializer; for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin ) { for-range-declaration = *__begin; statement; }

在这个循环中,begin-expr 返回的迭代子 __begin 需要支持自增操作,且每次循环时会与 end-expr 返回的迭代子 __end 做判不等比较,在循环内部,通过调用迭代子的解引用(*)操作取得实际的元素。这就是上文说的迭代子对象需要支持 operator++、operator != 和 operator* 的原因了。

但是上面的公式中,在一个逗号表达式中auto __begin = begin-expr, __end = end-expr;由于只使用了一个类型符号 auto 导致其实迭代子 __begin 和结束迭代子 __end 是同一个类型,这样不太灵活,在某些设计中,可能希望循环结束时的迭代子是另外一种类型。

因此到了 C++17 标准时,要求编译器解释 for-each 循环成如下形式:

auto && __range = for-range-initializer; auto __begin = begin-expr; auto __end = end-expr; for ( ; __begin != __end; ++__begin ) { for-range-declaration = *__begin; statement; }

看到了吧,代码行23将获取起始迭代子 __begin 和结束迭代子 __end 分开来写,这样这两个迭代子就可以是不同的类型了。虽然类型可以不一样,但这两种类型之间仍然要支持 operator!= 操作。C++17 就 C++14 的这种改变,对旧的代码不会产生任何影响,但可以让后来的开发更加灵活。

关于 Range-based for loop 更详细的规范,可以参考这里:https://en.cppreference.com/w/cpp/language/range-for。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询