16-8 基于范围的 for 循环(for-each)

第16.6节——数组与循环中,我们展示了使用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;
}

image

尽管for循环为遍历数组提供了便捷灵活的方式,但它们也容易出错,容易引发偏移量错误,且受数组索引符号问题的影响(详见第16.7节——数组、循环与符号挑战解决方案)。

由于(正向)遍历数组是如此常见的操作,C++支持另一种名为基于范围的for循环range-based for loop(有时也称为for-each循环for-each loop),它允许在无需显式索引的情况下遍历容器。基于范围的 for 循环更简洁、更安全,且适用于 C++ 中所有常见数组类型(包括 std::vector、std::array 和 C 风格数组)。


基于范围的 for 循环

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

for (element_declaration : array_object)
   statement;

遇到基于范围的 for 循环时,该循环将遍历数组对象 array_object 中的每个元素。每次迭代时,当前数组元素的值将被赋给在 element_declaration 中声明的变量,随后执行语句。

为获得最佳效果,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) // iterate over array fibonacci and copy each value into `num`
       std::cout << num << ' '; // print the current value of `num`

    std::cout << '\n';

    return 0;
}

这将输出:

image

请注意,这个示例既不需要我们使用数组的长度,也不需要我们对数组进行索引!

让我们深入解析其工作原理。这个基于范围的 for 循环将遍历 fibonacci 数组的所有元素。首次迭代时,变量 num 被赋予数组首元素的值(0)。随后程序执行关联语句,将 num 的值(0)输出至控制台。第二次迭代时,num 被赋予数组次元素的值(1)。关联语句再次执行,输出1。基于范围的for循环持续遍历数组元素,逐个执行关联语句,直至数组中无剩余元素可遍历。此时循环终止,程序继续执行(输出换行符并向操作系统返回0)。

关键要点
声明的变量(前例中的num)并非数组索引,而是被赋予当前迭代数组元素的值。

由于num被赋予数组元素值,这意味着数组元素会被复制(对某些类型而言可能造成性能开销)。

最佳实践:
遍历容器时,优先使用基于范围的for循环替代常规for循环。


基于范围的 for 循环与空容器

当遍历的容器中不存在元素时,基于范围的 for 循环主体将不会执行:

#include <iostream>
#include <vector>

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

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

    return 0;
}

image

上述示例不会输出任何内容。抱歉啦,妈妈!


基于范围的 for 循环与 auto 关键字的类型推断

由于元素声明应与数组元素具有相同类型(以避免发生类型转换),这正是使用 auto 关键字的理想场景,可让编译器为我们推断数组元素的类型。这样既避免了冗余的类型声明,又杜绝了误输类型的风险(哈哈,真是“误打误撞”啊!)。

以下是与前例相同的示例,但将 num 的类型改为 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) // compiler will deduce type of num to be `int`
       std::cout << num << ' ';

    std::cout << '\n';

    return 0;
}

image

由于 std::vector fibonacci 的元素类型为 int,num 将被推断为 int 类型。

最佳实践:
在基于范围的 for 循环中使用类型推断(auto),让编译器推断数组元素的类型。

使用 auto 的另一优势在于:若数组元素类型发生变更(例如从 int 变为 long),auto 将自动推断更新后的元素类型,确保类型保持同步(并避免类型转换发生)。


避免使用引用复制元素

考虑以下基于范围的 for 循环,该循环遍历一个 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';

    return 0;
}

image

在每次循环迭代中,words数组中的下一个std::string元素将被赋值(复制)到变量word中。复制std::string的开销较大,因此我们通常通过const引用将std::string传递给函数。除非确实需要副本,否则应避免复制耗费资源的对象。本例中,我们仅打印副本值后便销毁副本。若能直接引用数组原元素而非复制,将更为理想。

幸运的是,通过将元素声明改为(const)引用即可实现此目标:

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

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

    for (const auto& word : words) // word is now a const reference
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

image

在上例中,word 现已成为 const 引用。循环每次迭代时,word 将绑定到数组的下一个元素。这使我们能够访问数组元素的值,而无需进行代价高昂的复制操作。

若引用为非 const 类型,还可用于修改数组中的值(若 element_declaration 是值的副本则无法实现此操作)。


何时使用 auto、auto& 与 const auto&

通常我们使用 auto 表示复制成本低的类型,使用 auto& 表示需要修改元素的情况,而 const auto& 则用于复制成本高的类型。但在基于范围的 for 循环中,许多开发者认为始终使用 const auto& 更佳,因为它更具未来兼容性。

例如,考虑以下示例:

#include <iostream>
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "peter", "likes", "frozen", "yogurt" }; // elements are type std::string_view

    for (auto word : words) // We normally pass string_view by value, so we'll use auto here
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

image

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

但请考虑如果后来将 words 更新为 std::string 数组会发生什么情况。

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

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" }; // obvious we should update this

    for (auto word : words) // Probably not obvious we should update this too
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

image

基于范围的 for 循环可以正常编译和执行,但 word 现在会被推断为 std::string。由于我们使用了 auto,循环会默默地对 std::string 元素进行昂贵的复制操作。这导致性能大幅下降!

有几种合理方法可避免此问题:

  • 在基于范围的 for 循环中避免类型推断。若显式将元素类型指定为 std::string_view,当数组后续更新为 std::string 时,std::string 元素将隐式转换为 std::string_view,这不会引发问题。若数组更新为不可转换的类型,编译器将报错,此时可明确采取应对措施。但若元素类型可转换,编译器将静默执行转换,导致我们可能未察觉操作效率低下。

  • 在基于范围的 for 循环中使用类型推导时,若不希望操作副本,请使用 const auto& 替代 auto。通过引用而非值访问元素的性能开销通常较小,但此举能避免后续元素类型变更(如改为复制成本高的类型)时可能引发的重大性能损失。

最佳实践:
对于基于范围的 for 循环,建议按以下方式定义元素类型:

  • 修改元素副本时:auto
  • 修改原始元素时:auto&
  • 其他情况(仅需查看原始元素):const auto&

基于范围的 for 循环及其他标准容器类型

基于范围的 for 循环适用于多种数组类型,包括(未衰减的)C 风格数组、std::array、std::vector、链表、树和映射。我们尚未讲解这些内容,因此不必担心不了解它们。只需记住:基于范围的 for 循环提供了一种灵活且通用的迭代方式,不仅限于遍历 std::vector:

#include <array>
#include <iostream>

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

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

    std::cout << '\n';

    return 0;
}

image

对于高级读者:
基于范围的 for 循环无法用于衰减的 C 风格数组。这是因为基于范围的 for 循环需要知道数组的长度才能确定遍历何时完成,而衰减的 C 风格数组不包含此信息。

基于范围的 for 循环同样无法处理枚举类型。我们在第 17.6 节——std::array 与枚举类型中展示了绕过此限制的方法。


获取当前元素的索引

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

不过,由于基于范围的 for 循环始终按顺序向前迭代且不会跳过元素,您可自行声明计数器并进行递增。但若选择此方案,建议考虑是否改用常规 for 循环更为合适。


基于范围的 for 循环中的反向迭代( C++20 )

基于范围的 for 循环仅支持正向迭代。但在某些场景下,我们需要以反向顺序遍历数组。在 C++20 之前,基于范围的 for 循环难以实现此功能,通常需要采用其他解决方案(如常规 for 循环)。

但自C++20起,可借助Ranges库的std::views::reverse功能创建可遍历元素的逆序视图:

#include <iostream>
#include <ranges> // C++20
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "Alex", "Bobby", "Chad", "Dave" }; // sorted in alphabetical order

    for (const auto& word : std::views::reverse(words)) // create a reverse view
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

这将输出:

image

我们尚未讲解范围库,因此暂且将此视为一段实用的魔法代码。

测验时间

问题 #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 的复制成本很低。

显示解决方案

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

int main()
{
    std::vector<std::string_view> names{ "Alex", "Betty", "Caroline", "Dave",
        "Emily", "Fred", "Greg", "Holly" };

    std::cout << "Enter a name: ";
    std::string username{};
    std::cin >> username;

    bool found{ false };

    // We will be explicit about expecting `name` to be a std::string_view here
    // That way if `names` is ever changed to an expensive to copy type
    // (like std::string), we won't end up making expensive copies.
    for (std::string_view name : names)
    {
        if (name == username)
        {
            found = true;
            break;
        }
    }

    if (found)
        std::cout << username << " was found.\n";
    else
        std::cout << username << " was not found.\n";

    return 0;
}

image


问题 #2

修改你对练习1的解决方案。在此版本中,创建一个名为isValueInArray()的函数模板(而非普通函数),该函数接受两个参数:一个std::vector和一个值。若该值存在于数组中,函数应返回true;否则返回false。在main()中调用该函数,并传入姓名数组和用户输入的姓名。

注意事项:

使用模板参数推导(即未显式指定模板类型参数)的函数模板不会进行类型转换以匹配模板参数。调用时要么完全匹配模板(此时模板类型可被推导),要么完全不匹配。

显式指定模板类型参数的函数模板会将参数转换为匹配的类型(因类型已知)。

显示解决方案

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

template <typename T>
bool isValueInArray(const std::vector<T>& arr, const T& value )
{
    // we'll use a reference since we don't know if T is expensive to copy
    for (const auto& a : arr)
    {
        if (a == value)
            return true;
    }

    return false;
}

int main()
{
    std::vector<std::string_view> names{ "Alex", "Betty", "Caroline", "Dave",
        "Emily", "Fred", "Greg", "Holly" };

    std::cout << "Enter a name: ";
    std::string username{};
    std::cin >> username;

    // By explicitly specifying std::string_view as a function template argument,
    // the compiler will implicitly convert username to `std::string_view` to match the parameter type.
    bool found{ isValueInArray<std::string_view>(names, username) };

    // The following is also okay.  If we rely on template argument deduction instead, the compiler
    // won't do implicit conversions, so we need to make sure `username` has the expected type.
    // bool found{ isValueInArray(names, static_cast<std::string_view>(username)) };

    if (found)
        std::cout << username << " was found.\n";
    else
        std::cout << username << " was not found.\n";

    return 0;
}

image

posted @ 2026-01-06 23:25  游翔  阅读(21)  评论(0)    收藏  举报