CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章C++惯用法与ModernC++篇

    • 1.1 C++ RAII惯用法
    • 1.2 pimpl 惯用法
    • 1.3 拥抱C++新变化(C++11/14/17新增的实用特性)
    • 1.4 统一的类成员初始化语法与 std::initializer_list<T>
    • 1.5 C++17注解标签(attributes)
    • 1.6 final/override/=default/=delete语法
    • 1.7 auto关键字的前尘后事
    • 1.8 Range-based 循环语法
      • 1.9 C++17结构化绑定
      • 1.10 stl容器新增的实用方法介绍
      • 1.11 stl中的智能指针类详解
    • 第2章C++开发工具与调试进阶

    • 第3章C++多线程编程从入门到进阶

    • 第4章C++网络编程重难点解析

    • 第5章网络通信故障排查常用命令

    • 第6章网络通信协议设计

    • 第7章高性能服务结构设计

    • 第8章Redis 网络通信模块源码分析

    • 第9章 服务其他模块设计

    • C++后端开发进阶
    • 第1章C++惯用法与ModernC++篇
    zhangxf
    2023-04-03
    目录

    1.8 Range-based 循环语法

    # 1.8.1 Range-based循环语法介绍

    大多数语言都支持for-each语法遍历一个数组或集合中的元素,C++ 11中才支持这种语法,可谓姗姗来迟。在C++ 98/03规范中,对于一个数组int arr[10],如果我们想要遍历这个数组,只能使用递增的计数去引用数组中每个元素:

    int arr[10] = {0};
    for (int i = 0; i < 10; ++i)
    {
    	std::cout << arr[i] << std::endl;
    }
    
    1
    2
    3
    4
    5

    在C++ 11规范中有了for-each语法,我们可以这么写:

    int arr[10] = {0};
    for (int i : arr)
    {
    	std::cout << i << std::endl;
    }
    
    1
    2
    3
    4
    5

    对于上面auto关键字章节遍历std::map,我们也可以使用这种语法:

    std::map<std::string, std::string> seasons;
    seasons["spring"] = "123";
    seasons["summer"] = "456";
    seasons["autumn"] = "789";
    seasons["winter"] = "101112";
    
    for (auto iter : seasons)
    {
    	std::cout << iter.second << std::endl;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    for-each语法虽然很强大,但是有两个需要注意的地方:

    • for-each中的迭代器类型与数组或集合中的元素的类型完全一致,而原来使用老式语法迭代stl容器(如 std::map)时,迭代器是类型的取地址类型。因此,在上面的例子中,老式语法中,iter是一个指针类型(std::pair<std::string, std::string>*),使用iter->second去引用键值;而在for-each语法中,iter是数据类型(std::pair<std::string, std::string>),使用iter.second直接引用键值。

    • for-each语法中对于复杂数据类型,迭代器是原始数据的拷贝,而不是原始数据的引用。什么意思呢?我们来看一个例子:

      std::vector<std::string> v;
      v.push_back("zhangsan");
      v.push_back("lisi");
      v.push_back("maowu");
      v.push_back("maliu");
      for (auto iter : v)
      {
      	iter = "hello";
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      我们遍历容器v,意图将v中的元素的值都修改成“hello”,但是实际执行时我们却达不到我们想要的效果。这就是上文说的for-each中的迭代器是元素的拷贝,所以这里只是将每次拷贝修改成“hello”,原始数据并不会被修改。我们可以将迭代器修改成原始数据的引用:

      std::vector<std::string> v;
      v.push_back("zhangsan");
      v.push_back("lisi");
      v.push_back("maowu");
      v.push_back("maliu");
      for (auto& iter : v)
      {
      	iter = "hello";
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      这样我们就达到修改原始数据的目的了。这是使用for-each比较容易出错的地方,对于容器中是复杂数据类型的场景,我们应该尽量使用这种引用原始数据的方式,以避免复杂数据类型不必要的调用拷贝构造函数的开销。

      class A
      {
      public:
          A()
          {
          }
          ~A() = default;
      
          A(const A& rhs)
          {
          }
          
      public:
          int m;
      };
      
      int main()
      { 
          A a1;
          A a2;
          std::vector<A> v;
          v.push_back(a1);
          v.push_back(a2);
          for (auto iter : v)
          {
              //由于iter是v中的元素的拷贝,所以每一次循环,iter都会调用A的拷贝构造函数生成一份
              //实际使用for-each循环时应该尽量使用v中元素的引用,减少不必要的拷贝函数的调用开销
              iter.m = 9;
          }
          
          return 0;
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33

    # 1.8.2 自定义对象如何支持Range-based循环

    介绍了这么多,如何让我们自定义的对象支持Range-based循环语法呢?为了让一个对象支持这种语法,这个对象至少需要实现如下两个方法:

    //需要返回第一个迭代子的位置
    Iterator begin();
    //需要返回最后一个迭代子的下一个位置
    Iterator end();
    
    1
    2
    3
    4

    上面的Iterator是自定义数据类型的迭代子类型,这里的Iterator类型必须支持如下三种操作(原因下文会解释):

    • operator++ (即自增)操作,即可以自增之后返回下一个迭代子的位置;

    • operator != (即判不等操作)操作;

    • operator* 即解引用(dereference)操作。

    下面是一个自定义对象支持for-each循环的例子:

    #include <iostream>
    #include <string>
    
    template<typename T, size_t N>
    class A
    {
    public:
        A()
        {
            for (size_t i = 0; i < N; ++i)
            {
                m_elements[i] = i;
            }
        }
    
        ~A()
        {
    
        }
    
        T* begin()
        {
            return m_elements + 0;
        }
    
        T* end()
        {
            return m_elements + N;
        }
    
    private:
        T       m_elements[N];
    };
    
    int main()
    {
        A<int, 10> a;
        for (auto iter : a)
        {
            std::cout << iter << std::endl;
        }
        
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44

    注意:上述代码中,迭代子Iterator是 T*,这是指针类型,本身就支持operator ++和operator !=操作,所以这里并没有提供这两个方法的实现。那么为什么迭代子要支持operator ++和operator !=操作呢?我们来看一下编译器是如何实现这种for-each循环的。

    # 1.8.3 for-each循环的实现原理

    上述for-each循环可抽象成如下公式:

    for (for-range-declaration : for-range-initializer)
    	statement;
    
    1
    2

    C++14标准是这样解释上面的公式的:

    auto && __range = for-range-initializer;
    for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin )
    {
        for-range-declaration = *__begin;
        statement;
    }
    
    1
    2
    3
    4
    5
    6

    在这个循环中,begin-expr返回的迭代子__begin需要支持自增操作,且每次循环时会与end-expr返回的迭代子__end做判不等比较,在循环内部,通过调用迭代子的解引用(*)操作取得实际的元素。这就是上文说的迭代子对象需要支持operator++、operator !=和operator* 的原因了。

    但是上面的公式中,在一个逗号表达式中auto __begin = begin-expr, __end = end-expr;由于只使用了一个类型符号auto导致起始迭代子__begin和结束迭代子__end是同一个类型,这样不太灵活,在某些设计中,可能希望结束迭代子是另外一种类型。

    因此到了C++17标准时,要求编译器解释for-each循环成如下形式:

    1 auto && __range = for-range-initializer;
    2 auto __begin = begin-expr;
    3 auto __end = end-expr;
    4 for ( ; __begin != __end; ++__begin ) {
    5     for-range-declaration = *__begin;
    6     statement;
    7 }
    
    1
    2
    3
    4
    5
    6
    7

    看到了吧,代码行2和3将获取起始迭代子__begin和结束迭代子__end分开来写,这样这两个迭代子就可以是不同的类型了。虽然类型可以不一样,但这两种类型之间仍然要支持operator !=操作。C++17就C++14的这种改变,对旧的代码不会产生任何影响,但可以让后来的开发更加灵活。

    上次更新: 2025/04/01, 20:53:14
    1.7 auto关键字的前尘后事
    1.9 C++17结构化绑定

    ← 1.7 auto关键字的前尘后事 1.9 C++17结构化绑定→

    最近更新
    01
    C++语言面试问题集锦 目录与说明
    03-27
    02
    第四章 Lambda函数
    03-27
    03
    第二章 关键字static及其不同用法
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式