现代C++实战30讲
吴咏炜
前 Intel 资深软件架构师
立即订阅
3687 人已学习
课程目录
已更新 14 讲 / 共 30 讲
0/4登录后,你可以任选4讲全文学习。
课前必读 (2讲)
开篇词 | C++这么难,为什么我们还要用C++?
免费
课前必读 | 有关术语发音及环境要求
基础篇 (9讲)
01 | 堆、栈、RAII:C++里该如何管理资源?
02 | 自己动手,实现C++的智能指针
03 | 右值和移动究竟解决了什么问题?
04 | 容器汇编 I:比较简单的若干容器
05 | 容器汇编 II:需要函数对象的容器
06 | 异常:用还是不用,这是个问题
07 | 迭代器和好用的新for循环
08 | 易用性改进 I:自动类型推断和初始化
09 | 易用性改进 II:字面量、静态断言和成员函数说明符
提高篇 (3讲)
10 | 到底应不应该返回对象?
11 | Unicode:进入多文字支持的世界
12 | 编译期多态:泛型编程和模板入门
现代C++实战30讲
登录|注册

07 | 迭代器和好用的新for循环

吴咏炜 2019-12-11
你好,我是吴咏炜。
我们已经讲过了容器。在使用容器的过程中,你也应该对迭代器(iterator)或多或少有了些了解。今天,我们就来系统地讲一下迭代器。

什么是迭代器?

迭代器是一个很通用的概念,并不是一个特定的类型。它实际上是一组对类型的要求([1])。它的最基本要求就是从一个端点出发,下一步、下一步地到达另一个端点。按照一般的中文习惯,也许“遍历”是比“迭代”更好的用词。我们可以遍历一个字符串的字符,遍历一个文件的内容,遍历目录里的所有文件,等等。这些都可以用迭代器来表达。
我在用 output_container.h 输出容器内容的时候,实际上就对容器的 beginend 成员函数返回的对象类型提出了要求。假设前者返回的类型是 I,后者返回的类型是 S,这些要求是:
I 对象支持 * 操作,解引用取得容器内的某个对象。
I 对象支持 ++,指向下一个对象。
I 对象可以和 I 或 S 对象进行相等比较,判断是否遍历到了特定位置(在 S 的情况下是是否结束了遍历)。
注意在 C++17 之前,beginend 返回的类型 I 和 S 必须是相同的。从 C++17 开始,I 和 S 可以是不同的类型。这带来了更大的灵活性和更多的优化可能性。
上面的类型 I,多多少少就是一个满足输入迭代器(input iterator)的类型了。不过,output_container.h 只使用了前置 ++,但输入迭代器要求前置和后置 ++ 都得到支持。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《现代C++实战30讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(19)

  • 小一日一
    看了老师的代码再看自己学的代码,感觉我的C++是小学生水平。

    以为自己看过几遍C++ PRIMER 5th, 看过并理解effective++, more effective c++, inside the c++ object model, 能应付平时的开发需要,也能看懂公司别人的代码,就觉得自己的C++不错了,看了老师github的代码后我是彻底服了,感叹C++太博大精深,永远不敢说自己精通C++。

    我什么时候才能达到老师对C++理解并使用的高度呢,难道也需要20年么?

    作者回复: 肯定还有更好的 C++ 代码的。学习无止境!

    认真学习,应该不用那么久(我还没有极客时间专栏来帮助我学习呢😌)。

    反过来,说明老程序员还有点价值么。🤗

    2019-12-11
    2
  • nelson
    如果stream_是nullptr会怎么样?

    作者回复: 得到一个空的不能遍历的迭代器。跟任何 end() 相等比较返回真,因而你不可以对它做 ++ 操作。如果你要硬来,它就死给你看。

    2019-12-12
    1
    1
  • 晚风·和煦
    从 C++17 开始,I 和 S 可以是不同的类型。这带来了更大的灵活性和更多的优化可能性。 没太理解这句话😂

    作者回复: 现在 r.begin() 和 r.end() 可以是不同类型了。

    2019-12-11
    1
  • 千鲤湖
    过来看看老师问的那两个问题,好奇中。。。

    作者回复: 公布第 1 个问题的答案吧:

    #include <fstream>
    #include <iostream>
    #include "istream_line_reader.h"

    using namespace std;

    int main()
    {
        ifstream ifs{"test.cpp"};
        istream_line_reader reader{ifs};
        auto begin = reader.begin();
        for (auto it = reader.begin();
             it != reader.end(); ++it) {
            cout << *it << '\n';
        }
    }

    以上代码,因为 begin 多调用了一次,输出就少了一行……

    2019-12-18
  • 总统老唐
    吴老师,这一课有两个疑问:
    1,“到底应该让 * 负责读取还是 ++ 负责读取”,该怎样理解?如果“读取”指的是在istream上读取一行,放入line_成员中,用++实现这个操作是最常见和直觉的,同时,用 * 返回读取的内容也在最容易想到的方式,反过来,什么情况下会需要”用*来负责读取“?
    2,输入迭代器为什么要定义 iterator operator++(int)

    作者回复: 1. 用 ++ 是最合理的,但也有一个奇怪的地方,目前还没人说到。

    2. 这个就是后置 ++。迭代器要求前置和后置 ++ 都要定义,虽然我目前只使用了前置版本。

    2019-12-16
  • 千鲤湖
    1.可能是operator==中,比较时没有获取当前文件流位置,这样的话,无法比较不同istream(同一个文件)创建的iterator? 

    2 采用ftell获取当前文件流位置

    作者回复: 不是我想的那个……

    这个是个问题,但一般不必解决。要能够比较,对性能影响太大。我线上的版本里是有下面这段注释的:

    // This implementation basically says, any iterators
    // pointing to the same stream are equal. This behaviour
    // may seem a little surprising in the beginning, but, in
    // reality, it hardly has any consequences, as people
    // usually compare an input iterator only to the sentinel
    // object. The alternative, using _M_stream->tellg() to
    // get the exact position, harms the performance too dearly.
    // I do not really have a better choice.
    //
    // If you do need to compare valid iterators, consider using
    // file_line_reader or mmap_line_reader.

    2019-12-16
  • 禾桃
    #1 目前这个输入行迭代器的行为,在什么情况下可能导致意料之外的后果?
        auto x = istream_line_reader();
        auto xit = x.begin();
       这个函数会调用istream_line_reader:: iterator::operator++() {
       getline(*nullptr, _M_line); <---- 死翘翘 }
    但是用户觉得,我只是调用了x.begin, 不至于死的这么突然吧:(

    #2 请尝试一下改进这个输入行迭代器,看看能不能消除这种意外。如果可以,该怎么做?如果不可以,为什么?
    看了您的git代码, 看到了对nullptr的识别和抛出异常的处理,这是个解决方案。或者我们可以istream_line_reader() = delete? 没想到我们需要构造函数istream_line_reader() 的场景。

    作者回复: #1 对我来讲,这不是意外。就像你对空指针解引用崩溃也不是意外一样。没有有效的 istream,你要取这个流的开头,出错很正常。

    #2 因为你没有看到我想的问题,所以第二部分也不是我要的回答……

    2019-12-15
  • MT
    老师,这次是上次关于那个例子的补充:
    1. 在进行迭代的时候,begin()和end()方法,即你所说的,编译器会自动生成指向数组头尾的指针
    2. 在end()方法内返回了struct null_sentinel{} 的一个对象,即 I 和 S 的类型不同
    3.通过使用 struct null_sentinel{};所提供的operator!=() 从而达到对字符串遍历的截至
    如有不对,请老师指出
    之后我尝试过,在 c_string_view{} 中的 end 方法,返回一个它本身的对象,并为NULL,同时重载它的 != 运算符,但是失败了。
    我想问下,这便算是属于一种更多的优化可能性吗?在之后,若需要只需要修改struct null_sentinel{};即可?

    作者回复: 前面部分没有问题。后面的失败部分,没看懂你的意思。

    我这个例子重点在于,null_sentinel 表示的不是一个位置,而是一个条件。我们可以用迭代器来表示一个条件,这是对它的功能的很大扩展。虽然这种扩展方式性能非常好,但这个功能主要不是优化,而是新的可能性。

    2019-12-14
  • 旭东
    老师,您好,iterater中后置++的实现是不是应该返回const;避免(i++)++这样的代码通过编译?

    作者回复: 1. 不能写 const,因为你修改了自己。

    2. 就算能写也防不了,因为你返回的是个全新的对象。

    2019-12-14
  • 木瓜777
    iterator operator++(int) { iterator temp(*this); ++*this; return temp; }

    这个拷贝构造,是否会出问题? 如果失败,this继续读取下一行,但temp是异常的。

    作者回复: 拷贝构造失败的话,直接抛异常了,当然不会继续读取下一行。

    2019-12-13
  • 我叫bug谁找我
    遍历一遍后,第二次调用begin会崩溃,stream_指针已经为空

    作者回复: 作为input iterator,本来你就不应该遍历第二次的。这个不是问题。

    2019-12-13
  • MT
    老师,可以讲以下为什么可以将I 和 S 设置成不同的类型吗?具体使用在那些方面?

    作者回复: 给个例子你仔细研读一下吧。功能是遍历字符串,直到遇到字符串结尾(事先不知道字符串长度)。

    #include <stdio.h>

    struct null_sentinel {};

    bool operator!=(const char* ptr, null_sentinel)
    {
        return *ptr != 0;
    }

    // operator!=(null_sentinel, const char* ptr), operator==, ...

    struct c_string_view {
        c_string_view(const char* str) : str_(str) {}
        const char* begin() const { return str_; }
        null_sentinel end() const { return null_sentinel{}; }
        const char* str_;
    };

    int main()
    {
        c_string_view msg{"Hello world!"};
        for (char ch : msg) {
            putchar(ch);
        }
        putchar('\n');
    }

    2019-12-12
  • Scott
    我的理解是istream_line_reader的iterator在到达end时,再++会直接crash,这个和STL里面主流容器的行为是不一致的。
    可以在get_line之前,判断一下stream_是否为nullptr,不是才调用,对end的iterator反复进行++都一直返回自己本身。

    作者回复: 你对容器的 end() 解引用,同样可能崩溃(取决于实现)。你不被允许这么做。这么做,你就进入了 undefined behavior 的领域,系统是死还是出 bug 都正常。

    2019-12-12
  • 禾桃
    输入迭代器和输出迭代器,
    这个入和出是相对于什么而言的?
    感觉有点绕。

    谢谢!

    作者回复: cout << *it 就是读;
    *it = 42 就是写。

    2019-12-11
  • tt
    意料之外的后果,是不是主要就是资源发生了不可控或不可知的泄露或状态改变?

    这里的资源我觉得一是string对象,一个是istream对象,那么在这两个对象的内存管理上会引起问题?

    比如构造函数中传入的istream指针没有被管理起来,它指向的对象如果被析构就会发生异常?

    作者回复: 你说的情况会出问题,但这个是需要调用者保证的,我做不了什么事情。

    再想想。🤓

    2019-12-11
  • 小一日一
    1. 我能想到的一点是,istream_line_reader在构造时没有对输入流的状态做检测,如果在输入流处于错误状态时调用getline(),会抛 ios_base::failure异常。
    2. 我把带输入流检查的构造贴一下:
        istream_line_reader() noexcept : stream_(nullptr) {}
        explicit istream_line_reader( istream& is) noexcept {
            if (is.good()) stream_ = &is;
            else stream_ = nullptr;
        }

    作者回复: 抛异常是很正常的呀,不是问题。不过,IO streams 缺省应该是不抛异常的。

    2019-12-11
  • 廖熊猫
    我认为是因为stream操作有副作用吧,在使用++还是*读取的时候有提到,每次读取的话都会受到影响,如果我在使用迭代器之前操作了stream,这个迭代器的操作范围就不是我预期的范围了。迭代器玩法很多啊,有一本《Functional C++》讲了很多基于范围的操作,不过水平不够,没学到什么精髓。

    作者回复: 你说的用法我觉得还是不会让人惊讶的……

    你是说 Ivan Čukić 写的 Functional Programming in C++ 吗?我是那本书的技术校对……嗯,我当然推荐它的。Range 下面会单独有一讲。

    学语言,还是要多读多写多练。

    2019-12-11
  • Geek_71d4ac
    在构造函数中使用this是否安全?万一构造中途失败了呢?

    作者回复: 本身没有任何问题。如何保证行为安全(如异常安全)是个独立问题,跟是否在构造函数里没啥关系。尽量不使用裸指针非常重要,用了的话就需要照顾很多细节了……

    2019-12-11
  • 未来、尽在我手
    老师,可以讲讲auto?
    我一直很期待新的for,可是没看到在哪?
    这儿就详细介绍了迭代器及各种不同的迭代器。

    作者回复: auto 正是下一讲的主要内容。第二个问题,在正文里搜“基于范围的 for 循环”。代码例子没细看么?🤔

    2019-12-11
    1
收起评论
19
返回
顶部