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 += "!";
}
小提示:如果你在范围 for 里不需要修改元素,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)的情况下,性能几乎没有差别。

注意:范围 for 循环有一个“坑”——它会在循环开始时缓存 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 的语义。

核心要点:范围 for 循环本质上就是调用 begin()end(),然后不断递增迭代器直到与 end() 相等。只要你的类型满足这个契约,就能用范围 for。

3.4 在 STL 容器中的应用

STL 里几乎所有容器都支持范围 for。我列几个常用的:

  • 顺序容器vectordequelistforward_listarray
  • 关联容器setmultisetmapmultimap
  • 无序容器unordered_setunordered_map
  • 适配器stackqueuepriority_queue 不支持范围 for

对于 mapunordered_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 循环的核心知识点:

范围 for 循环 语法详解 • 按值拷贝 vs 引用 • const auto& 最佳实践 • 结构化绑定 (C++17) 性能对比 • 与普通 for 性能相同 • 编译器展开为迭代器 • 注意迭代器失效 自定义类型支持 • 实现 begin()/end() • 迭代器运算符重载 • 自由函数版本 STL 容器应用 • vector, list, deque • map, set, unordered_map • 适配器不支持 注意事项 • 循环内不要修改容器 • 避免不必要的拷贝 • 优先用 const auto&

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要在范围 for 里修改容器大小:比如在遍历 vectorpush_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