章节目录

基于范围的for循环(range for / for each)

本节阅读量:

前面展示了使用for循环迭代数组每个元素的示例,其中使用循环变量作为索引。下面是一个这样的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    std::size_t length { fibonacci.size() };
    for (std::size_t index { 0 }; index < length; ++index)
       std::cout << fibonacci[index] << ' ';

    std::cout << '\n';

    return 0;
}

尽管for循环提供了一种方便而灵活的数组迭代方法,但它们也很容易写错,容易出现索引越界错误,也容易遇到数组索引变量的符号问题。

由于遍历数组非常常见,C++支持另一种类型的for循环,称为基于范围的for循环(有时也称为for each循环)。该循环允许遍历容器,而不必显式使用索引。基于范围的for循环更简单、更安全,并且可以与C++中的所有常见数组类型(包括std::vector、std::array和C样式数组)一起使用。


基于范围的for循环

基于范围的for语句的语法如下所示:

1
2
for (元素声明 : 数组对象)
   语句;

遇到基于范围的for循环时,循环会迭代“数组对象”中的每个元素。每次迭代时,当前数组元素的值会被赋给“元素声明”中声明的变量,然后执行语句。

为了获得最佳结果,“元素声明”应该与数组元素具有相同类型,否则将发生类型转换。

下面是一个简单示例,使用基于范围的for循环打印名为fibonacci的数组中的所有元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (int num : fibonacci) // 迭代 fibonacci 里的每个元素,将其拷贝到 `num`
       std::cout << num << ' '; // 打印 `num` 的值

    std::cout << '\n';

    return 0;
}

这将打印:

1
0 1 1 2 3 5 8 13 21 34 55 89

注意,这个例子不需要使用数组长度,也不需要索引数组!

让我们仔细看看它是如何工作的。这个基于范围的for循环会遍历fibonacci中的所有元素。第一次迭代时,变量num被赋为第一个元素(0)的值。然后,程序执行关联语句,将num(0)的值打印到控制台。第二次迭代时,num被赋为第二个元素(1)的值。关联语句再次执行,打印1。基于范围的for循环会继续依次迭代每个数组元素,并为每个元素执行关联语句,直到数组中没有剩余元素可供迭代。此时,循环终止,程序继续执行(打印换行,然后将0返回给操作系统)。

由于num被赋为数组元素的值,这意味着数组元素会被复制(对于某些类型,这可能很昂贵)。


使用auto关键字进行类型自动推导

因为元素声明应该与数组元素具有相同类型(以防止发生类型转换),所以这里非常适合使用auto关键字,让编译器为我们推断数组元素类型。这样,就不必冗余地指定类型,也不会因为手动输入类型而出错。

下面是与上面相同的示例,但使用auto作为num的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (auto num : fibonacci) // 编译器将会自动推导 num 的类型是 `int`
       std::cout << num << ' ';

    std::cout << '\n';

    return 0;
}

由于std::vector fibonacci包含int类型的元素,因此num会被推断为int。

使用auto的另一个好处是,如果数组元素类型以后发生变化(例如从int改成long),auto会自动推断更新后的元素类型,确保它们保持同步(并防止发生类型转换)。


尽量避免使用拷贝

考虑以下基于范围的for循环,它会迭代std::string数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };

    for (auto word : words)
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

对于这个循环的每次迭代,words数组中的下一个std::string元素都会被赋值(复制)到变量word中。复制std::string的开销很大,这就是为什么我们通常通过常量引用将std::string传递给函数。我们希望避免复制成本高昂的对象,除非确实需要副本。在当前情况下,我们只是打印副本的值,然后副本就会被销毁。如果可以避免复制,而只是引用实际的数组元素,那会更好。

幸运的是,可以通过将“元素声明”设置为(const)引用来做到这一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };

    for (const auto& word : words) // word 现在是常量引用
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

在上面的示例中,word现在是常量引用。在该循环的每次迭代中,word都会绑定到下一个数组元素。这允许我们访问数组元素的值,而不必制作昂贵副本。

如果引用是非常量的,它也可以用于更改数组中的值(如果“元素声明”是值的副本,则做不到这一点)。


什么时候使用 auto,auto& 或 const auto&

通常,可廉价复制的类型使用auto,需要更改的对象使用 auto&,复制成本高的类型使用 const auto&。但对于基于范围的for循环,许多开发人员认为最好始终使用const auto&,因为它更经得起未来变化。

例如,考虑以下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "peter", "likes", "frozen", "yogurt" }; // 元素是 std::string_view

    for (auto word : words) // string_view 通常按值传递,所以这里使用auto
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

在这个例子中,有一个包含std::string_view对象的std::vector。由于std::string_view通常按值传递,因此使用auto似乎是合适的。

但考虑一下,如果后来将words更新为std::string数组,会发生什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" }; // 这里修改了

    for (auto word : words) // 可能这里也需要修改了
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

基于范围的for循环仍然可以很好地编译和执行,但word现在会被推断为std::string。由于使用的是auto,循环会默默制作std::string元素的昂贵副本。性能会受到巨大影响!

有两种合理方法可以确保不会发生这种情况:

  1. 不要在基于范围的for循环中使用类型推导。如果显式将元素类型指定为std::string_view,那么当数组稍后更新为std::string时,std::string元素会隐式转换为std::string_view,这没有问题。如果数组被更新为其他不可转换的类型,编译器会报错,我们也能据此判断此时应该怎么做。
  2. 当不想处理副本时,如果在基于范围的for循环中使用类型推导,请始终使用 const auto& 而不是auto。通过引用而不是按值访问元素的性能损失通常很小;如果元素类型后来被更改为复制成本高昂的类型,这可以避免潜在的重大性能损失。

其他标准容器类型

基于范围的循环适用于各种数组类型,包括C样式数组、std::array、std::vector、链表、树和map。后面这些还没有讲到,因此如果您不知道它们是什么,也不用担心。请记住,基于范围的for循环提供了一种灵活且通用的迭代方法,而不仅仅适用于std::vector:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <array>
#include <iostream>

int main()
{
    std::array fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; // 这里使用 std::array

    for (auto number : fibonacci)
    {
        std::cout << number << ' ';
    }

    std::cout << '\n';

    return 0;
}

获取当前元素的索引

基于范围的for循环不提供直接获取当前元素数组索引的方法。这是因为基于范围的for循环可以迭代的许多结构(如std::list)并不支持索引。

然而,由于基于范围的for循环总是向前迭代,并且不会跳过元素,因此您始终可以声明并维护自己的计数器。不过,如果需要这样做,应该考虑是否直接使用普通for循环会更合适。


基于范围的反向循环(C++20)

基于范围的循环只按正向顺序迭代。然而,在某些情况下,我们希望以相反顺序遍历数组。在C++20之前,基于范围的for循环不能轻易用于这个目的,必须采用其他解决方案。

不过,从C++20开始,可以使用Ranges库的std::views::reverse功能,创建可遍历元素的反向视图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <ranges> // C++20
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "Alex", "Bobby", "Chad", "Dave" }; // 按字母表排序

    for (const auto& word : std::views::reverse(words)) // 创建一个反向的视图
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

这将打印:

1
2
3
4
Dave
Chad
Bobby
Alex

我们还没有介绍range库,所以现在可以先把它看作一个有用的技巧。


16.6 数组、循环和有符号下标

上一节

16.8 使用枚举值来作为数组索引

下一节