C++ 基于范围的 for 循环(for-each)

在课程《数组与循环》中,我们展示了如何利用 for 循环并以循环变量作为下标来遍历数组元素。下面是一个类似示例:

#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 循环灵活且功能强大,却容易出错:可能发生差一错误,也会遇到符号转换警告(见课程 16.7《数组、循环与符号难题解决》)。

由于“从头到尾顺序遍历容器”极为常见,C++ 提供了另一种 for 循环——基于范围的 for 循环(range-based for loop,也称 for-each 循环)。它无需显式下标即可遍历容器,语法更简单、更安全,且适用于 C++ 中所有常见数组类型(包括 std::vectorstd::array 以及 C 风格数组)。

基于范围的 for 循环

语法如下:

for (element_declaration : array_object)
    statement;

循环执行时,会依次遍历 array_object 的每个元素。每次迭代,当前元素的值被赋给 element_declaration 中声明的变量,随后执行 statement

为获得最佳效果,element_declaration 的类型应与数组元素类型一致,否则会发生类型转换。

下面用基于范围的 for 循环打印 fibonacci 的所有元素:

#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)           // 遍历数组,把每个值复制到 num
       std::cout << num << ' ';         // 打印 num

    std::cout << '\n';

    return 0;
}

输出:

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

注意:我们既未使用数组长度,也未显式下标。

工作机制详解

该循环会依次遍历 fibonacci 的所有元素。

  • 第一次迭代:num 获得首元素值 0,打印 0
  • 第二次迭代:num 获得第二个元素值 1,打印 1
  • 依次类推,直到遍历完所有元素,循环结束,程序继续执行(打印换行并返回 0)。

关键洞见
element_declaration(上例中的 num)并非下标,而是“当前元素的值”。
由于 num 被赋值为元素值,这意味着元素被复制(对某些类型可能代价高昂)。

最佳实践
遍历容器时,优先使用基于范围的 for 循环,而非传统 for 循环。

空容器的处理

若被遍历容器为空,循环体不会执行:

#include <iostream>
#include <vector>

int main()
{
    std::vector empty { };

    for (int num : empty)
       std::cout << "Hi mom!\n";

    return 0;
}

程序无输出——抱歉,妈妈!

使用 auto 进行类型推导

由于 element_declaration 应与元素类型一致,可用 auto 让编译器自行推导,避免重复书写类型,也能防止误输:

#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;
}

最佳实践
基于范围的 for 循环中使用 auto 进行类型推导。

额外好处:若元素类型后来改变(如从 int 变为 long),auto 会自动同步,避免类型转换。

避免元素拷贝:使用引用

考虑以下示例,遍历 std::string 数组:

#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';
}

每次迭代都会把 std::string 元素复制到 word,而 std::string 拷贝开销高。我们只需读取值,无需拷贝。可以改用引用:

#include <iostream>
#include <string>
#include <vector>

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

    for (const auto& word : words)      // word 为 const 引用
        std::cout << word << ' ';

    std::cout << '\n';
}

word 被绑定到当前元素,无需拷贝即可访问其值。若引用为非 const,还可修改元素。

何时使用 auto / auto& / const auto&

需求推荐写法
需要修改元素副本auto
需要修改原元素auto&
只需查看原元素const auto&

许多开发者建议 始终使用 const auto&,以免未来元素类型变为“复制昂贵”时产生性能问题。

示例:

std::vector<std::string_view> words{ "peter", "likes", "frozen", "yogurt" };
for (auto word : words)           // 通常 string_view 按值传递,看似合理

若以后改为 std::vector<std::string>

std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };
for (auto word : words)           // 编译正常,但会复制昂贵的 std::string

此时循环仍能编译运行,却会默默产生拷贝开销!

两种避免方案:

  1. 明确指定元素类型为 std::string_view,以后若改为不可转换类型则编译报错;若可转换则静默转换,但可能效率不佳。
  2. 在基于范围的 for 循环中使用 const auto&,即使元素类型变为昂贵类型,也不会产生拷贝。

最佳实践
基于范围的 for 循环中,按以下规则声明元素:

  • 要修改副本auto
  • 要修改原元素auto&
  • 其余情况(只读):const auto&

与其他标准容器配合

基于范围的 for 循环适用于多种数组类型:非退化 C 风格数组、std::arraystd::vector、链表、树、映射等。我们尚未介绍这些类型,只需记住其通用性:

#include <array>
#include <iostream>

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

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

    std::cout << '\n';
}

高级读者

  • 基于范围的 for 循环不适用于退化的 C 风格数组,因为需要长度信息,而退化数组不含长度。
  • 也不适用于枚举类型,课 17.6 将给出解决方案。

获取当前元素下标

基于范围的 for 循环不直接提供下标,因为某些可迭代结构(如 std::list)不支持索引。

但由于循环永远顺序遍历且不会跳过元素,可手动维护计数器。不过,若需下标,通常应考虑改用传统 for 循环。

C++20 逆向遍历

基于范围的 for 循环只支持正向遍历。C++20 前需用传统循环。C++20 起,可用 Ranges 库的 std::views::reverse 创建反向视图:

#include <iostream>
#include <ranges>
#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';
}

输出:

Dave Chad Bobby Alex

Ranges 库尚未介绍,先当作“魔法”即可。


小测验

问题 1
定义 std::vector 并初始化如下姓名:"Alex""Betty""Caroline""Dave""Emily""Fred""Greg""Holly"。提示用户输入姓名,并用基于范围的 for 循环判断输入是否在数组中。

示例运行:

Enter a name: Betty
Betty was found.
Enter a name: Megatron
Megatron was not found.

提示:用 std::string 保存用户输入;std::string_view 复制开销小。

[显示解答]

问题 2
修改问题 1 的答案,将判断逻辑封装为函数模板 isValueInArray(),接收 std::vector 与待查值,返回布尔结果。从 main() 调用该模板。

提醒:

  • 使用模板实参推导时,不会执行类型转换以匹配模板形参;
  • 显式指定模板实参时,会执行类型转换。

[显示解答]

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"