C++容器介绍
在C++中有很多的数据容器,例如,string,vector,list,stack,queue,deque等。
下面介绍两种最常用的容器,来作为迭代器的前缀:
vector
先来介绍一下 vector的几个常用接口:
1,push_back
尾插数据
2,pop_back
尾删数据
3,front
获取第一个元素
4,back
获取最后一个元素
5,resize
扩容有效数据空间
6,reverse
扩容总空间大小
7,insert
指定位置插入
8,erase
指定位置删除
vector的底层使用的是线性数组的数据结构,其实就是使用动态内存开辟的空间,和数组基本相似,他的底层使用了三个指针:
start :头部位置
finish :最后一个有效数据的下一个位置
end_of_storage:总空间的最后一个位置
下面来用一张图解释这三个指针的概念:
可能这样解释还是不能完全的将vector讲明白,如果不了解vector的底层,迭代器解释起来会非常的抽象,所以下面我们来模拟实现vector和它的部分接口,为迭代器做铺垫
上代码:
首先我们定义一个自己的命名空间,创建一个vector类
#pragma once
namespace ys
{
//使用模板,实现泛型编程
template<class T>
class vector
{
//重命名
typedef T* iterator;
public:
private:
iterator _start = nullptr;//起始位置
iterator _finish = nullptr;//最后一个有效元素的下一个位置
iterator _end_of_storage = nullptr;//总空间大小的最后一个位置
};
在使用模板后,可以接收任意类型的参数
上面说到,vector的底层就是数组,所以我们可以使用动态内存来开辟空间存放数据,在创建这三个指针的时候,我们初始化为nullptr,所以首先解决总空间扩容的问题:
#pragma once
namespace ys
{
//使用模板,实现泛型编程
template<class T>
class vector
{
//重命名
typedef T* iterator;
public:
//构造函数
vector()
{
//调用capacity,完成初始化
//默认开辟10个单位的空间
reserve(10);
}
//返回有效元素个数
size_t size()
{
return _finish - _start;
}
//返回总空间大小
size_t capaicty()
{
return _end_of_storage - _start;
}
//总空间扩容
void reserve(size_t n)
{
//如果要扩容的大小大于原有大小,才进行扩容
if (n > capaicty())
{
//如果数组当中已经有数据
//先将原有的数据拷贝到一个新数组,防止开辟动态内存失败后丢失数据
//创建一个新数组
iterator tmp = new T[n];
size_t sz = size();
//将原有的数据存入tmp
if (_start)
{
for (int i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
//释放旧数组空间
delete[] _start;
}
//更新三个指针的位置
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
在解决完总空间大小扩容后,再完成有效空间扩容的问题:
//有效数据扩容
void resize(size_t n, T val)
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capaicty())
{
//扩容
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
_finish++;
}
}
然后我们可以完成尾插和尾删的接口,_finish是有效元素的下一个位置,所以尾插,直接在_finish位置插入,尾删只需要–_finish即可:
//尾插
void push_back(T val)
{
//插入前先判断是否需要扩容
if (_finish == _end_of_storage)
{
//扩容
reserve(size() == 0 ? 10 : capaicty() * 2);
}
//尾插
*_finish = val;
_finish++;
}
bool empty()
{
return size() == 0;
}
//尾删
void pop_back()
{
assert(!empty);
//尾删之前判断数组是否为空
_finish--;
}
指定位置插入insert
在vector的设计中,指定位置参数是用迭代器,所以我们先实现正向迭代器:begin 和 end
//正向迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
可以看到,我们只用了6行代码,就搞定了正向迭代器,这里我们使用原生指针来作为迭代器。
迭代器到底是个什么东西呢?
其实就是一个指针,只不过在stl库里面被封装了,迭代器就是一个指针,可以进行解引用,++ 或 – 操作
搞定迭代器后,再来实现insert和erase
//指定位置前插入
void insert(iterator pos, T val)
{
assert(pos >= _start);
assert(pos <= _finish);
//判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capaicty() == 0 ? 4 : capaicty() * 2);
// 扩容后更新pos,解决pos失效的问题
pos = _start + len;
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
it--;
}
*pos = val;
_finish++;
}
//指定位置删除
void erase(iterator it)
{
assert(it >= _start);
assert(it <= _finish);
//将数据向前覆盖
while (it < _finish - 1)
{
*it = *(it + 1);
it++
}
_finish--;
}
list
list的常用接口
1,push_back
尾插数据
2,pop_back
尾删数据
3,push_front
头插数据
4,pop_front
头删数据
5,front
获取第一个元素
6,back
获取最后一个元素
7,resize
扩容有效数据空间
8,reverse
扩容总空间大小
list的底层实现是由一个带头链表,链表可以是双向也可以是单向
list的模拟实现和vector的模拟实现其实大差不差,这里就不具体介绍了,直接上代码:
//这里我模拟实现的是一个双向带头链表
namespace yslist
{
//节点
template<class T>
struct list_node
{
list_node* _prev;//上一个节点
list_node* _next;//下一个节点
T _val;
list_node(const T& data = T())
:_prev(nullptr)
, _next(nullptr)
, _val(data)
{
}
};
//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> node;
typedef list_iterator<T, Ref, Ptr> Self;
node* _node;
list_iterator(node* n)
{
_node = n;
}
Ref operator*()
{
return _node->_val;
}
Ptr operator->()
{
return &_node->_val;
}
Self& operator++()//前置++
{
_node = _node->_next;
return *this;
}
Self operator++(int)//后置++
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--()//前置--
{
_node = _node->_prev;
return *this;
}
Self& operator--(int)//后置--
{
_node = _node->_prev;
return *this;
}
bool operator !=(const Self it)
{
return _node != it._node;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
//普通迭代器
typedef list_iterator<T, T&, T*> iterator;
//const 迭代器
typedef list_iterator<T, const T&, const T*> const_iterator;
//构造函数
list()
{
_head = new node;
node* tmp = new node;
_head->_next = _head->_prev = _head;
}
list(const T& data)
{
_head = new node;
node* tmp = new node(data);
_head->_next = _head->_prev = _head;
_head->_next = tmp;
_head->_prev = tmp;
tmp->_next = _head;
tmp->_prev = _head;
}
//迭代器区间构造
template<class Iterator>
list(Iterator begin, Iterator end)
{
_head = new node;
_head->_next = _head->_prev = _head;
while (begin != end)
{
push_back(*begin);
_size++;
begin++;
}
}
//拷贝构造
list(const list<T>& lt)
{
_head = new node;
_head->_next = _head->_prev = _head;
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
}
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
//迭代器
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
//const 迭代器
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
//尾插
void push_back(const T& data)
{
//找尾
node* tmp = _head->_prev;
//创建新节点
node* newnode = new node(data);
//连接
tmp->_next = newnode;
newnode->_prev = tmp;
newnode->_next = _head;
_head->_prev = newnode;
_size++;
}
//尾删
void pop_back()
{
if (_size > 0)
{
node* tmp = _head->_prev;
tmp->_prev->_next = _head;
_head->_prev = tmp->_prev;
delete tmp;
_size--;
}
}
//头插
void push_pront(const T& data)
{
insert(begin(), data);
_size++;
}
//头删
void pop_pront(const T& data)
{
erase(begin, data);
_size--;
}
//指定位置插入
void insert(iterator pos, const T& data)
{
node* tmp = new node(data);
node* cur = pos._node;
//找上一个节点
node* prev = cur->_prev;
prev->_next = tmp;
tmp->_prev = prev;
tmp->_next = cur;
cur->_prev = tmp;
cur = nullptr;
/*node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(data);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;*/
_size++;
}
//指定位置删除
void erase(iterator pos)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
_size--;
}
//判空
bool empty()
{
return _size == 0;
}
//有效数据个数
size_t size()
{
return _size;
}
//清空所有数据
void clear()
{
if (_size != 0)
{
/*ys::list<T>::iterator it = begin();
while (it != end())
{
erase(it++);
}*/
node* cur = _head->_next;
node* next = nullptr;
while (cur != _head)
{
next = cur->_next;
delete cur;
cur = next;
}
_head->_next = _head->_prev = _head;
}
_size = 0;
}
private:
node* _head;
size_t _size = 0;
};
}
这里的重点是迭代器,list是一个链表,所以不能使用原生指针,list的原生指针是不支持++,–,[],* 操作的,因此我们将迭代器单独封装成一个类,再在这个类里面实现运算符重载,这样迭代器就可以支持++,–,* 操作了
我们单独把迭代器拿出来剖析:
//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
//首先迭代器要有一个原生指针
//这个原生指针是迭代器的关键
typedef list_node<T> node;
typedef list_iterator<T, Ref, Ptr> Self;
node* _node;
list_iterator(node* n)
{
_node = n;
}
Ref operator*()
{
return _node->_val;
}
Ptr operator->()
{
return &_node->_val;
}
Self& operator++()//前置++
{
_node = _node->_next;
return *this;
}
Self operator++(int)//后置++
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--()//前置--
{
_node = _node->_prev;
return *this;
}
Self& operator--(int)//后置--
{
_node = _node->_prev;
return *this;
}
bool operator !=(const Self it)
{
return _node != it._node;
}
};
//迭代器
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
这里将迭代器封装成一个类,如果使用迭代器,其实就是使用这个类来进行操作,而类的内部有一个原生指针,对迭代器进行操作,就是对这个原生指针进行操作
详解:
首先我们来看list的的节点是怎么构造的:
list是一个链表,这里我模拟实现的是一个双向链表,T是数据类型,里面定义了记录了上一个节点的指针和下一个节点的指针,还有一个存储数据的变量,如果要实现迭代器的++和–功能,我们需要拿到_prev和_next,但是节点是一个结构体,结构体本身是不支持++和–操作的,因此不能像vector那样使用原生指针去进行操作。
所以list的迭代器需要另外一种方法来封装,也就是结构体,我们来看一下迭代器结构体的定义:
可以看到,迭代器结构体里面只有一个指针,这个指针就是list节点的指针。
再来看一下list对迭代器结构体的定义:
将list_iterator重命名为 iterator,在stl当中其实也是这样定义的,iterator就是一个结构体的重命名,而对iterator去进行++ 或者-- 解引用操作,其实就是迭代器结构体的运算符重载,而const迭代器和普通迭代器的区别就是给在给迭代器结构体传参的时候带不带const。
再来看迭代器的++,–解引用是怎么实现的:
首先我们来看解引用操作:
对迭代器解引用,就是返回迭代器指针中的数据
我们俩解析一下这个操作:
ys::list<int>::iterator it1 = ls.begin();
上面这行代码,如果去掉list中的重命名是这样的:
ys::list<int>::list_iterator it1 = ls.begin();
iterator就是list_iterator的重命名,it1其实是创建了一个结构体,然后用这结构体去接收begin()的返回值,完成拷贝构造函数,而解引用 ++ – 操作都是这个结构体的运算符重载,所以list中迭代器的解引用操作非常简单,只需要将节点指针中的数据返回即可。
来用一张图解释:
list的迭代器本质就是使用节点去构造一个结构体,结构体中存储这某个节点,然后再封装运算符重载,对节点指针进行不同的操作
再来看++:
list的节点指针存储着下一个节点的地址,要想++到下一个节点,直接将迭代器里的节点指针的_next赋值给当前节点指针即可
–:
和++操作一样,只不过是将上一个节点的指针赋值给当前节点
总结:list的迭代器是一个结构体,++,–,*等操作都是这个结构体的运算符重载,真正操作的是这个结构体里的节点指针
正向迭代器
我们来观察一下,stl的迭代器和我模拟实现的迭代器有什么区别:
可以看到,在stl里的迭代器,end是最后一个有效元素的下一个位置,如果对这个迭代器进行解引用操作,会报错,但是在我实现的迭代器中,还是会打印输出,因为end在实现的时候,直接返回的是_finish,但是无伤大雅,反正不是正确的数据,在日常使用的时候,也不会有人对end进行解引用操作,为了验证end确实是最后一个有效数据的下一个位置,我们对end进行–:
我们来使用迭代器进行遍历:
有了迭代器就可以使用范围for:
也支持从后先前遍历:
list:
void TestList1()
{
cout << "模拟实现list:" << endl;
yslist::list<int> ls;
ls.push_back(1);
ls.push_back(2);
ls.push_back(3);
ls.push_back(4);
ls.push_back(5);
yslist::list<int>::iterator it1 = ls.begin();
yslist::list<int>::iterator it2 = ls.end();
cout << "正向遍历:" << endl;
while (it1 != it2)
{
cout << *(it1++) << " ";
}
cout << endl;
it1 = ls.begin();
it2 = ls.end();
cout << "反向遍历:" << endl;
while (it2 != it1)
{
it2--;
cout << *it2 << " ";
}
cout << endl;
it1 = ls.begin();
it2 = ls.end();
cout << "范围for遍历:" << endl;
for (auto e : ls)
{
cout << e << " ";
}
cout << endl;
}
void TestList2()
{
cout << "STL list:" << endl;
list<int> ls;
ls.push_back(1);
ls.push_back(2);
ls.push_back(3);
ls.push_back(4);
ls.push_back(5);
list<int>::iterator it1 = ls.begin();
list<int>::iterator it2 = ls.end();
cout << "正向遍历:" << endl;
while (it1 != it2)
{
cout << *(it1++) << " ";
}
cout << endl;
it1 = ls.begin();
it2 = ls.end();
cout << "反向遍历:" << endl;
while (it2 != it1)
{
it2--;
cout << *it2 << " ";
}
cout << endl;
it1 = ls.begin();
it2 = ls.end();
cout << "范围for遍历:" << endl;
for (auto e : ls)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
/*TestVector1();
TestVector2();*/
TestList1();
cout << endl;
TestList2();
return 0;
}
反向迭代器
先来看看反向迭代器的概念:
反向迭代器:rbegin,rend
先看STL里的:
begin:第一个数据
end:最后一个数据的下一个位置
这里可能有点奇怪,为什么end不是最后一个数据,而是最后一个数据的下一个位置,咱也不知道,反正STL是这样设计的
所以在设计反向迭代器的时候,可能是为了和正向迭代器对称,rend返回的是第一个数据的前一个位置,rbegin则返回的是最后一个数据
在对反向迭代器进行 ++ – 操作的时候,和正向迭代器是相反的
比如用正向迭代器遍历,beign++,知道begin == end就结束
rend是第一个数据的前一个位置,正常逻辑是,先将rend++,得到第一个数据,然后再不断的++
直到遍历完,可是大佬在设计的时候并不是这样想的,反向迭代器,得和正向迭代器反着来,所以
反向迭代器的++是往往前走,–才是往后走
我们来验证一下:
void TestRiterator1()
{
vector<int> vt;
list<int> ls;
int arr[] = {1,2,3,4,5};
for (auto e : arr)
{
vt.push_back(e);
ls.push_back(e);
}
vector<int>::reverse_iterator vtit1 = vt.rbegin();//最后一个元素
vector<int>::reverse_iterator vtit2 = vt.rend();//第一个元素的前一个位置
cout << "vector rbegin:" << *(++vtit1) << endl;
cout << "vector rend:" << *(--vtit2) << endl;
list<int>::reverse_iterator lsit1 = ls.rbegin();//最后一个元素
list<int>::reverse_iterator lsit2 = ls.rend();//第一个元素的前一个位置
cout << "list rbegin:" << *(++lsit1) << endl;
cout << "list rend:" << *(--lsit2) << endl;
}
下面来用反向迭代器来遍历:
void TestRiterator1()
{
vector<int> vt;
list<int> ls;
int arr[] = {1,2,3,4,5};
for (auto e : arr)
{
vt.push_back(e);
ls.push_back(e);
}
vector<int>::reverse_iterator vtit1 = vt.rbegin();//最后一个元素
vector<int>::reverse_iterator vtit2 = vt.rend();//第一个元素的前一个位置
cout << "vector_reverse_iterator" << endl;
while (vtit2 != vtit1)
{
--vtit2;
cout << *vtit2 << " ";
}
cout << endl;
list<int>::reverse_iterator lsit1 = ls.rbegin();//最后一个元素
list<int>::reverse_iterator lsit2 = ls.rend();//第一个元素的前一个位置
cout << "list_reverse_iterator" << endl;
while (lsit2 != lsit1)
{
--lsit2;
cout << *lsit2 << " ";
}
cout << endl;
}
反向遍历:
void TestRiterator1()
{
vector<int> vt;
list<int> ls;
int arr[] = {1,2,3,4,5};
for (auto e : arr)
{
vt.push_back(e);
ls.push_back(e);
}
vector<int>::reverse_iterator vtit1 = vt.rbegin();//最后一个元素
vector<int>::reverse_iterator vtit2 = vt.rend();//第一个元素的前一个位置
cout << "vector_reverse_iterator" << endl;
while (vtit1 != vtit2)
{
cout << *vtit1 << " ";
++vtit1;
}
cout << endl;
list<int>::reverse_iterator lsit1 = ls.rbegin();//最后一个元素
list<int>::reverse_iterator lsit2 = ls.rend();//第一个元素的前一个位置
cout << "list_reverse_iterator" << endl;
while (lsit1 != lsit2)
{
cout << *lsit1 << " ";
++lsit1;
}
cout << endl;
}
那么反向迭代器是如果实现的,直接说结论,在STL的设计中,反向迭代器是是使用正向迭代器封装的,仔细思考一下反向迭代器的++,不就是正向迭代器的–,反向迭代器的–,不就是正向迭代器的++
话不多说,直接上代码:
vector反向迭代器
template<class T>
struct Reserveiterator
{
typedef T* iterator;
iterator _it;
Reserveiterator(T* it)
:_it(it)
{}
T& operator * ()
{
//如果是rbegin,使用begin来封装,返回的是begin的上一个位置
//如果是rend,使用end来封装,返回的是end的上一个位置
iterator tmp = _it;
--tmp;
return *tmp;
}
Reserveiterator& operator++()
{
--_it;
return *this;
}
Reserveiterator& operator--()
{
++_it;
return *this;
}
bool operator !=(Reserveiterator it)
{
return _it != it._it;
}
bool operator ==(Reserveiterator it)
{
return _it == it->_it;
}
};
typedef Reserveiterator<T> reverse_iterator;
//反向迭代器
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
list反向迭代器
template<class T, class Ref, class Ptr>
struct Reserveiterator
{
//使用正向迭代器来封装
list_iterator<T,Ref,Ptr> _it;
Reserveiterator(list_iterator<T, Ref, Ptr> it)
:_it(it)
{}
T& operator *()
{
list_iterator<T, Ref, Ptr> tmp = _it;
--tmp;
return *tmp;
}
Reserveiterator& operator++()
{
--_it;
return *this;
}
Reserveiterator& operator--()
{
++ _it;
return *this;
}
bool operator !=(Reserveiterator it)
{
return _it != it._it;
}
bool operator ==(Reserveiterator it)
{
return _it == it._it;
}
};
typedef Reserveiterator<T, T&, T*> reverse_iterator;
//反向迭代器
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
来验证一下我们模拟实现的反向迭代器:
void TestRiterator2()
{
ysvector:: vector<int> vt;
yslist::list<int> ls;
int arr[] = { 1,2,3,4,5 };
for (auto e : arr)
{
vt.push_back(e);
ls.push_back(e);
}
ysvector::vector<int>::reverse_iterator vtit1 = vt.rbegin();//最后一个元素
ysvector::vector<int>::reverse_iterator vtit2 = vt.rend();//第一个元素的前一个位置
cout << "vector_reverse_iterator:" << endl;
while (vtit2 != vtit1)
{
--vtit2;
cout << *vtit2 << " ";
}
cout << endl;
vtit1 = vt.rbegin();
vtit2 = vt.rend();
while (vtit1 != vtit2)
{
cout << *vtit1 << " ";
++vtit1;
}
cout << endl;
yslist::list<int>::reverse_iterator lsit1 = ls.rbegin();//最后一个元素
yslist::list<int>::reverse_iterator lsit2 = ls.rend();//第一个元素的前一个位置
cout << "list_reverse_iterator:" << endl;
while (lsit2 != lsit1)
{
--lsit2;
cout << *lsit2 << " ";
}
cout << endl;
lsit1 = ls.rbegin();
lsit2 = ls.rend();
while (lsit1 != lsit2)
{
cout << *lsit1 << " ";
++lsit1;
}
cout << endl;
}
迭代器失效问题
来通过过一段代码观察:
为什么在插入数据后,it从1变成了0
在来观察一段代码:
删除一个数据后,怎么it从1变成了2
这个问题其实很简单,因为vector的迭代器使用的是原生指针,而vectr的插入是将数据往后挪动,删除是将数据往前挪动,但是it位置的指针是不会动的,只是数据在流通。
所以插入数据,只是从it位置开始,把数据向后挪动,然后再在it处赋值新数据:
删除也是同样的道理:
那什么时候指针会变呢?
扩容总空间:
在扩容空间的时候,会开辟一块新空间,然后将原有的数据拷贝到新空间中,旧空间的地址就会失效,我们来试验一下:
在不断的插入后,数组进行了扩容,那么原有指针全部失效了,迭代器自然也就失效了,
那么如何解决这个问题呢?
很简单,只需要给insert和erase函数加上返回值,每次执行完插入和删除操作都返回当前指针:
//指定位置前插入
iterator insert(iterator pos, T val)
{
assert(pos >= _start);
assert(pos <= _finish);
//判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capaicty() == 0 ? 4 : capaicty() * 2);
// 扩容后更新pos,解决pos失效的问题
pos = _start + len;
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
it--;
}
*pos = val;
_finish++;
return pos;
}
//指定位置删除
iterator erase(iterator it)
{
assert(it >= _start);
assert(it < _finish);
iterator start = it;
//将数据向前覆盖
while ( start< _finish - 1)
{
*start = *(start + 1);
start++;
}
_finish--;
return it;
}
以上就是C++容器迭代器的浅见