C++(四十五):范围 for 循环

范围 for 循环

范围 for 循环(也称为 range-based for loop 或 for-each loop)是 C++11 标准引入的一种更简洁、更易读、更不易出错的遍历序列(如数组、容器等)的方式。它抽象掉了手动管理索引或迭代器的细节。

核心语法:

for ( declaration : range_expression ) {
    // 循环体 (loop body)
    // 在这里使用 declaration 定义的变量来访问当前元素
}

语法解析:

  1. declaration (声明):

    • 这部分声明一个变量,在每次循环迭代时,range_expression 中的一个元素会被用来初始化这个变量。
    • 常见的形式有:
      • type variable: 按值拷贝。每次迭代,range_expression 中的当前元素会被拷贝variable 中。如果在循环体内修改 variable不会影响到 range_expression 中的原始元素。适用于基本类型或需要副本的场景。
      • type& variable: 按引用variable 成为 range_expression 中当前元素的引用(别名)。这避免了拷贝开销(特别是对于大对象),并且如果在循环体内修改 variable会直接修改 range_expression 中的原始元素。
      • const type& variable: 按常量引用variable 成为 range_expression 中当前元素的常量引用。这既避免了拷贝开销,又防止在循环体内意外修改原始元素。通常是最推荐的方式,除非你确实需要修改元素或需要一个副本。
      • auto variable, auto& variable, const auto& variable: 使用 auto 关键字让编译器自动推断元素的类型。这是现代 C++ 中非常常用的做法,代码更简洁。auto 对应按值拷贝,auto& 对应按引用,const auto& 对应按常量引用。选择哪个取决于你是否需要拷贝、是否需要修改原始元素。
  2. : (冒号): 分隔符,用于分隔元素声明和要遍历的范围。

  3. range_expression (范围表达式):

    • 任何可以被迭代的序列。这通常意味着它需要提供 begin()end() 函数(可以是成员函数,也可以是通过参数依赖查找(ADL)找到的全局函数)。
    • 常见的例子包括:
      • 标准库容器(如 std::vector, std::list, std::map, std::string 等)。
      • C 风格数组(编译器知道其大小的情况下,例如在声明的作用域内)。
      • 初始化列表({1, 2, 3})。
      • 任何定义了 begin()end() 成员或非成员函数的自定义类型。
  4. loop body (循环体):

    • 在大括号 {} 内的代码块。
    • 在每次迭代中,declaration 中声明的变量会持有当前元素的值或引用,你可以在循环体内使用这个变量进行操作。

工作原理 (简化理解):

编译器大致会将范围 for 循环转换为使用迭代器的传统 for 循环。它会获取 range_expressionbegin()end() 迭代器,然后循环直到 begin 迭代器等于 end 迭代器,在每次循环中解引用 begin 迭代器 (*iterator) 来获取当前元素,并将其赋值(或绑定引用)给 declaration 中声明的变量,然后递增 begin 迭代器 (++iterator)。

优点:

  • 简洁性: 代码量更少,意图更清晰(“对范围中的每个元素做某事”)。
  • 可读性: 更易于阅读和理解,尤其是对于不熟悉迭代器复杂性的开发者。
  • 安全性: 避免了许多常见的与索引或迭代器相关的错误,如索引越界、迭代器失效(部分情况)等。

注意事项:

  • 没有直接索引: 范围 for 循环本身不提供当前元素的索引。如果需要索引,你需要自己维护一个计数器变量。
  • 修改元素: 如果你想在循环中修改容器/数组中的元素,必须使用引用(如 auto&type&)。
  • 拷贝开销: 如果元素类型较大且你不需要修改它,使用 const auto&const type& 可以避免不必要的拷贝开销。

示例代码:

#include <iostream>
#include <vector>
#include <string>
#include <map>

int main() {
    // 1. 遍历 std::vector
    std::vector<int> numbers = {10, 20, 30, 40, 50};
    std::cout << "Vector elements: ";
    for (const auto& num : numbers) { // 使用 const auto& 避免拷贝且不修改
        std::cout << num << " ";
        // num = 100; // 编译错误,因为是 const 引用
    }
    std::cout << std::endl;

    // 2. 遍历 C 风格数组 (在声明的作用域内)
    int c_array[] = {1, 2, 3, 4, 5};
    std::cout << "C-style array elements: ";
    for (int val : c_array) { // 按值拷贝 (对于 int 通常没问题)
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 3. 遍历并修改 std::vector 中的元素
    std::vector<std::string> fruits = {"apple", "banana", "cherry"};
    std::cout << "Original fruits: ";
    for(const auto& fruit : fruits) std::cout << fruit << " ";
    std::cout << std::endl;

    for (auto& fruit : fruits) { // 使用 auto& 允许修改
        fruit += "!"; // 在每个水果名称后添加感叹号
    }

    std::cout << "Modified fruits: ";
    for (const auto& fruit : fruits) { // 再次遍历查看修改结果
        std::cout << fruit << " ";
    }
    std::cout << std::endl;

    // 4. 遍历 std::map (元素是 std::pair)
    std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 92}};
    std::cout << "Scores:" << std::endl;
    for (const auto& pair : scores) { // pair 是 const std::pair<const std::string, int>& 类型
        // pair.first 是 key (const std::string&)
        // pair.second 是 value (int&) - 但因为 pair 是 const&,所以这里也是 const int&
        std::cout << "  " << pair.first << ": " << pair.second << std::endl;
        // pair.second = 100; // 编译错误,因为 pair 是 const 引用
    }
    
    // 如果要修改 map 的值
    for (auto& pair : scores) {
        pair.second += 5; // 给每个人的分数加 5
    }
     std::cout << "Updated Scores:" << std::endl;
    for (const auto& pair : scores) { 
        std::cout << "  " << pair.first << ": " << pair.second << std::endl;
    }

    // 5. 遍历初始化列表
    std::cout << "Initializer list: ";
    for (int x : {11, 22, 33}) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    return 0;
}

C++范围for循环介绍

范围for循环(Range-based for loop)是C++11引入的一种简化的循环语法,专门用于遍历容器或数组中的元素,无需手动管理索引或迭代器。

基本语法

范围for循环的基本语法如下:

for (声明变量 : 要遍历的容器或数组) {
    // 对每个元素进行操作
}

详细说明

基本用法

std::vector<int> numbers = {1, 2, 3, 4, 5};

// 按值遍历
for (int n : numbers) {
    std::cout << n << " "; // 输出: 1 2 3 4 5
}

// 按引用遍历(可修改元素)
for (int& n : numbers) {
    n *= 2; // 将每个元素乘以2
}

// 使用auto进行类型推导
for (auto n : numbers) {
    std::cout << n << " "; // 输出: 2 4 6 8 10
}

// 使用const引用(避免拷贝,但不修改元素)
for (const auto& n : numbers) {
    std::cout << n << " ";
}

适用范围

范围for循环可以用于:

  • 内置数组
  • STL容器(如vector, list, set, map等)
  • 任何定义了begin()end()方法的类
  • 任何可以用std::begin()std::end()函数处理的对象

示例:不同类型的容器

// 数组
int arr[] = {1, 2, 3, 4, 5};
for (const auto& element : arr) {
    std::cout << element << " ";
}

// 向量
std::vector<std::string> fruits = {"apple", "banana", "cherry"};
for (const auto& fruit : fruits) {
    std::cout << fruit << " ";
}

// 映射表
std::map<std::string, int> ages = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 35}};
for (const auto& pair : ages) {
    std::cout << pair.first << ": " << pair.second << std::endl;
}

底层工作原理

范围for循环在编译时会被转换为等效的传统for循环,大致如下:

// 这段代码:
for (auto element : container) {
    // 使用element
}

// 会被转换为大致相当于:
{
    auto&& __range = container;
    auto __begin = begin(__range);
    auto __end = end(__range);
    for (; __begin != __end; ++__begin) {
        auto element = *__begin;
        // 使用element
    }
}

优势与注意事项

优势:

  • 语法简洁,易读易写
  • 避免了常见的索引错误
  • 无需考虑容器内部结构

注意事项:

  • 默认情况下创建元素的副本,可能影响性能
  • 如果需要修改元素,必须使用引用
  • 不能直接获取元素的索引或位置
  • 不能在循环中安全地修改被遍历的容器大小

想象你有一个装满糖果的盒子,里面有红糖果、蓝糖果和绿糖果。你想要把每颗糖果拿出来看一看。

用范围for循环,我们可以这样做:

  1. 准备好盒子里的所有糖果
  2. 一次拿出一颗糖果
  3. 看一看这颗糖果
  4. 放回盒子,拿下一颗
  5. 一直重复,直到所有糖果都看过一遍

比如,如果盒子里有5颗糖果:一颗红的,两颗蓝的,两颗绿的。我们会:

  • 先拿出红糖果,看一看,放回去
  • 再拿出第一颗蓝糖果,看一看,放回去
  • 然后拿出第二颗蓝糖果,看一看,放回去
  • 接着拿出第一颗绿糖果,看一看,放回去
  • 最后拿出第二颗绿糖果,看一看,放回去

这样,我们就看过了盒子里的所有糖果,而且不需要数数"这是第几颗糖果"。

让我们画出范围for循环的工作方式:

盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
      ⬆️
    现在拿出这颗看看
    
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
       ➡️  ⬆️
      看完了  现在拿出这颗看看
    
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
           ➡️  ⬆️
         看完了  现在拿出这颗看看
    
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
               ➡️  ⬆️
            看完了  现在拿出这颗看看
    
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
                   ➡️  ⬆️
                看完了  最后一颗
    
盒子: [🔴] [🔵] [🔵] [🟢] [🟢]
                       ➡️
                    全部看完了

每次循环,我们自动拿出一颗糖果,而且不需要说"我要拿第1颗"、"我要拿第2颗"这样的话。

在编程中,我们使用范围for循环来简化遍历过程。

例如,假设我们有一个装满数字的"盒子"(数组):

int numbers[] = {10, 20, 30, 40, 50};

如果我们想查看每个数字,可以使用范围for循环:

for (int number : numbers) {
    cout << "我找到了数字: " << number << endl;
}

这段代码的意思是:

  1. 创建一个名为number的变量
  2. numbers盒子中,每次取出一个数字,放入number变量
  3. 执行大括号{}里的代码,显示这个数字
  4. 自动取出下一个数字,重复步骤2和3
  5. 直到所有数字都处理完毕

输出结果会是:

我找到了数字: 10
我找到了数字: 20
我找到了数字: 30
我找到了数字: 40
我找到了数字: 50

与传统的for循环相比:

  • 不需要创建计数器变量(int i = 0)
  • 不需要检查是否到达末尾(i < 5)
  • 不需要手动增加计数器(i++)
  • 不需要使用索引访问元素(numbers[i])

范围for循环就像是一个魔法盒子,它会自动取出每一个元素,让我们可以专注于处理这些元素,而不用担心如何取出它们。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容