Menu

C++ 范围 for 循环:语法、auto 与引用

讲解 C++ 的范围 for 循环:简洁地遍历数组、vector、字符串和 map,为什么要用 auto& 和 const auto&,以及要避开的拷贝陷阱和迭代器失效陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

“对每个元素,做这件事”

经典的计数 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 循环。

Coddy 编程语言插图

和 Coddy 一起学编程

开始