“对每个元素,做这件事”
经典的计数 for 循环在你需要用下标驱动时很好用。但大多数时候你根本不关心下标,只想处理容器里的每一个元素。为此写 for (int i = 0; i < v.size(); i++) 既啰嗦,i 又离差一错误导致的越界读取只有一步之遥。
C++11 正是为此加入了范围 for 循环。你给出一个变量名,指向一个容器,循环就会替你走遍每个元素:
没有下标,没有 .size(),也没有会写错的边界。把它读作“对 scores 中的每个 s”。它适用于原生数组、std::vector、std::string、std::map,以及任何提供 begin() 和 end() 的东西。
让 auto 来选类型
把元素类型写出来也行,但不够灵活:容器的类型一变,每个循环都得跟着改。把范围 for 和 auto 搭配起来,编译器就会替你推导元素类型:
不过这里有个隐藏的代价。普通的 auto name 推导出 string,并在每一轮都把元素拷贝到 name 中。对 int 来说这不花什么;对 string 或一个大结构体来说,每次迭代都是一次白白浪费的内存分配。解决办法是引用,这也是接下来要弄懂的内容。
用 auto& 原地修改
如果你写 auto x,得到的是一份拷贝,所以给 x 赋值改的是拷贝,而不是容器。看看这个陷阱:
乘以 10 的操作悄无声息地什么都没做,因为 n 只是一份用完即弃的拷贝。要真正修改元素,就用 auto& 按引用取得它们:
一个 & 就是“只看不碰”和“原地修改”的全部区别。如果你哪天纳闷自己的修改为什么不见了,原因几乎总是这个。
只读不拷贝:const auto&
当你只需要读取元素,而它们拷贝起来又很昂贵时,用 const auto&。引用避免了拷贝,const 则写明(并强制保证)你不会修改任何东西:
一条好用的经验法则:
for (auto x : c) // copy - cheap types (int, char, pointers)
for (auto& x : c) // edit - you want to change the elements
for (const auto& x : c) // read - heavy types you only inspect
读取时默认用 const auto&,写入时用 auto&。只有对真正小巧、拷贝廉价的类型,才用普通的 auto。
遍历 map 和 pair
对 std::map 使用范围 for,每个条目会以一个 std::pair 交给你,其中 .first 是键,.second 是值。从 C++17 开始,结构化绑定让你可以直接在循环头里把这个 pair 拆成两个有名字的变量:
[name, age] 比到处重复写 entry.first 和 entry.second 清楚得多。这里同样保留 const auto&:map 条目的键是 string,拷贝每个 pair 都是浪费。
陷阱:循环时不要改变大小
最大的坑是在范围 for 遍历容器的同时改变它的大小。调用 push_back、erase、insert 或 clear 可能会重新分配底层存储,让循环内部的迭代器失效,结果是未定义行为,也就是崩溃或垃圾数据,而不是一条友好的错误信息:
vector<int> v = {1, 2, 3};
for (int x : v) {
v.push_back(x); // UNDEFINED BEHAVIOR - reallocation invalidates the range
}
如果处理过程中需要添加或删除元素,就改用基于下标或基于迭代器的 for 循环,自己管理边界;或者另建一个结果容器,处理完再交换进来。同一类里还有两个小坑:永远不要让范围 for 绑定到一个马上就会销毁的临时对象(for (auto x : makeVector()) 没问题,但 for (auto& x : someObj.getTempVector()) 可能悬空);另外记住,for (auto& c : myString) 可以让你原地修改单个字符。
下一节:函数
范围 for 循环让遍历变得整洁,而你刚学到的 auto / auto& / const auto& 这些选择,可以直接用到 C++ 最重要的工具之一上。接下来我们会把逻辑封装成可复用的函数:给代码起个名字,加上参数和返回值,这样你就能在任何地方调用它,而不必重复编写。
常见问题
C++ 中的范围 for 循环是什么?
范围 for 循环会访问容器(数组、vector、string、map 等)中的每一个元素,不需要你自己管理下标或迭代器。语法是 for (auto x : container) { ... }。它在 C++11 中加入,是表达“对每个元素都做这件事”最简洁的写法。
范围 for 循环中什么时候该用 auto& 而不是 auto?
想要原地修改元素时用 auto& x;只需要读取元素又想避免拷贝时用 const auto& x(对 string、vector 或大对象很重要)。普通的 auto x 每次迭代都会拷贝一份:对 int 这样廉价的类型没问题,其他情况就是浪费。
C++ 中可以在范围 for 循环里改变 vector 的大小吗?
不可以。对正在遍历的容器调用 push_back、erase、insert 或 clear,会让循环内部的迭代器失效,这是未定义行为:可能崩溃,也可能悄悄破坏数据。如果需要在循环时添加或删除元素,请改用基于下标或基于迭代器的 for 循环。