Range-based 循环语法 大多数语言都支持 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::mapstd::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::pairstd::string, std::string*使用iter-second 去引用键值而在 for-each 语法中iter是数据类型std::pairstd::string, std::string使用 iter.second 直接引用键值。for-each 语法中对于复杂数据类型迭代器是原始数据的拷贝而不是原始数据的引用。什么意思呢我们来看一个例子std::vectorstd::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::vectorstd::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::vectorA 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 templatetypename 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() { Aint, 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 是同一个类型这样不太灵活在某些设计中可能希望循环结束时的迭代子是另外一种类型。因此到了 C17 标准时要求编译器解释 for-each 循环成如下形式auto __range for-range-initializer; auto __begin begin-expr; auto __end end-expr; for ( ; __begin ! __end; __begin ) { for-range-declaration *__begin; statement; }看到了吧代码行2和3将获取起始迭代子 __begin 和结束迭代子 __end 分开来写这样这两个迭代子就可以是不同的类型了。虽然类型可以不一样但这两种类型之间仍然要支持 operator! 操作。C17 就 C14 的这种改变对旧的代码不会产生任何影响但可以让后来的开发更加灵活。关于 Range-based for loop 更详细的规范可以参考这里https://en.cppreference.com/w/cpp/language/range-for。