在课程《数组与循环》中,我们展示了如何利用 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::vector
、std::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
此时循环仍能编译运行,却会默默产生拷贝开销!
两种避免方案:
- 明确指定元素类型为
std::string_view
,以后若改为不可转换类型则编译报错;若可转换则静默转换,但可能效率不佳。 - 在基于范围的 for 循环中使用
const auto&
,即使元素类型变为昂贵类型,也不会产生拷贝。
最佳实践
基于范围的 for 循环中,按以下规则声明元素:
- 要修改副本:
auto
- 要修改原元素:
auto&
- 其余情况(只读):
const auto&
与其他标准容器配合
基于范围的 for 循环适用于多种数组类型:非退化 C 风格数组、std::array
、std::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()
调用该模板。
提醒:
- 使用模板实参推导时,不会执行类型转换以匹配模板形参;
- 显式指定模板实参时,会执行类型转换。
[显示解答]