【C++】STL之迭代器介绍、原理、失效

简介: 【C++】STL之迭代器介绍、原理、失效

一、迭代器有什么用?

我们知道,STL标准库一共有六大部件:分配器、容器、迭代器、算法、仿函数、适配器。其中,迭代器就是用来“联结”算法、仿函数与容器的纽带。

除此之外,在设计模式中有一种模式叫迭代器模式,简单来说就是提供一种方法,在不需要暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素,这种设计思维在STL中得到了广泛的应用,是STL的关键所在,通过迭代器,容器和算法可以有机的粘合在一起,只要对算法给予不同的迭代器,就可以对不同容器进行相同的操作。(参考:https://blog.csdn.net/shudou/article/details/11099931

比如下面这个find函数,展示了容器、算法和迭代器如何合作:

template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, const T &value)
{
    while (first != last && *frist != value)
        ++first;
    return first;
}

上述的find函数,只需要传递容器的迭代器,就可以实现对不同的容器实现相同的算法,这其实是一种泛型编程的思想。

二、不同容器的迭代器实现

vector

我们来看看在vector中对于iterator的实现:

 template<typename T,class Alloc = alloc >
    class vector
    { 
 public:
      typedef T    value_type;
      typedef value_type*  iterator;
      ······
 };

在此可以看到iterator在vector中也只是简单的被定义成了我们传入的类型参数T的指针。

List

下面是某位博主自己实现的一个简单List 迭代器,供大家学习使用:

@wengle

#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

对于其他的容器迭代器分析,暂略。

三、迭代器分类

在STL中,除了原生指针以外,迭代器被分为五类:

Input Iterator

顾名思义,input——此迭代器不允许修改所指的对象,即是只读的。支持==、!=、++、*、->等操作。

Output Iterator

允许算法在这种迭代器所形成的区间上进行只写操作。支持++、*等操作。

Forward Iterator

允许算法在这种迭代器所形成的区间上进行读写操作但只能单向移动,每次只能移动一步。支持Input Iterator和Output Iterator的所有操作。

Bidirectional Iterator

允许算法在这种迭代器所形成的区间上进行读写操作,可双向移动,每次只能移动一步。支持Forward Iterator的所有操作,并另外支持–操作。

Random Access Iterator

包含指针的所有操作,可进行随机访问(vector容器支持),随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + n、it - n、it += n、 it -= n、it1 - it2和it[n]等操作。

上述五种迭代器的分类和联系可参考下图:



了解了迭代器的类型,我们就能解释vector的迭代器和list迭代器的区别了。显然vector的迭代器具有所有指针算术运算能力,而list由于是双向链表,因此只有双向读写但不能随机访问元素。**故vector的迭代器种类为Random Access Iterator,list 的迭代器种类为Bidirectional Iterator。**我们只需要根据不同的迭代器种类,利用traits编程技巧萃取出迭代器种类,然后由C++的重载机制就能够对不同型别的迭代器采用不同的处理流程了。为此,对于每个迭代器都必须定义型别iterator_category,也就是源码中的typedef std::forward_iterator_tag iterator_category; 实际中可以直接继承STL中定义的iterator模板,模板后三个参数都有默认值,因此继承时只需要指定前两个模板参数即可。如下所示,STL定义了五个空类型作为迭代器的标签:

template<class Category,class T,class Distance = ptrdiff_t,class Pointer=T*,class Reference=T&>
class iterator{
    typedef Category iterator_category;
    typedef T        value_type;
    typedef Distance difference_type;
    typedef Pointer  pointer;
    typedef Reference reference;
};

struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public forward_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};

四、迭代器失效?

当使用一个容器的insert或者erase函数通过迭代器插入、删除或者修改元素(如map、set,因为其底层是红黑树)"可能"会导致迭代器失效,因此为了避免危险,应该重新获取的新的有效的迭代器进行正确的操作。plus

vector

1、当插入(push_back)一个元素后,end操作返回的迭代器肯定失效。

2、当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。

3、当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。

list

1、插入操作(insert)和接合操作(splice)不会造成原有的list迭代器失效,这在vector中是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致所有的迭代器全部失效。

2、list的删除操作(erase)也只有指向被删除元素的那个迭代器失效,其他迭代器不受影响。(list目前只发现这一种失效的情况)

关联容器

对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响(虽然删除了一个元素,整棵树也会调整,以符合红黑树或者二叉树的规范,但是单个节点在内存中的地址没有变化,变化的是各节点之间的指向关系)。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用**erase(iter++)(分三步走,先把iter传值到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了)**的方式删除迭代器。

相关文章
|
1月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
95 10
|
1月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
49 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
1月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
69 5
|
1月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
52 1
|
1月前
|
算法 安全 Linux
【C++STL简介】——我与C++的不解之缘(八)
【C++STL简介】——我与C++的不解之缘(八)
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
54 2
|
1月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
22 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4