范围 for 循环
范围 for
循环(也称为 range-based for loop 或 for-each loop)是 C++11 标准引入的一种更简洁、更易读、更不易出错的遍历序列(如数组、容器等)的方式。它抽象掉了手动管理索引或迭代器的细节。
核心语法:
for ( declaration : range_expression ) {
// 循环体 (loop body)
// 在这里使用 declaration 定义的变量来访问当前元素
}
语法解析:
-
declaration
(声明):- 这部分声明一个变量,在每次循环迭代时,
range_expression
中的一个元素会被用来初始化这个变量。 - 常见的形式有:
-
type variable
: 按值拷贝。每次迭代,range_expression
中的当前元素会被拷贝到variable
中。如果在循环体内修改variable
,不会影响到range_expression
中的原始元素。适用于基本类型或需要副本的场景。 -
type& variable
: 按引用。variable
成为range_expression
中当前元素的引用(别名)。这避免了拷贝开销(特别是对于大对象),并且如果在循环体内修改variable
,会直接修改range_expression
中的原始元素。 -
const type& variable
: 按常量引用。variable
成为range_expression
中当前元素的常量引用。这既避免了拷贝开销,又防止在循环体内意外修改原始元素。通常是最推荐的方式,除非你确实需要修改元素或需要一个副本。 -
auto variable
,auto& variable
,const auto& variable
: 使用auto
关键字让编译器自动推断元素的类型。这是现代 C++ 中非常常用的做法,代码更简洁。auto
对应按值拷贝,auto&
对应按引用,const auto&
对应按常量引用。选择哪个取决于你是否需要拷贝、是否需要修改原始元素。
-
- 这部分声明一个变量,在每次循环迭代时,
:
(冒号): 分隔符,用于分隔元素声明和要遍历的范围。-
range_expression
(范围表达式):- 任何可以被迭代的序列。这通常意味着它需要提供
begin()
和end()
函数(可以是成员函数,也可以是通过参数依赖查找(ADL)找到的全局函数)。 - 常见的例子包括:
- 标准库容器(如
std::vector
,std::list
,std::map
,std::string
等)。 - C 风格数组(编译器知道其大小的情况下,例如在声明的作用域内)。
- 初始化列表(
{1, 2, 3}
)。 - 任何定义了
begin()
和end()
成员或非成员函数的自定义类型。
- 标准库容器(如
- 任何可以被迭代的序列。这通常意味着它需要提供
-
loop body
(循环体):- 在大括号
{}
内的代码块。 - 在每次迭代中,
declaration
中声明的变量会持有当前元素的值或引用,你可以在循环体内使用这个变量进行操作。
- 在大括号
工作原理 (简化理解):
编译器大致会将范围 for
循环转换为使用迭代器的传统 for
循环。它会获取 range_expression
的 begin()
和 end()
迭代器,然后循环直到 begin
迭代器等于 end
迭代器,在每次循环中解引用 begin
迭代器 (*iterator
) 来获取当前元素,并将其赋值(或绑定引用)给 declaration
中声明的变量,然后递增 begin
迭代器 (++iterator
)。
优点:
- 简洁性: 代码量更少,意图更清晰(“对范围中的每个元素做某事”)。
- 可读性: 更易于阅读和理解,尤其是对于不熟悉迭代器复杂性的开发者。
- 安全性: 避免了许多常见的与索引或迭代器相关的错误,如索引越界、迭代器失效(部分情况)等。
注意事项:
-
没有直接索引: 范围
for
循环本身不提供当前元素的索引。如果需要索引,你需要自己维护一个计数器变量。 -
修改元素: 如果你想在循环中修改容器/数组中的元素,必须使用引用(如
auto&
或type&
)。 -
拷贝开销: 如果元素类型较大且你不需要修改它,使用
const auto&
或const type&
可以避免不必要的拷贝开销。
示例代码:
#include <iostream>
#include <vector>
#include <string>
#include <map>
int main() {
// 1. 遍历 std::vector
std::vector<int> numbers = {10, 20, 30, 40, 50};
std::cout << "Vector elements: ";
for (const auto& num : numbers) { // 使用 const auto& 避免拷贝且不修改
std::cout << num << " ";
// num = 100; // 编译错误,因为是 const 引用
}
std::cout << std::endl;
// 2. 遍历 C 风格数组 (在声明的作用域内)
int c_array[] = {1, 2, 3, 4, 5};
std::cout << "C-style array elements: ";
for (int val : c_array) { // 按值拷贝 (对于 int 通常没问题)
std::cout << val << " ";
}
std::cout << std::endl;
// 3. 遍历并修改 std::vector 中的元素
std::vector<std::string> fruits = {"apple", "banana", "cherry"};
std::cout << "Original fruits: ";
for(const auto& fruit : fruits) std::cout << fruit << " ";
std::cout << std::endl;
for (auto& fruit : fruits) { // 使用 auto& 允许修改
fruit += "!"; // 在每个水果名称后添加感叹号
}
std::cout << "Modified fruits: ";
for (const auto& fruit : fruits) { // 再次遍历查看修改结果
std::cout << fruit << " ";
}
std::cout << std::endl;
// 4. 遍历 std::map (元素是 std::pair)
std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 92}};
std::cout << "Scores:" << std::endl;
for (const auto& pair : scores) { // pair 是 const std::pair<const std::string, int>& 类型
// pair.first 是 key (const std::string&)
// pair.second 是 value (int&) - 但因为 pair 是 const&,所以这里也是 const int&
std::cout << " " << pair.first << ": " << pair.second << std::endl;
// pair.second = 100; // 编译错误,因为 pair 是 const 引用
}
// 如果要修改 map 的值
for (auto& pair : scores) {
pair.second += 5; // 给每个人的分数加 5
}
std::cout << "Updated Scores:" << std::endl;
for (const auto& pair : scores) {
std::cout << " " << pair.first << ": " << pair.second << std::endl;
}
// 5. 遍历初始化列表
std::cout << "Initializer list: ";
for (int x : {11, 22, 33}) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
C++范围for循环介绍
范围for循环(Range-based for loop)是C++11引入的一种简化的循环语法,专门用于遍历容器或数组中的元素,无需手动管理索引或迭代器。
基本语法
范围for循环的基本语法如下:
for (声明变量 : 要遍历的容器或数组) {
// 对每个元素进行操作
}
详细说明
基本用法
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 按值遍历
for (int n : numbers) {
std::cout << n << " "; // 输出: 1 2 3 4 5
}
// 按引用遍历(可修改元素)
for (int& n : numbers) {
n *= 2; // 将每个元素乘以2
}
// 使用auto进行类型推导
for (auto n : numbers) {
std::cout << n << " "; // 输出: 2 4 6 8 10
}
// 使用const引用(避免拷贝,但不修改元素)
for (const auto& n : numbers) {
std::cout << n << " ";
}
适用范围
范围for循环可以用于:
- 内置数组
- STL容器(如vector, list, set, map等)
- 任何定义了
begin()
和end()
方法的类 - 任何可以用
std::begin()
和std::end()
函数处理的对象
示例:不同类型的容器
// 数组
int arr[] = {1, 2, 3, 4, 5};
for (const auto& element : arr) {
std::cout << element << " ";
}
// 向量
std::vector<std::string> fruits = {"apple", "banana", "cherry"};
for (const auto& fruit : fruits) {
std::cout << fruit << " ";
}
// 映射表
std::map<std::string, int> ages = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 35}};
for (const auto& pair : ages) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
底层工作原理
范围for循环在编译时会被转换为等效的传统for循环,大致如下:
// 这段代码:
for (auto element : container) {
// 使用element
}
// 会被转换为大致相当于:
{
auto&& __range = container;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
auto element = *__begin;
// 使用element
}
}
优势与注意事项
优势:
- 语法简洁,易读易写
- 避免了常见的索引错误
- 无需考虑容器内部结构
注意事项:
- 默认情况下创建元素的副本,可能影响性能
- 如果需要修改元素,必须使用引用
- 不能直接获取元素的索引或位置
- 不能在循环中安全地修改被遍历的容器大小
想象你有一个装满糖果的盒子,里面有红糖果、蓝糖果和绿糖果。你想要把每颗糖果拿出来看一看。
用范围for循环,我们可以这样做:
- 准备好盒子里的所有糖果
- 一次拿出一颗糖果
- 看一看这颗糖果
- 放回盒子,拿下一颗
- 一直重复,直到所有糖果都看过一遍
比如,如果盒子里有5颗糖果:一颗红的,两颗蓝的,两颗绿的。我们会:
- 先拿出红糖果,看一看,放回去
- 再拿出第一颗蓝糖果,看一看,放回去
- 然后拿出第二颗蓝糖果,看一看,放回去
- 接着拿出第一颗绿糖果,看一看,放回去
- 最后拿出第二颗绿糖果,看一看,放回去
这样,我们就看过了盒子里的所有糖果,而且不需要数数"这是第几颗糖果"。
让我们画出范围for循环的工作方式:
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
⬆️
现在拿出这颗看看
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
➡️ ⬆️
看完了 现在拿出这颗看看
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
➡️ ⬆️
看完了 现在拿出这颗看看
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
➡️ ⬆️
看完了 现在拿出这颗看看
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
➡️ ⬆️
看完了 最后一颗
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
➡️
全部看完了
每次循环,我们自动拿出一颗糖果,而且不需要说"我要拿第1颗"、"我要拿第2颗"这样的话。
在编程中,我们使用范围for循环来简化遍历过程。
例如,假设我们有一个装满数字的"盒子"(数组):
int numbers[] = {10, 20, 30, 40, 50};
如果我们想查看每个数字,可以使用范围for循环:
for (int number : numbers) {
cout << "我找到了数字: " << number << endl;
}
这段代码的意思是:
- 创建一个名为
number
的变量 - 从
numbers
盒子中,每次取出一个数字,放入number
变量 - 执行大括号
{}
里的代码,显示这个数字 - 自动取出下一个数字,重复步骤2和3
- 直到所有数字都处理完毕
输出结果会是:
我找到了数字: 10
我找到了数字: 20
我找到了数字: 30
我找到了数字: 40
我找到了数字: 50
与传统的for循环相比:
- 不需要创建计数器变量(
int i = 0
) - 不需要检查是否到达末尾(
i < 5
) - 不需要手动增加计数器(
i++
) - 不需要使用索引访问元素(
numbers[i]
)
范围for循环就像是一个魔法盒子,它会自动取出每一个元素,让我们可以专注于处理这些元素,而不用担心如何取出它们。