目录
一、list类的介绍
二、list的使用
1.构造、拷贝构造函数和迭代器
2.数据的增删查改
三、list的部分接口实现
1.节点定义
2.list类的组织
四、list的迭代器
1.迭代器的设计思路
2.const迭代器
3.->操作符的重载
4.反向迭代器
一、list类的介绍
list就是C++库中对链表的实现,它的底层是一个带头双向循环链表。
与vector这样的数据结构相比,链表最大的缺陷是不支持随机访问。访问list的第n个元素就必须从一个已知位置(常见的是头部或者尾部)向前或向后走到该位置,需要一定的时间开销。同时list还需要额外的空间保存前后节点的地址。
二、list的使用
1.构造、拷贝构造函数和迭代器
explicit list (const allocator_type& alloc = allocator_type());————默认构造,缺省参数alloc我先不讲
explicit list (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type());————构造的list中包含n个值为val的元素
template
list (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());————用[first, last)区间中的元素构造list
list (const list& x);————拷贝构造函数
#include<iostream>
#include<list>
using namespace std;
void test()
{
list<int> l1;//构造空list
list<int> l2(4, 100);//l2中放4个值为100的元素
list<int> l3(l2.begin(), l2.end());//用l2的[begin(), end())左闭右开区间构造l3
list<int> l4(l3);//用l3构造l4
//迭代器区间的构造可以是任何一种数据结构的迭代器,甚至是原生指针,下面就是以数组的原生指针为迭代器的构造
int arr[] = { 16, 2, 77, 29 };
list<int> l5(arr, arr + sizeof(arr) / sizeof(arr[0]));
//C++11中引入了列表式的初始化
list<int> l6{ 1, 2, 3, 4, 5 };
//用迭代器可以遍历元素,范围for底层也是使用迭代器
list<int>::iterator it = l6.begin();
while (it != l6.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
test();
return 0;
}
2.数据的增删查改
void push_front (const value_type& val);————在list头部插入值为val的元素
void pop_front ();————删除list中第一个元素
void push_back (const value_type& val); 在list尾部插入值为val的元素
iterator insert(iterator position, const value_type& val);————在position位置插入值为val的元素
iterator erase (iterator position);————删除position位置的元素
void swap (list& x);————交换两个list中的元素
void clear();————清空list中的有效元素
void empty();————检测list是否为空
size_t size() const;————返回list中有效节点的个数
#include<iostream>
#include<list>
using namespace std;
void test()
{
list<int> l1;
l1.push_front(2);
l1.push_front(3);
l1.push_front(4);
l1.insert(l.begin(), 1);
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
//1234
cout << endl;
l1.pop_front();
l1.erase(l1.begin());
vector<int> l2;
l2.swap(l1);
it = l2.begin();
while (it != l2.end())
{
cout << *it << " ";
++it;
}
//34
cout << l2.empty();
l2.clear();
it = l2.begin();
while (it != l2.end())
{
cout << *it << " ";
++it;
}
//
}
int main()
{
test();
return 0;
}
三、list的部分接口实现
在list的实现中需要三个类,一个是节点类,一个是list类,还有一个是迭代器类,全部都存放在my_list的命名空间中。
1.节点定义
list我们使用一个双向链表实现,那么一个节点就需要指向前节点和后节点的指针还有储存的数据本身
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
//构造函数
list_node(const T& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
2.list类的组织
在list类中我们需要一个保存哨兵卫的成员变量。由于链表不支持随机访问,所以也可以加上一个size_t _size的变量保存节点的个数,如果不加的化需要一个O(N)的遍历函数,相比直接获取会快得多。
template <class T>
class list
{
typedef list_node<T> node;
public:
//成员函数
private:
node* _head;//哨兵卫头节点
};
下面是大部分接口的模拟实现:
template <class T>
class list
{
typedef list_node<T> node;
public:
//构建空链表
void empty_initialize()
{
_head = new node(T());//创建一个哨兵卫头
_head->_next = _head;
_head->_prev = _head;//prev和next都指向哨兵卫
_size = 0;
}
//默认构造
list()
{
empty_initalize();
}
//拷贝构造
list(const list<T> & lt)
{
empty_initalize();//创建空链表
for (const auto &e : lt)//将每一个数据按顺序尾插
{
push_back(e);
}
return *this;
}
//析构函数
~list()
{
clear();//清空数据
//释放哨兵卫并置空
delete _head;
_head = nullptr;
}
//清空数据
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//一个一个释放
}
}
//赋值运算符重载可能会涉及深拷贝
list<T>& operator=(const list<T>& lt)
{
if (this != <)//避免自己给自己赋值
{
clear();//清空数据
for (const auto&e : lt)
//将每一个数据按顺序尾插,而且用引用还避免了深浅拷贝的问题
{
push_back(e);
}
}
}
//任意迭代器位置插入
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos._pnode;//迭代器时一个类,类中找这个原生指针
node* prev = cur->_prev;
prev->_next = newnode;//链接前面
newnode->_prev = prev;
newnode->_next = cur;//链接后面
cur->_prev = newnode;
return iterator(newnode);
}
//头插
void push_front(const T& x)
{
insert(begin(), x);//调用insert
}
//头删
void pop_front()
{
erase(begin());//调用erase
}
//尾删
void pop_back()
{
erase(--end());
}
//尾插
void push_back(const T& x)
{
insert(end(),x)
}
iterator erase(iterator pos)
{
assert(pos != end());//不能删除头节点
node* prev = pos._pnode->_prev;//保存pos前的节点
node* next = pos._pnode->_next;//保存pos后的节点
prev->_next = next;//将后节点链接前节点
next->_prev = prev;//将前节点链接后节点
delete pos._pnode;//删除pos节点--会失效
return iterator(next);//返回pos节点后的节点
}
size_t size()
{
int i = 0;
list<int>::iterator it = begin();
while(it != end())
{
it++;
i++;
}
return i;
}
private:
node* _head;
};
四、list的迭代器
1.迭代器的设计思路
迭代器在各种不同的数据结构中有着巨大的优势:迭代器可以提供统一的迭代方式。
它的设计思路一般有两种:一种是连续的内存访问可以使用原生指针的封装加以实现,另一种是链表这样即开即用的数据结构,它的迭代器增减是通过多次向前或向后移动节点指向实现的,解引用是通过获取迭代器内部的变量来实现,list使用的思路就是后者。
所以虽然各种数据结构的访问方式是一致的但是底层实现可能完全不同,这也是封装特征作为面向对象语言的基本特征巨大优势。
迭代器会专门设计一个类,成员变量保存节点指针。
template<class T>
struct __list_iterator
{
typedef list_node<T> node;
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
T operator*()//非const
{
return _pnode->_data;
}
//前置++,迭代器内部指针后挪
__list_iterator<T>& operator++()
{
_pnode = _pnode->_next;
return *this;
}
//后置++,迭代器内部指针后挪
__list_iterator<T> operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
//前置--,迭代器内部指针前挪
__list_iterator<T>& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
//后置--,迭代器内部指针前挪
__list_iterator<T> operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
//迭代器的相同和不同不应该是结构体成员的一致,而应该是内部储存的节点的指针一致才可以
bool operator!=(const __list_iterator<T>& it) const
{
return _pnode != it._pnode;
}
bool operator==(const __list_iterator<T>& it) const
{
return _pnode == it._pnode;
}
};
2.const迭代器
有些人会简单地这样写:const list::iterator lt1 = lt.begin();
只是在迭代器前加上const的话只是让迭代器本身不能被修改,当然也不能+或者-。
const迭代器应该是迭代器中的元素不能被修改。所以我们只需要一个用const修饰返回值的operate*函数,那么我们可以试试函数重载,但根据上面的错误,我们可以明确认定这个函数的重载一定不能用const修饰this指针。
所以这两个函数应该是这样的:
T& operator*()
{
return _pnode->_data;
}
const T& operator*()
{
return _pnode->_data;
}
但是我们需要重新熟悉以下函数重载的要求:两个函数的函数名和参数相同则两个函数构成重载。
它们只是返回类型不同,不构成重载,而是重定义。此时我们只能为了一个函数的正当存在而去为它创建另一个类:const_iterator
这个const_iterator中,相比于非const迭代器其实功能发生改变的只有:T& operator*()的返回类型用const修饰。
结果大概就是这样的:
struct __list_iterator
{
//省略重复的代码
T& operator*()
{
return _pnode->_data;
}
//省略重复的代码
};
struct const_iterator
{
//省略重复的代码
const T& operator*()
{
return _pnode->_data;
}
//省略重复的代码
};
但是又写一堆重复的代码,不觉得很难受吗?
所以我们在其中引入了迭代器类的第二个参数claas Ref,这个Ref的类型不同,它实例化出来的类也是不同的。所以我们可以通过:__list_iterator和__list_iterator两种显示实例化来做到让编译器生成两个类。
也就是说,为了解引用重载前面的const我们必须要定义一个新类,但是我们生成这个类的工作交给了编译器,而不是我们自己来完成。
而且把__list_iterator这个类命名为Self,这样也方便我们对代码的修改。
template<class T, class Ref>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref> Self;
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
Ref operator*()
{
return _pnode->_data;
}
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
bool operator!=(const Self& it) const
{
return _pnode != it._pnode;
}
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
};
3.->操作符的重载
我们之前在C语言就学过->操作符,当时我们说对于一个结构体指针p可以有以下效果:
#include<stdio.h>
struct test
{
int a;
};
int main()
{
struct test example;
example.a = 0;
struct test* p = &example;
printf("%d",p->a);
printf("%d",(*p).a);
//二者打印的结果是相同的
return 0;
}
但是->的重载和C语言中不太一样,它的重载函数返回的一定是指针,这个指针可以是任何一种,但是必须是一个指针。
那在C++中,我们又该如何理解以上面的struct test为基础的p->a呢?
我再写一段代码:
#include<iostream>
class test
{
public:
test()
:a(0)
{}
int* operate->()
{
return &a;
}
private:
int a;
};
int main()
{
test example;
printf("%d",(example->a));
return 0;
}
应该是这样的,p->a可以理解为p->->a,中间省略了一个->,第一个箭头是函数调用,返回a的地址,而后面的->可以理解为解引用。所以不要一看到->操作符就说是解引用,它还可能是函数调用。
我们此时也想重载一下这个->,像下面这样:
T* operator->()
{
return &_pnode->_data;
}
但是此时问题就又发生了。我们使用的*操作符重载可以达到const或非const的效果,但是如果我们只写一个非const版本,人们就可以借用这个指针来改掉本不想修改的内容。也就是说,我们还要面对和上面一样的情况,为一个函数而多生成一个类,是这样没错,但是这个函数可以放到const迭代器的类中,只是多一个参数控制它而已。此时我们就引入了模板的第三个参数,编译器依旧形成两个类。
它们是:普通迭代器__list_iterator和const迭代器__list_iterator
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> Self;
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
Ptr operator->()
{
return &_pnode->_data;
}
Ref operator*()
{
return _pnode->_data;
}
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
bool operator!=(const Self& it) const
{
return _pnode != it._pnode;
}
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
};
4.反向迭代器
我们学习STL的目的就在于这些数据结构都有大佬已经实现好了,不像C语言中我们使用各种数据结构还需要实现该数据结构的各种接口。既然有现成的为什么不用呢?使用他人的代码实现另一个功能的方式就叫做代码的复用。
也就是说反向迭代器其实就是正向迭代器的封装,在STL的实现中也是一样的。
反向迭代器的rbegin就是正向迭代器的end,而反向迭代器的rend就是正向迭代器的begin,只是我们在重载解引用时需要在当前正向迭代器位置先前挪一位再解引用,所有反向迭代器的++和--都和正向对调就可以了。
self& operator++()
{
--current;
return *this;
}
self operator++(int)
{
self tmp = *this;
--current;
return tmp;
}
self& operator--()
{
++current;
return *this;
}
self operator--(int)
{
self tmp = *this;
++current;
return tmp;
}