基于范围的 for 循环
定义与作用
基于范围的 for 循环(range-based for loop)是 C++11 引入的遍历语法糖,让开发者以简洁、安全的方式遍历容器或序列的每个元素,无需手动管理迭代器或下标。
for (auto& x : container) {
// 对 x 的操作直接作用于容器元素
}
编译器将上述语法等价展开为传统的 for 循环,使用 std::begin() 和 std::end() 获取迭代器边界。这意味着任何实现了 begin() / end() 的类型都可以被范围 for 遍历。
核心原理
编译器展开机制
关键展开细节:
// 用户写的代码
for (auto& x : container) {
std::cout << x;
}
// 编译器等价展开(简化版)
{
auto&& __range = container;
for (auto __begin = begin(__range), __end = end(__range);
__begin != __end; ++__begin) {
auto& x = *__begin;
std::cout << x;
}
}
编译器使用非成员 begin() 和 end()(通过 ADL 查找),以此兼容 C 风格数组——数组没有成员函数 .begin()。
适用范围
| 类型 | 条件 | 示例 |
|---|---|---|
| C 风格数组 | 编译期已知大小 | int arr[] = {1, 2, 3}; |
| STL 容器 | 有 begin() / end() | vector, map, list 等 |
std::initializer_list | 天然支持 | for (auto x : {1, 2, 3}) |
| 自定义类型 | 实现 begin() / end() | 自定义容器 |
| 字符串字面量 | 退化为 const char* 后不适用 | 若需要遍历字符用 std::string |
不可用于:
- 不完整类型(如
void*指向的数组) - 没有
begin()/end()且非 C 数组的类型 for循环内修改容器结构导致迭代器失效的场景
完整示例
示例一:飞翔科技员工信息遍历
场景说明:孔蓝管理产品部成员信息,使用不同遍历方式处理员工数据。
#include <iostream>
#include <vector>
#include <string>
#include <map>
struct Employee {
std::string name;
std::string title;
int age;
double salary;
};
int main() {
// 飞翔科技产品部员工
std::vector<Employee> team = {
{"孔蓝", "高级产品经理", 28, 32000.0},
{"赵鸣", "内容运营", 26, 18888.0},
{"孙鹤", "产品助理", 23, 8888.0},
};
std::cout << "===== 产品部名册 =====" << std::endl;
// ---- 方式1:const auto& —只读遍历,不拷贝 ----
std::cout << "\n[只读遍历 — const auto&]" << std::endl;
for (const auto& emp : team) {
std::cout << emp.name << " | " << emp.title
<< " | " << emp.age << " 岁"
<< " | ¥" << emp.salary << std::endl;
}
// ---- 方式2:auto& —需要修改元素 ----
std::cout << "\n[修改遍历 — auto&]" << std::endl;
for (auto& emp : team) {
emp.salary *= 1.10; // 全员涨薪 10%
}
for (const auto& emp : team) {
std::cout << emp.name << " 涨薪后: ¥" << emp.salary << std::endl;
}
// ---- 方式3:auto —值拷贝(不推荐用于大对象) ----
std::cout << "\n[值拷贝 — auto(仅适合小类型)]" << std::endl;
for (auto name_only : {"孔蓝", "赵鸣", "孙鹤"}) {
std::cout << " " << name_only << std::endl;
}
// ---- 方式4:C 风格数组 ----
std::cout << "\n[遍历 C 数组]" << std::endl;
int bonus[] = {3000, 2000, 1000};
for (const auto& b : bonus) {
std::cout << "奖金: ¥" << b << std::endl;
}
return 0;
}
预期输出:
===== 产品部名册 =====
[只读遍历 — const auto&]
孔蓝 | 高级产品经理 | 28 岁 | ¥32000
赵鸣 | 内容运营 | 26 岁 | ¥18888
孙鹤 | 产品助理 | 23 岁 | ¥8888
[修改遍历 — auto&]
孔蓝 涨薪后: ¥35200
赵鸣 涨薪后: ¥20776.8
孙鹤 涨薪后: ¥9776.8
[值拷贝 — auto(仅适合小类型)]
孔蓝
赵鸣
孙鹤
[遍历 C 数组]
奖金: ¥3000
奖金: ¥2000
奖金: ¥1000
逐段分析:
const auto&是遍历的最佳默认选择:不拷贝、不修改、类型自动推导。对vector<Employee>中的每个元素,emp是const Employee&,避免了深拷贝std::string成员的开销auto&用于需要修改容器内元素的场景——直接修改引用即修改原数据auto做值拷贝,仅适合int、double等小类型或initializer_list遍历;对大对象会触发不必要的拷贝构造- C 风格数组 直接支持范围 for,因为编译器展开时调用的是非成员
begin(arr)/end(arr)
示例二:自定义类型实现 begin/end 以支持范围 for
场景说明:白歌为飞翔科技内部框架实现了一个简单的环形缓冲区,希望通过范围 for 遍历。
#include <iostream>
#include <cstddef>
// 简易环形缓冲区
template <typename T, std::size_t N>
class RingBuffer {
public:
void push(const T& value) {
data_[write_pos_] = value;
write_pos_ = (write_pos_ + 1) % N;
if (full_) {
read_pos_ = (read_pos_ + 1) % N;
}
full_ = (write_pos_ == read_pos_);
}
// 迭代器——让范围 for 能遍历
class Iterator {
public:
Iterator(const T* data, std::size_t pos, std::size_t start, std::size_t count)
: data_(data), pos_(pos), start_(start), count_(count) {}
const T& operator*() const { return data_[pos_]; }
Iterator& operator++() {
pos_ = (pos_ + 1) % N;
++count_;
return *this;
}
bool operator!=(const Iterator& other) const {
return count_ != other.count_;
}
private:
const T* data_;
std::size_t pos_;
std::size_t start_;
std::size_t count_;
};
Iterator begin() const {
return Iterator(data_, read_pos_, read_pos_, 0);
}
Iterator end() const {
std::size_t total = full_ ? N : write_pos_;
return Iterator(data_, write_pos_, read_pos_, total);
}
private:
T data_[N]{};
std::size_t read_pos_ = 0;
std::size_t write_pos_ = 0;
bool full_ = false;
};
int main() {
RingBuffer<int, 5> rb;
// 推送数据
for (int i = 1; i <= 7; ++i) {
rb.push(i * 10);
}
std::cout << "环形缓冲区遍历(容量5,推入7个):" << std::endl;
for (const auto& val : rb) {
std::cout << " " << val;
}
std::cout << std::endl;
std::cout << "(只保留最后 5 个:30 40 50 60 70)" << std::endl;
return 0;
}
预期输出:
环形缓冲区遍历(容量5,推入7个):
30 40 50 60 70
(只保留最后 5 个:30 40 50 60 70)
逐段分析:
- 要让自定义类型支持范围 for,只需提供
begin()和end()成员函数(或同名非成员函数通过 ADL) begin()返回指向第一个元素的迭代器,end()返回指向"末尾之后"的哨兵- 迭代器必须实现
operator*(解引用)、operator++(前进)和operator!=(比较) - 编译器对
for (const auto& val : rb)的展开等价于auto __begin = rb.begin(); auto __end = rb.end(); for (; __begin != __end; ++__begin) { ... }
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 遍历中修改容器(增删元素) | 迭代器失效,未定义行为 | 先收集要操作的元素,遍历后再修改 |
用 auto 遍历大对象 | 每次迭代触发拷贝构造 | 使用 const auto& 或 auto& |
用 auto& 绑定到临时对象 | 悬垂引用 | 确保被遍历的容器生命周期覆盖循环 |
遍历 initializer_list 时试图修改 | initializer_list 元素是 const | 用 auto 或 const auto& |
遍历 vector<bool> | vector<bool> 返回代理对象而非 bool& | 使用 auto&& 或显式类型 |
常见面试问题
范围 for 的底层展开原理是什么?——编译器展开为传统 for 循环,调用非成员
begin()/end()获取迭代器。对于 C 数组,标准库提供了begin(T(&arr)[N])和end(T(&arr)[N])重载。为什么展开使用非成员 begin/end 而非成员函数?——为了兼容 C 风格数组(数组没有成员函数)。非成员函数通过 ADL 查找,既能找到成员
begin()(被 ADL 找到)也能找到标准库的数组重载。auto、auto&、const auto&、auto&&在范围 for 中如何选择?——const auto&是只读遍历最佳默认;auto&用于修改元素;auto用于小类型(int/double);auto&&用于代理迭代器(如vector<bool>)。范围 for 对比传统 for 的优势?——消除手动边界错误(越界),代码意图清晰,自动推导类型,减少样板代码。代价是灵活性降低(不能跳过元素、不能从中间开始)。
为什么遍历中增删容器元素是危险的?——增删操作可能导致底层存储重新分配或元素移动,使展开代码中的
__begin/__end迭代器失效——后续的++__begin和*__begin变为未定义行为。
小结
- 范围 for 是语法糖,编译器展开为传统 for +
begin()/end() - 适用范围:C 数组、STL 容器、
initializer_list、任何实现了begin()/end()的类型 - 遍历方式选择:
const auto&(只读默认)→auto&(修改)→auto(小类型)→auto&&(代理迭代器) - 遍历期间禁止在容器中增删元素——迭代器失效
- 自定义类型只需提供
begin()/end()即可无缝支持范围 for