范围 for 循环的坑与正确姿势
范围 for 是 C++11 最受欢迎的语法之一,但它把迭代器的细节藏起来了,于是"藏起来的问题"变成"看不见的 bug"。本文把常见坑逐个拆开。
一、语法展开
for (decl : range) statement;
标准等价展开(简化版):
{
auto&& __range = range;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
decl = *__begin;
statement;
}
}
关键点:__range 是引用绑定,所以 range 的生命周期会被延长到循环结束;__end 只在开始时计算一次。
二、坑 1:遍历时修改容器
std::vector<int> v{1, 2, 3};
for (int x : v) {
if (x == 2) v.push_back(4); // ❌ 迭代器失效,未定义行为
}
vector 扩容会重新分配,__begin/__end 全部失效。安全做法是先收集再修改,或用"下标 + 显式步进":
for (std::size_t i = 0; i < v.size(); ++i) {
if (v[i] == 2) v.push_back(4); // 每次重新取 size(),可控
}
对 std::list/std::map,删除当前元素是安全的(用 it = c.erase(it)),但范围 for 拿不到迭代器,仍需手写循环。
三、坑 2:按值遍历导致无意义拷贝
std::vector<std::string> v{"a", "b", "c"};
for (auto s : v) { // ❌ 每个元素一次拷贝
std::cout << s << "\n";
}
for (const auto& s : v) { // ✅ 零拷贝只读
std::cout << s << "\n";
}
for (auto& s : v) { s += "!"; } // ✅ 需要修改时
对 int、double、裸指针等小类型,按值反而更高效(避免间接寻址),不必强求 const&。
四、坑 3:临时对象的隐藏生命周期
for (auto& x : getVector()) { ... } // ✅ 生命周期延长,合法
for (auto&& x : makeStrings()) { ... } // ✅ 万能引用绑定临时量
auto&& r = getVector(); // r 生命周期被延长
但下面这种不延长:
for (auto& x : getVector().front()) { ... } // front() 返回元素引用,容器本体已销毁
判断方法:__range 绑定的表达式应当是"整个容器",而不是容器内部元素的引用。
五、坑 4:C 数组与指针退化
int arr[5] = {1,2,3,4,5};
for (int x : arr) {} // ✅ ok,数组类型可推导长度
int* p = arr;
for (int x : p) {} // ❌ 编译错误:指针没有 begin/end
老代码里 sizeof(arr)/sizeof(arr[0]) 的写法,只要数组没退化成指针就可以安全替换为范围 for。
六、坑 5:遍历自定义类型的私有 begin/end
范围 for 通过 ADL 查找 begin/end。如果你的容器只有成员函数 begin()/end(),成员版本会被优先找到;若两者都没有,就会编译失败。
class Range {
public:
int* begin() { return data_; }
int* end() { return data_ + n_; }
private:
int data_[10] = {};
std::size_t n_ = 10;
};
若要支持 const 对象遍历,需要同时提供 const 重载与 cbegin/cend。
七、坑 6:structured bindings 下的拷贝(C++17)
C++17 可以这样写:
for (const auto& [key, value] : m) {
std::cout << key << "=" << value << "\n";
}
若写成 for (auto [key, value] : m),key/value 各拷贝一次,遍历 map<string, BigStruct> 时开销显著。
八、正确姿势汇总
| 场景 | 写法 |
|---|---|
| 只读遍历,元素较大 | for (const auto& x : c) |
| 只读遍历,元素为内置小类型 | for (auto x : c) |
| 需要修改元素 | for (auto& x : c) |
| 遍历临时容器或需要转发 | for (auto&& x : c) |
| 遍历时增删元素 | 手写迭代器循环,重新取 end() |
| 只遍历部分区间或需要跳过 | 手写迭代器循环 |
九、与其他循环的取舍
- 需要索引:手写
for (std::size_t i = 0; ...),不要为了范围 for 额外维护计数器; - 需要反向遍历:用
rbegin()/rend(); - C++20:
std::ranges::views::reverse等适配器可让范围 for 直接处理逆序、过滤、变换; - 性能敏感路径:确认元素类型大小与是否内联,必要时看反汇编或 benchmark。
范围 for 不解决"逻辑复杂度",只消除"样板代码"。逻辑复杂时,老老实实写迭代器更清楚。