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

尽管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;
}
这将输出:

请注意,这个示例既不需要我们使用数组的长度,也不需要我们对数组进行索引!
让我们深入解析其工作原理。这个基于范围的 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;
}

上述示例不会输出任何内容。抱歉啦,妈妈!
基于范围的 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;
}

由于 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;
}

在每次循环迭代中,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;
}

在上例中,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;
}

在此示例中,我们有一个包含 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;
}

基于范围的 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;
}

对于高级读者:
基于范围的 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;
}
这将输出:

我们尚未讲解范围库,因此暂且将此视为一段实用的魔法代码。
测验时间
问题 #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;
}

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


浙公网安备 33010602011771号