基于范围的for循环(range for / for each)
本节阅读量:前面展示了使用for循环迭代数组每个元素的示例,其中使用循环变量作为索引。下面是一个这样的例子:
|
|
尽管for循环提供了一种方便而灵活的数组迭代方法,但它们也很容易写错,容易出现索引越界错误,也容易遇到数组索引变量的符号问题。
由于遍历数组非常常见,C++支持另一种类型的for循环,称为基于范围的for循环(有时也称为for each循环)。该循环允许遍历容器,而不必显式使用索引。基于范围的for循环更简单、更安全,并且可以与C++中的所有常见数组类型(包括std::vector、std::array和C样式数组)一起使用。
基于范围的for循环
基于范围的for语句的语法如下所示:
|
|
遇到基于范围的for循环时,循环会迭代“数组对象”中的每个元素。每次迭代时,当前数组元素的值会被赋给“元素声明”中声明的变量,然后执行语句。
为了获得最佳结果,“元素声明”应该与数组元素具有相同类型,否则将发生类型转换。
下面是一个简单示例,使用基于范围的for循环打印名为fibonacci的数组中的所有元素:
|
|
这将打印:
|
|
注意,这个例子不需要使用数组长度,也不需要索引数组!
让我们仔细看看它是如何工作的。这个基于范围的for循环会遍历fibonacci中的所有元素。第一次迭代时,变量num被赋为第一个元素(0)的值。然后,程序执行关联语句,将num(0)的值打印到控制台。第二次迭代时,num被赋为第二个元素(1)的值。关联语句再次执行,打印1。基于范围的for循环会继续依次迭代每个数组元素,并为每个元素执行关联语句,直到数组中没有剩余元素可供迭代。此时,循环终止,程序继续执行(打印换行,然后将0返回给操作系统)。
由于num被赋为数组元素的值,这意味着数组元素会被复制(对于某些类型,这可能很昂贵)。
关键点
声明的元素(上例中的num)不是数组索引。相反,它会被赋为当前迭代到的数组元素值。
最佳实践
遍历容器时,优先使用基于范围的for循环。
使用auto关键字进行类型自动推导
因为元素声明应该与数组元素具有相同类型(以防止发生类型转换),所以这里非常适合使用auto关键字,让编译器为我们推断数组元素类型。这样,就不必冗余地指定类型,也不会因为手动输入类型而出错。
下面是与上面相同的示例,但使用auto作为num的类型:
|
|
由于std::vector fibonacci包含int类型的元素,因此num会被推断为int。
使用auto的另一个好处是,如果数组元素类型以后发生变化(例如从int改成long),auto会自动推断更新后的元素类型,确保它们保持同步(并防止发生类型转换)。
最佳实践
将类型演绎(auto)与基于范围的for循环一起使用,以使编译器推断数组元素的类型。
尽量避免使用拷贝
考虑以下基于范围的for循环,它会迭代std::string数组:
|
|
对于这个循环的每次迭代,words数组中的下一个std::string元素都会被赋值(复制)到变量word中。复制std::string的开销很大,这就是为什么我们通常通过常量引用将std::string传递给函数。我们希望避免复制成本高昂的对象,除非确实需要副本。在当前情况下,我们只是打印副本的值,然后副本就会被销毁。如果可以避免复制,而只是引用实际的数组元素,那会更好。
幸运的是,可以通过将“元素声明”设置为(const)引用来做到这一点:
|
|
在上面的示例中,word现在是常量引用。在该循环的每次迭代中,word都会绑定到下一个数组元素。这允许我们访问数组元素的值,而不必制作昂贵副本。
如果引用是非常量的,它也可以用于更改数组中的值(如果“元素声明”是值的副本,则做不到这一点)。
什么时候使用 auto,auto& 或 const auto&
通常,可廉价复制的类型使用auto,需要更改的对象使用 auto&,复制成本高的类型使用 const auto&。但对于基于范围的for循环,许多开发人员认为最好始终使用const auto&,因为它更经得起未来变化。
例如,考虑以下示例:
|
|
在这个例子中,有一个包含std::string_view对象的std::vector。由于std::string_view通常按值传递,因此使用auto似乎是合适的。
但考虑一下,如果后来将words更新为std::string数组,会发生什么。
|
|
基于范围的for循环仍然可以很好地编译和执行,但word现在会被推断为std::string。由于使用的是auto,循环会默默制作std::string元素的昂贵副本。性能会受到巨大影响!
有两种合理方法可以确保不会发生这种情况:
- 不要在基于范围的for循环中使用类型推导。如果显式将元素类型指定为std::string_view,那么当数组稍后更新为std::string时,std::string元素会隐式转换为std::string_view,这没有问题。如果数组被更新为其他不可转换的类型,编译器会报错,我们也能据此判断此时应该怎么做。
- 当不想处理副本时,如果在基于范围的for循环中使用类型推导,请始终使用 const auto& 而不是auto。通过引用而不是按值访问元素的性能损失通常很小;如果元素类型后来被更改为复制成本高昂的类型,这可以避免潜在的重大性能损失。
其他标准容器类型
基于范围的循环适用于各种数组类型,包括C样式数组、std::array、std::vector、链表、树和map。后面这些还没有讲到,因此如果您不知道它们是什么,也不用担心。请记住,基于范围的for循环提供了一种灵活且通用的迭代方法,而不仅仅适用于std::vector:
|
|
获取当前元素的索引
基于范围的for循环不提供直接获取当前元素数组索引的方法。这是因为基于范围的for循环可以迭代的许多结构(如std::list)并不支持索引。
然而,由于基于范围的for循环总是向前迭代,并且不会跳过元素,因此您始终可以声明并维护自己的计数器。不过,如果需要这样做,应该考虑是否直接使用普通for循环会更合适。
基于范围的反向循环(C++20)
基于范围的循环只按正向顺序迭代。然而,在某些情况下,我们希望以相反顺序遍历数组。在C++20之前,基于范围的for循环不能轻易用于这个目的,必须采用其他解决方案。
不过,从C++20开始,可以使用Ranges库的std::views::reverse功能,创建可遍历元素的反向视图:
|
|
这将打印:
|
|
我们还没有介绍range库,所以现在可以先把它看作一个有用的技巧。