详解C++ 的STL迭代器原理和实现

1. 迭代器简介

为了提高C++编程的效率,STL(Standard Template Library)中提供了许多容器,包括vector、list、map、set等。然而有些容器(vector)可以通过下标索引的方式访问容器里面的数据,但是大部分的容器(list、map、set)不能使用这种方式访问容器中的元素。为了统一访问不同容器时的访问方式,STL为每种容器在实现的时候设计了一个内嵌的iterator类,不同的容器有自己专属的迭代器(专属迭代器负责实现对应容器访问元素的具体细节),使用迭代器来访问容器中的数据。除此之外,通过迭代器可以将容器和通用算法结合在一起,只要给予算法不同的迭代器,就可以对不同容器执行相同的操作,例如find查找函数(因为迭代器提供了统一的访问方式,这是使用迭代器带来的好处)。迭代器对一些基本操作如*、->、++、==、!=、=进行了重载,使其具有了遍历复杂数据结构的能力,其遍历机制取决于所遍历的容器,所有迭代器的使用和指针的使用非常相似。通过begin,end函数获取容器的头部和尾部迭代器,end迭代器不包含在容器之内,当begin和end返回的迭代器相同时表示容器为空。

STL主要由 容器、迭代器、算法、函数对象、和内存分配器 五大部分构成。

2. 迭代器的实现原理

首先,看看STL中迭代器的实现思路:

从上图中可以看出,STL通过类型别名的方式实现了对外统一;在不同的容器中类型别名的真实迭代器类型是不一样的,而且真实迭代器类型对于++、--、*、->等基本操作的实现方式也是不同的。(PS:迭代器很好地诠释了接口与实现分离的意义)

既然我们已经知道了迭代器的实现思路,现在如果让我们自己设计一个list容器的简单迭代器,应该如何实现呢?

1.list类需要有操作迭代器的方法

1.begin/end

2.insert/erase/emplace

2.list类有一个内部类list_iterator

1.有一个成员变量ptr指向list容器中的某个元素

2.iterator负责重载++、--、*、->等基本操作

3.list类定义内部类list_iterator的类型别名

以上就是实现一个list容器的简单迭代器需要考虑的具体细节。

3. 迭代器的简单实现

my_list.h(重要部分有注释说明

//
// Created by wengle on 2020-03-14.
//

#ifndef CPP_PRIMER_MY_LIST_H
#define CPP_PRIMER_MY_LIST_H

#include <iostream>

template<typename T>
class node {
public:
    T value;
    node *next;
    node() : next(nullptr) {}
    node(T val, node *p = nullptr) : value(val), next(p) {}
};

template<typename T>
class my_list {
private:
    node<T> *head;
    node<T> *tail;
    int size;

private:
    class list_iterator {
    private:
        node<T> *ptr; //指向list容器中的某个元素的指针

    public:
        list_iterator(node<T> *p = nullptr) : ptr(p) {}

        //重载++、--、*、->等基本操作
        //返回引用,方便通过*it来修改对象
        T &operator*() const {
            return ptr->value;
        }

        node<T> *operator->() const {
            return ptr;
        }

        list_iterator &operator++() {
            ptr = ptr->next;
            return *this;
        }

        list_iterator operator++(int) {
            node<T> *tmp = ptr;
            // this 是指向list_iterator的常量指针,因此*this就是list_iterator对象,前置++已经被重载过
            ++(*this);
            return list_iterator(tmp);
        }

        bool operator==(const list_iterator &t) const {
            return t.ptr == this->ptr;
        }

        bool operator!=(const list_iterator &t) const {
            return t.ptr != this->ptr;
        }
    };

public:
    typedef list_iterator iterator; //类型别名
    my_list() {
        head = nullptr;
        tail = nullptr;
        size = 0;
    }

    //从链表尾部插入元素
    void push_back(const T &value) {
        if (head == nullptr) {
            head = new node<T>(value);
            tail = head;
        } else {
            tail->next = new node<T>(value);
            tail = tail->next;
        }
        size++;
    }

    //打印链表元素
    void print(std::ostream &os = std::cout) const {
        for (node<T> *ptr = head; ptr != tail->next; ptr = ptr->next)
            os << ptr->value << std::endl;
    }

public:
    //操作迭代器的方法
    //返回链表头部指针
    iterator begin() const {
        return list_iterator(head);
    }

    //返回链表尾部指针
    iterator end() const {
        return list_iterator(tail->next);
    }

    //其它成员函数 insert/erase/emplace
};

#endif //CPP_PRIMER_MY_LIST_H

test.cpp

//// Created by wengle on 2020-03-14.//#include <string>#include "my_list.h"struct student {    std::string name;    int age;    student(std::string n, int a) : name(n), age(a) {}    //重载输出操作符    friend std::ostream &operator<<(std::ostream &os, const student &stu) {        os << stu.name << " " << stu.age;        return os;    }};int main() {    my_list<student> l;    l.push_back(student("bob", 1)); //临时量作为实参传递给push_back方法    l.push_back(student("allen", 2));    l.push_back(student("anna", 3));    l.print();    for (my_list<student>::iterator it = l.begin(); it != l.end(); it++) {        std::cout << *it << std::endl;        *it = student("wengle", 18);    }    return 0;}//
// Created by wengle on 2020-03-14.
//

#include <string>
#include "my_list.h"

struct student {
    std::string name;
    int age;

    student(std::string n, int a) : name(n), age(a) {}

    //重载输出操作符
    friend std::ostream &operator<<(std::ostream &os, const student &stu) {
        os << stu.name << " " << stu.age;
        return os;
    }
};

int main() {
    my_list<student> l;
    l.push_back(student("bob", 1)); //临时量作为实参传递给push_back方法
    l.push_back(student("allen", 2));
    l.push_back(student("anna", 3));
    l.print();

    for (my_list<student>::iterator it = l.begin(); it != l.end(); it++) {
        std::cout << *it << std::endl;
        *it = student("wengle", 18);
    }
    return 0;
}

4. 迭代器失效

// inserting into a vector
#include <iostream>
#include <vector>

int main ()
{
  std::vector<int> myvector (3,100);
  std::vector<int>::iterator it;

  it = myvector.begin();
  it = myvector.insert ( it , 200 );

  myvector.insert (it,200,300);
  //it = myvector.insert (it,200,300);
  myvector.insert (it,5,500); //当程序执行到这里时,大概率会crash
  for (std::vector<int>::iterator it2=myvector.begin(); it2<myvector.end(); it2++)
    std::cout << ' ' << *it2;
  std::cout << '\n';

  return 0;
}

上面的代码很好地展示了什么是迭代器失效?迭代器失效会导致什么样的问题?

当执行完myvector.insert (it,200,300);这条语句后,实际上myvector已经申请了一块新的内存空间来存放之前已保存的数据本次要插入的数据,由于it迭代器内部的指针还是指向旧内存空间的元素,一旦旧内存空间被释放,当执行myvector.insert (it,5,500);时就会crash(PS:因为你通过iterator的指针正在操作一块已经被释放的内存,大多数情况下都会crash)。迭代器失效就是指:迭代器内部的指针没有及时更新,依然指向旧内存空间的元素。

上图展示了STL源码中vector容器insert方法的实现方式。当插入的元素个数超过当前容器的剩余容量时,就会导致迭代器失效。这也是测试代码中myvector.insert (it,200,300);插入200个元素的原因,为了模拟超过当前容器剩余容量的场景,如果你的测试环境没有crash,可以将插入元素设置的更多一些。

5. 参考资料

迭代器失效问题?

(0)

相关推荐

  • C++特性:迭代器

    1. 迭代器(Iterator)的介绍 背景:指针可以用来遍历存储空间连续的数据结构,但是对于存储空间费连续的,就需要寻找一个行为类似指针的类,来对非数组的数据结构进行遍历. 定义:迭代器是一种检查容器内元素并遍历元素的数据类型. 迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围. 迭代器(Iterator)是指针(pointer)的泛化,它允许程序员用相同的方式处理不同的数据结构(容器). (1)迭代器类似于C语言里面的指针类型,它提供了对对象的间接访问. (2)指针是C语言

  • 浅谈c++ stl迭代器失效的问题

    之前看<C++ Primier>的时候,也解到在顺序型窗口里insert/erase会涉及到迭代器失效的问题,并没有深究.今天写程序的时候遇到了这个问题. 1 莫名其妙的Erase 最初我的程序是酱紫的,别说话,我知道这样是有问题的,可这样是最直观的想法 int arr[]={0,1,2,3,4,5,6,7,8,9,10}; vector<int> a(arr,arr+sizeof(arr)/sizeof(*arr));for (auto it = a.begin(); it !=

  • C++设计模式之迭代器模式

    前言 又到年底了,时间真的过的好快啊.最近也非常感伤,总是怀念大学的日子,做梦的时候也常常梦到.梦到大学在电脑前傻傻的敲着键盘,写着代码,对付着数据结构与算法的作业:建立一个链表,遍历链表,打印链表.现在把那个时候声明的链表的头文件拿出来看看: 复制代码 代码如下: typedef struct tagNode {      int value;      tagNode *pPre;      tagNode *pNext; }Node;   class CList { public:    

  • C++迭代器iterator详解

    目录 1.迭代器分类 1) 正向迭代器 2) 常量正向迭代器 3) 反向迭代器 4) 常量反向迭代器 2.迭代器用法示例 3.迭代器:++it 与 it++ 哪个好? (1)前置返回一个引用,后置返回一个对象 (2)前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低 4.迭代器的功能分类 5.迭代器的辅助函数 总结 1.迭代器分类 要访问顺序容器和关联容器中的元素,需要通过"迭代器(iterator)"进行.迭代器是一个变量,相当于容器和操纵容器的算法之间的中介.迭代器

  • 详解C++ 的STL迭代器原理和实现

    1. 迭代器简介 为了提高C++编程的效率,STL(Standard Template Library)中提供了许多容器,包括vector.list.map.set等.然而有些容器(vector)可以通过下标索引的方式访问容器里面的数据,但是大部分的容器(list.map.set)不能使用这种方式访问容器中的元素.为了统一访问不同容器时的访问方式,STL为每种容器在实现的时候设计了一个内嵌的iterator类,不同的容器有自己专属的迭代器(专属迭代器负责实现对应容器访问元素的具体细节),使用迭代

  • 详解C# ConcurrentBag的实现原理

    一.ConcurrentBag类 ConcurrentBag<T>对外提供的方法没有List<T>那么多,但是同样有Enumerable实现的扩展方法.类本身提供的方法如下所示. 名称 说明 Add 将对象添加到 ConcurrentBag 中. CopyTo 从指定数组索引开始,将 ConcurrentBag 元素复制到现有的一维 Array 中. Equals(Object) 确定指定的 Object 是否等于当前的 Object. (继承自 Object.) Finalize

  • 一文详解Python中生成器的原理与使用

    目录 什么是生成器 迭代器和生成器的区别 创建方式 生成器表达式 基本语法 生成器函数 yield关键字 yield和return yield的使用方法 生成器函数的基本使用 send的使用 可迭代对象的优化 总结 我们学习完推导式之后发现,推导式就是在容器中使用一个for循环而已,为什么没有元组推导式? 原因就是“元组推导式”的名字不是这样的,而是叫做生成器表达式. 什么是生成器 生成器表达式本质上就是一个迭代器,是定义迭代器的一种方式,是允许自定义逻辑的迭代器.生成器使用generator表

  • 详解JSP 中Spring工作原理及其作用

    详解JSP 中Spring工作原理及其作用 1.springmvc请所有的请求都提交给DispatcherServlet,它会委托应用系统的其他模块负责负责对请求进行真正的处理工作. 2.DispatcherServlet查询一个或多个HandlerMapping,找到处理请求的Controller. 3.DispatcherServlet请请求提交到目标Controller 4.Controller进行业务逻辑处理后,会返回一个ModelAndView 5.Dispathcher查询一个或多个

  • 详解http访问解析流程原理

    详解http访问解析流程原理 http访问网址域名解析流程: 1.在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析. 2.如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析. 3.如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/ip参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器

  • 详解Vue中的MVVM原理和实现方法

    下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到: 1.Vue数据双向绑定核心代码模块以及实现原理 2.订阅者-发布者模式是如何做到让数据驱动视图.视图驱动数据再驱动视图 3.如何对元素节点上的指令进行解析并且关联订阅者实现视图更新 一.思路整理 实现的流程图: 我们要实现一个类MVVM简单版本的Vue框架,就需要实现一下几点: 1.实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者. 2.实现一个解析器Compi

  • 详解vue 组件的实现原理

    组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性.本文我们一起从源码的角度,了解一下组件的底层实现原理. 组件注册时做了什么? 在Vue中使用组件,要做的第一步就是注册.Vue提供了全局注册和局部注册两种方式. 全局注册方式如下: Vue.component('my-component-name', { /* ... */ }) 局部注册方式如下: var ComponentA = { /* ... */ } new Vu

  • 详解React Fiber的工作原理

    啥是React Fiber? React Fiber,简单来说就是一个从React v16开始引入的新协调引擎,用来实现Virtual DOM的增量渲染. 说人话:就是一种能让React视图更新过程变得更加流畅顺滑的处理手法. 我们都知道:进程大,线程小.而Fiber(纤维)是一种比线程还要细粒度的处理机制.从这个单词也可以猜测:React Fiber会很"细".到底怎么个细法,我们接着往下看. 为什么会有React Fiber? 之前说了,React Fiber是为了让React的视

  • 详解IOS WebRTC的实现原理

    概述 它在2011年5月开放了工程的源代码,在行业内得到了广泛的支持和应用,成为下一代视频通话的标准. WebRTC的音视频通信是基于P2P,那么什么是P2P呢? 它是点对点连接的英文缩写. P2P连接模式 一般我们传统的连接方式,都是以服务器为中介的模式: 类似http协议:客户端?服务端(当然这里服务端返回的箭头仅仅代表返回请求数据). 我们在进行即时通讯时,进行文字.图片.录音等传输的时候:客户端A?服务器?客户端B. 而点对点的连接恰恰数据通道一旦形成,中间是不经过服务端的,数据直接从一

  • 详解SpringBoot异常处理流程及原理

    异常处理流程 执行目标方法,目标方法运行期间有任何异常都会被catch捕获,并标志当前请求结束,dispatchException抛出异常 进入视图解析流程,并渲染页面,发生异常时,参数mv为空,传入捕获的异常dispatchException 处理handler发生的异常,处理完成返回ModelAndView (1)遍历所有的HandlerExceptionResolvers,找到可以处理当前异常的解析器来解析异常 (2)调用resolveException解析异常,传入request和res

随机推荐