今天抽空来看看C++标准库中迭代器的相关知识。
我们知道,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中对于iterator的实现:
template<typename T,class Alloc = alloc >
class vector
{
public:
typedef T value_type;
typedef value_type* iterator;
······
};
在此可以看到iterator在vector中也只是简单的被定义成了我们传入的类型参数T的指针。
下面是某位博主自己实现的一个简单List 迭代器,供大家学习使用:
@wengle
#ifndef CPP_PRIMER_MY_LIST_H
#define CPP_PRIMER_MY_LIST_H
#include
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在失效前已经自增了)**的方式删除迭代器。