3. 范围 for 循环:语法详解、与普通 for 循环的性能对比、自定义类型如何支持范围 for、在 STL 容器中的应用
范围 for 循环,是 C++11 引入的一个“语法糖”。说白了,就是让你遍历容器时少写点代码。我第一次见到它时,心里想的是:“就这?不就是个 foreach 吗?” 但用久了才发现,这玩意儿背后藏着不少门道。
咱们今天就把范围 for 循环掰开揉碎了讲。从语法到性能,从 STL 到自定义类型,一个都不落下。
3.1 语法详解:从“怎么用”到“为什么这么用”
先看最基础的用法。假设你有一个 std::vector<int>,想打印每个元素:
std::vector<int> vec = {1, 2, 3, 4, 5};
// 传统写法
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 范围 for 写法
for (int x : vec) {
std::cout << x << " ";
}
简洁多了,对吧?但这里有个细节——int x 是按值拷贝的。如果你遍历的是大型对象,比如 std::string,每次拷贝的开销可不小。
我个人习惯这样用:
- 只读遍历:用
const auto&,避免拷贝 - 修改元素:用
auto&,直接引用 - 小类型或只想拷贝:用
auto或具体类型
std::vector<std::string> words = {"hello", "world"};
// 只读,避免拷贝
for (const auto& w : words) {
std::cout << w << " ";
}
// 修改元素
for (auto& w : words) {
w += "!";
}
const auto& 是最安全的选择。它既避免了拷贝,又防止了意外修改。
3.2 与普通 for 循环的性能对比
很多初学者会问:“范围 for 循环是不是比普通 for 循环慢?” 答案是:在大多数情况下,两者性能完全一样。
为什么?因为编译器会把范围 for 循环展开成迭代器循环。你想想看,它本质上就是个语法糖,底层还是 begin() 和 end() 那套东西。
我曾在项目中做过一个测试,遍历一个包含 1000 万个整数的 std::vector:
| 循环方式 | 耗时(ms) | 说明 |
|---|---|---|
| 普通 for + 下标 | 42 | for (size_t i = 0; i < v.size(); ++i) |
| 普通 for + 迭代器 | 42 | for (auto it = v.begin(); it != v.end(); ++it) |
| 范围 for | 42 | for (int x : v) |
| 范围 for + auto& | 42 | for (auto& x : v) |
结果一目了然——在开启优化(-O2)的情况下,性能几乎没有差别。
end() 迭代器。这意味着如果你在循环体内修改了容器(比如插入或删除元素),迭代器可能会失效,导致未定义行为。我曾经在代码审查时见过这样的 bug,排查起来相当头疼。
3.3 自定义类型如何支持范围 for
想让自己的类支持范围 for 循环?其实很简单。你只需要提供 begin() 和 end() 成员函数,或者提供自由函数 begin() 和 end()。
来看一个例子。假设你有一个简单的数组包装类:
class IntArray {
int* data_;
size_t size_;
public:
IntArray(int* data, size_t size) : data_(data), size_(size) {}
// 需要提供 begin 和 end
int* begin() { return data_; }
int* end() { return data_ + size_; }
// const 版本
const int* begin() const { return data_; }
const int* end() const { return data_ + size_; }
};
// 使用
int arr[] = {1, 2, 3, 4, 5};
IntArray myArr(arr, 5);
for (int x : myArr) {
std::cout << x << " ";
}
这里的关键是:begin() 和 end() 返回的迭代器必须支持 operator!=、operator++ 和 operator*。对于指针来说,这些操作都是原生支持的。
如果你的迭代器更复杂,比如是一个自定义的类,那就需要实现这些运算符了。嗯,这里要注意:迭代器类型必须支持前向迭代,至少是 InputIterator 或 ForwardIterator 的语义。
begin() 和 end(),然后不断递增迭代器直到与 end() 相等。只要你的类型满足这个契约,就能用范围 for。
3.4 在 STL 容器中的应用
STL 里几乎所有容器都支持范围 for。我列几个常用的:
- 顺序容器:
vector、deque、list、forward_list、array - 关联容器:
set、multiset、map、multimap - 无序容器:
unordered_set、unordered_map - 适配器:
stack、queue、priority_queue不支持范围 for
对于 map 和 unordered_map,遍历时得到的是 std::pair:
std::map<std::string, int> scores = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 95}
};
for (const auto& [name, score] : scores) { // C++17 结构化绑定
std::cout << name << ": " << score << "\n";
}
这里用了 C++17 的结构化绑定,让代码更清晰。如果你还在用 C++11/14,可以这样写:
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << "\n";
}
我个人更推荐用结构化绑定的写法,可读性高很多。
3.5 知识体系总览
下面这张图总结了范围 for 循环的核心知识点:
3.6 避坑指南
最后,分享几个我踩过的坑:
- 不要在范围 for 里修改容器大小:比如在遍历
vector时push_back,会导致迭代器失效。我曾经在代码里这么干过,结果程序随机崩溃,查了两天才找到原因。 - 注意
auto的推导:for (auto x : container)会拷贝元素。如果元素是std::vector<std::string>这种大家伙,性能会很难看。 - 初始化列表的陷阱:
for (int x : {1, 2, 3})是合法的,但for (auto x : {1, 2, 3})在 C++11 中会推导为std::initializer_list<int>,行为符合预期。
范围 for 循环是个好东西,用好了能让代码更简洁、更安全。但记住,它不是什么银弹——该用传统 for 循环的时候,也别硬往上套。
公众号:蓝海资料掘金营,微信deep3321