作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
个人主页:不 良
系列专栏:C++ Linux
学习格言:博观而约取,厚积而薄发
欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长!
list相当于带头节点的双向链表,我们定义节点时要用类模板参数,同时定义_next
、_prev
指针和数据_data
,使用struct定义类,因为节点类要能够被访问,而struct的默认访问权限就是public;构造函数缺省值要使用匿名对象,保证无论是自定义类型还是内置类型都能够构造成功。
//定义节点
template<class T>
struct list_node {
list_node<T>* _next;
list_node<T>* _prev;
T _data;
//节点构造函数,缺省值使用匿名对象
list_node(const T& x = T())
:_next(nullptr)
,_prev(nullptr)
,_data(x)
{
}
};
list是带头节点的双向链表,所以成员变量我们只需要定义一个头结点_head
,构造函数就是让头节点指向自己。
注意成员变量类型为类名 + 模板参数。
template<class T>
class list {
typedef list_node<T> node;
public:
list()
{
_head = new node;
_head->_prev = _head;
_head->_next = _head;
}
private:
node* _head;
};
vector和string的底层物理空间是连续的,我们可以通过指针的++或–来移动找到对应的元素,然而list的底层物理空间是不连续的,所以模拟实现list迭代器时,如果使用++
或 --
操作,++
或 --
的只是一个指针,并不能找到对应的位置。我们可以封装一个list迭代器类,实际上就是对结点指针进行封装,将各种运算符进行重载,使得在list类中能够像vector和string一样能够直接使用迭代器,而不用关心底层实现。
//迭代器类
template<class T>
struct __list_iterator {
typedef list_node<T> node;
typedef __list_iterator<T> self;
node* _node;
//构造一个迭代器,用结点的指针构造
__list_iterator(node* n)
:_node(n)
{
}
T& operator*()
{
return _node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
//不相等比较的就是结点的指针
bool operator!=(const self& s)
{
return _node != s._node;
}
};
测试:
void test_list1()
{
list<int> l1;
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
在上面的测试代码中list
这句话调用了拷贝构造,因为我们没有实现所以调用的就是编译器自动生成的即浅拷贝,这里之所以能够使用浅拷贝是因为迭代器类中没有写析构函数,而且不需要释放结点(结点的指针不属于迭代器,仅仅是使用list结点,没有拥有list结点,没有权利释放list的对象,结点的析构交给链表。迭代器只是一个工具)。
完善一些后置++、后置–之后的代码:
//迭代器类
template<class T>
struct __list_iterator {
typedef list_node<T> node;
typedef __list_iterator<T> self;
node* _node;
//构造一个迭代器,用结点的指针构造
__list_iterator(node* n)
:_node(n)
{
}
//重载解引用操作符*
T& operator*()
{
return _node->_data;
}
//重载前置++
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)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//不相等比较的就是结点的指针
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
当我们实现上述代码之后我们就可以在list类中实现begin和end函数:
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
当我们想要接收const对象时就需要先实现const_iterator
:如下的代码
void print_list(const list<int>& l1)
{
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
(*it) *= 2;
cout << *it << " ";
++it;
}
cout << endl;
}
我们在list类中实现下面的代码中编译能够通过:
iterator begin() const
{
return iterator(_head->_next);
}
iterator end() const
{
return iterator(_head);
}
但是为什么使用const修饰之后还能构造普通迭代器?这里的const修饰的是*this
,也就是this指针指向的内容,但是这里this指针指向的内容还是一个指针,修饰的是指针本身即修饰的_head
不能被改变,并不是_head
指向的内容,指针本身不能改变,但是可以拷贝给别人,也就是说可以将这个指针传到迭代器类中。
虽然上述代码可以通过编译了,但是并不能达到我们想要的效果,仍然能够改变list中的元素值,为什么呢?因为构造出来的迭代器是普通迭代器,可读可修改。
库中const对象调用的是const函数,返回的是const迭代器。
库中函数声明:
iterator begin();
const_iterator begin() const;
const迭代器和普通迭代器的区别是:const迭代器本身可以修改,const迭代器指向的内容不可以被修改。
那我们可不可以用下面这种方式定义const迭代器呢?
typedef __list_iterator<T> iterator; //T*
typedef const iterator const_iterator; // T* const,而我们想要的是迭代器内容不能被修改,const T*
不可以,普通迭代器对标的是指针即T*
,而const加在iterator前面保护迭代器本身不能被修改相当于保护指针不被修改即 T* const
,而我们想要的是迭代器指向的内容不能被修改即const T*
。
那我们想实现const迭代器应该怎么实现呢?很简单,只需要将普通迭代器代码稍加修改:
//const迭代器类
template<class T>
struct __list_const_iterator {
typedef list_node<T> node;
typedef __list_const_iterator<T> self;
node* _node;
//构造一个迭代器,用结点的指针构造
__list_const_iterator(node* n)
:_node(n)
{
}
//重载解引用操作符*
const T& operator*()
{
return _node->_data;
}
//重载前置++
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)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//不相等比较的就是结点的指针
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
再将list类中的begin函数和end函数重载:
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
此时我们再将print_list代码修改如下即可实现我们想要的功能:
void print_list(const list<int>& l1)
{
list<int>::const_iterator it = l1.begin();
while (it != l1.end())
{
//(*it) *= 2;
cout << *it << " ";
++it;
}
cout << endl;
}
观察发现,const迭代器类和普通迭代器类中只有*
重载运算符函数的返回值不一样,我们可以通过增加一个模板参数来控制返回值:
//迭代器类
template<class T,class Ref> //增加一个模板参数
struct __list_iterator {
typedef list_node<T> node;
typedef __list_iterator<T,Ref> self;
node* _node;
//构造一个迭代器,用结点的指针构造
__list_iterator(node* n)
:_node(n)
{
}
//重载解引用操作符*
Ref operator*()
{
return _node->_data;
}
};
然后在list类中通过模板参数控制返回值:
template<class T>
class list {
//将节点类型重命名为node
typedef list_node<T> node;
public:
//将迭代器类重命名为iterator
typedef __list_iterator<T,T&> iterator;
typedef __list_iterator<T,const T&> const_iterator;
private:
node* _head;
};
模板参数传引用或者传指针都可以,模板参数不同类型也就不同。
库里面的list迭代器类中一共有3个模板参数:
template<class T,class Ref,class Ptr>
我们要知道:
1.迭代器要么就是原生指针;
2.迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为(因为node*本身的++不符合需求,不是连续的空间)。
我们还需要重载一个->
,自定义类型的指针要用->
,内置类型用解引用*
就可以。
如下面的测试代码:一般的类最好都将默认构造加上,并且给上缺省值。
struct AA {
int _a1;
int _a2;
AA(int a1 = 0, int a2 = 0)
:_a1(a1)
,_a2(a2)
{
}
};
void test_list2()
{
list<AA> l1;
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
list<AA>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果报错信息如下:
我们可以通过重载流插入<<
和流提取>>
运算符解决问题,也可以通过下面的方式访问:
struct AA {
int _a1;
int _a2;
AA(int a1 = 0, int a2 = 0)
:_a1(a1)
,_a2(a2)
{
}
};
void test_list2()
{
list<AA> l1;
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
list<AA>::iterator it = l1.begin();
while (it != l1.end())
{
cout << (*it)._a1 << " " << (*it)._a2 << endl;
++it;
}
cout << endl;
}
但是这种方法有点麻烦,当我们有一个AA* ptr
类型的指针时,我们通常使用->
进行解引用:
AA* ptr = new AA(3,4);
cout << ptr->_a1 << " " << ptr->_a2 << endl;
所以迭代器也要去重载->
,因为模拟的是指针的行为,所以也要支持使用->
解引用。
在迭代器类中实现重载->
:
//重载->,这里返回的相当于AA*
T* operator->()
{
return &_node->_data;
}
然后测试代码就可以使用->:
void test_list2()
{
list<AA> l1;
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
l1.push_back(AA(1, 2));
list<AA>::iterator it = l1.begin();
while (it != l1.end())
{
//cout << it.operator->()->_a1 << it.operator->()->_a2 << endl;
//可优化为下面这样:
cout << it->_a1 << " " << it->_a2 << endl;
++it;
}
cout << endl;
}
而当是const迭代器的时候返回值就是const T*
,所以我们可以在迭代器类中再加一个模板参数Ptr用来表示->
返回值:
//迭代器类
template<class T,class Ref,class Ptr>
struct __list_iterator {
typedef list_node<T> node;
typedef __list_iterator<T,Ref,Ptr> self;
//重载->,这里返回的相当于AA*
Ptr operator->()
{
return &_node->_data;
}
}
在list类中通过模板参数控制返回值:
template<class T>
class list {
//将节点类型重命名为node
typedef list_node<T> node;
public:
//将迭代器类重命名为iterator
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T,const T&,const T*> const_iterator;
}
链表中只要实现了insert和erase,头插头删和尾插尾删都可以实现,所以我们先实现insert和erase函数。
在pos位置之前插入一个新结点。
//在pos位置之前插入一个新结点
void insert(iterator pos,const T& val)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(val);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
list的迭代器不会失效,因为pos的指向不会改变,且它们的位置关系没有改变。
测试代码:迭代器pos的指向都是同一个位置。
void test_list1()
{
list<int> l1;
l1.push_back(1);
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
//输出1
cout << endl;
list<int>::iterator pos = l1.begin();
l1.insert(pos, 5);
l1.insert(pos, 6);
l1.insert(pos, 7);
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
//输出5 6 7 1
cout << endl;
}
有了插入之后头插和尾插就可以复用insert:
//尾插
void push_back(const T& x )
{
//node* tail = _head->_prev;//这就是尾
//node* new_node = new node(x);
//tail->_next = new_node;
//new_node->_prev = tail;
//new_node->_next = _head;
//_head->_prev = new_node;
insert(end(), x);
}
//头插
void push_front(const T& x)
{
insert(begin(), x);
}
删除pos位置的结点。
void erase(iterator pos)
{
//头结点不能被删除
assert(pos != _head);
//记录pos位置结点的前后结点
node* prev = pos._node->_prev;
node* next = pos._node->_next;
//将pos结点移出list
prev->_next = next;
next->_prev = prev;
//释放结点
delete pos._node;
}
注意erase删除之后迭代器会失效。
迭代器失效是指迭代器所指向的节点失效,list中即节点被删除了,erase函数执行后,it所指向的节点被删除,因此it无效,在下一次使用it时,必须先给it赋值。
复用erase实现头删和尾删:
//尾删
void pop_back()
{
erase(--end());
}
//头删
void pop_front()
{
erase(begin());
}
注意引入模板参数之后类名已经不能做类型了,类名加模板参数才是类型。
**迭代器不需要关心模板参数是什么,直接使用就可以。vector的迭代器不一定是原生指针,有可能是被封装的。**我们可以通过typeid().name()
函数查看类型。
重载运算符可以由我们自己实现,关于迭代器失效问题具体问题具体对待。
list<int> l1;
//使用迭代器
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
//直接使用结点来定义
list_node<int>* pnode = l1._head->_next;
上面代码中分别使用迭代器和结点来直接定义,如果没有访问限定符的限制,那么上面两个一样吗?从物理空间上看一样,但是我们不能使用pnode遍历,因为++
的结果不一样,*pnode
是原生指针的解引用,pnode++
只是加了一个指针,因为空间不连续并不一定能找到下一个结点。
既然erase之后迭代器失效,那么就要对erase进行修改,要有返回值:返回当前位置的下一个元素。
iterator erase(iterator pos)
{
//头结点不能被删除
assert(pos != _head);
//记录pos位置结点的前后结点
node* prev = pos._node->_prev;
node* next = pos._node->_next;
//将pos结点移出list
prev->_next = next;
next->_prev = prev;
//释放结点
delete pos._node;
//返回下一个结点
return iterator(next);
}
清空list
void clear()
{
iterator it = begin();
while (it != end())
{
//it = erase(it);//erase会返回当前元素的下一个位置。
erase(it++);
}
}
我们也可以使用erase(it++),因为后置++返回的是++之前的值,erase的不是it,是返回的it的拷贝。
迭代器中后置++:
//重载后置++
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
默认构造函数
要push_back的前提要有哨兵位的头结点,所以要先建立一个头结点。我们可以学库里直接建立一个empty_init
函数,用来建立头节点,用来初始化,构造函数调用就可以了。
void empty_init()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
有迭代器区间的构造函数
template<class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
const对象可不可以调用构造函数?
可以,const对象定义的时候没有const属性,如:
const int n = 2
,否则const对象怎么初始化。
注意也要给定头节点,先进行初始化,调用empty_init
函数,再通过尾插将被拷贝list内容拷贝到新容器中。
传统写法:
void empty_init()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
list(const list<T>& lt)
{
empty_init();
for ( auto e : lt)
{
push_back(e);
}
}
现代写法:
//先实现交换函数,交换两个链表的头节点
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
list(const list<int>& lt)
{
empty_init();//初始化
list<T> tmp(lt.begin(), lt.end());//使用迭代器区间构造
swap(tmp);//调用swap函数交换头节点
}
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
//l1 = l2
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
通过传值传参调用拷贝构造函数构造出一个临时对象,然后通过交换头节点实现赋值。不能使用引用,使用引用就相当于将l3和l1的头节点交换,就改变了l3的值,相当于两者交换了并不能正确完成拷贝。
~list()
{
//先将list中数据清空
clear();
//再将头结点删除
delete _head;
//指向空
_head = nullptr;
}
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续的空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问任意元素的效率O(1) | 不支持随机访问,访问某个元素的效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要移动元素,时间复杂度为O(N),插入时可能需要增容(开辟新空间,拷贝元素,释放旧空间),导致效率更低 | 任意位置插入和删除效率高,不需要移动元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低, 缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入 元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代 器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
正向迭代器的++
运算符重载是向后走,即向着_next
指针走;而反向迭代器的++
运算符重载是沿着_prev
方向走,我们可以通过将list迭代器的代码改造一下:修改的地方主要就是将正向迭代器的重载++
运算符中改成_prev
,重载--
运算符中改为_next
。
list的反向迭代器代码如下:
//反向迭代器
template<class T, class Ref, class Ptr>
struct __list_reverse_iterator {
typedef list_node<T> node;
typedef __list_reverse_iterator<T, Ref, Ptr> self;
node* _node;
//构造一个迭代器,用结点的指针构造
__list_reverse_iterator(node* n)
:_node(n)
{
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
_node = _node->_prev;
return *this;
}
//后置++
self operator++(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
self& operator--()
{
_node = _node->_next;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
//不相等比较的就是结点的指针
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
然后在list类中将反向迭代器类重命名以及rbegin和rend函数:
template<class T>
class list {
//将节点类型重命名为node
typedef list_node<T> node;
public:
void empty_init()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//将迭代器类重命名为iterator
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T,const T&,const T*> const_iterator;
//将反向迭代器类重命名为 reverse_iterator
typedef __list_reverse_iterator<T, T&, T*> reverse_iterator;
typedef __list_reverse_iterator<T, const T&, const T*> const_reverse_iterator;
list()
{
empty_init();
}
list(const list<T>& lt)
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
//反向迭代器对应函数
reverse_iterator rbegin()
{
return reverse_iterator(_head->_prev);
}
reverse_iterator rend()
{
return reverse_iterator(_head);
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(_head->_prev);
}
const_reverse_iterator rend() const
{
return const_reverse_iterator(_head);
}
private:
node* _head;
};
测试:
void test_list()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//反向迭代器
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << (*rit) << " ";
++rit;
}
cout << endl;
}
输出结果:
上面begin和end、rbegin和rend的指向刚开始的位置如下:
但是库中的反向迭代器是通过正向迭代器完成的, 库中这种方法使反向迭代器作为一种正向迭代器的适配器模式,可以生产出任何容器的反向迭代器,只要正向迭代器可以正常工作,那么反向迭代器就能够正常的工作。库中begin和end、rbegin和rend的指向刚开始的位置如下:
如果是上面这样刚开始头结点的位置不能解引用即rbegin位置不能解引用,此时我们如果想要实现倒序遍历就要对*
重新进行重载,在重载*
函数内让其对上一个位置进行解引用。
重载解引用返回的时候,返回值是个问题,我们可以再定义两个模板参数解决。此时我们就不用再用上面那种反向迭代器实现,可以在新的头文件中定义:
#pragma once
namespace Niu {
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator {
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
Iterator _cur;
//我去用正向迭代器去构造一个方向迭代器
ReverseIterator(Iterator it)
:_cur(it)
{
}
//重载*
Ref operator*()
{
Iterator tmp = _cur;//拷贝构造
--tmp;
return *tmp;//返回上一个结点
}
Self& operator++()
{
--_cur;
return *this;
}
Self& operator--()
{
++_cur;
return *this;
}
bool operator!=(const Self& s)
{
//反向迭代器和正向迭代器都是指针可以直接判断
return _cur != s._cur;
}
};
}
在类list中定义:
typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;
此时list类中的rbegin和rend函数也要修改:
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
此时再测试:
void test_list()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//反向迭代器
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << (*rit) << " ";
++rit;
}
cout << endl;
}
测试结果:
我们使用模板,只要实现了一个迭代器,所有的迭代器都出来了,但是要求必须是双向迭代器,支持--
才能使用,如list和vector。
list可以拷贝一份正向迭代器改造成反向迭代器,但是vector不能解决,因为vector本身就是内置类型。此时我们通过模板给vector的正向迭代器可以得到vector的反向迭代器:
namespace Niu {
template<class T>
class vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
……
};
}
测试:
void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//正向遍历
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//反向迭代器
vector<int>::reverse_iterator rit = v.rbegin();
while (rit != v.rend())
{
cout << (*rit) << " ";
++rit;
}
cout << endl;
}
测试结果: