list介绍
list是stl中的一个容器,它是一个带头双向循环链表的线性结构,有头插,尾插等一系列操作,同时,也保存了迭代器的运用,但因为它存储的数据是指向一个空间的指针,实现起来十分繁琐
具体实现
list的成员变量
不同于vector,list存储的是一个指针,这个指针中需要有三个数据元素,一个是指向下一个节点的指针,一个是指向上一个节点的指针,还有一个是存储的数据(这个和vector相同),所以list实现时,我们用在list中存储的是一个结构体的指针,结构体的书写放在别处
template<class T>
struct List_Node
{
typedef struct List_Node<T> Node;
T data;
Node* next;
Node* prev;
List_Node(const T&x=T())
:data(x)
, next(nullptr)
, prev(nullptr)
{ }
};
这是一个模板类,可以让存储的数据更加灵活,在c++中,struct和class都是定义类的关键字,二者不同之处在于,struct中的默认限定访问符是puplic,class的默认限定访问符是private,当你试实例化一个类对象时,编译器会调用默认构造函数,如果自己没有写,编译器会自己生成,但编译器生成的默认函数不会处理内置类型,只会处理自定义类型,所以我们最好是自己书写一个构造函数,struct类和class类二者除了默认访问限定符存在差别,其他任何地方都没有差别,所以构造拷贝析构等函数都是可以自己写的。
list的迭代器
我们目前学习的迭代器有string和vector的的迭代器,认识到了迭代器的便利性,因为string和vector存储数据的方式为数组,它们的数据是存储在一段连续的地址空间上,所以可以用原生指针去实现迭代器会更加方便和容易,但list作为双向循环链表,它的数据是存储在基本不连续的空间上,且每个数据是一个存储最终数据的节点,如果在list中设计重载符号,是无法做到指针自加和自减操作,能做到的仅仅是返回节点指针,无法让节点指针自己进行额外操作,如自加自减等,所以我们需要对节点指针封装。
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T> self;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
这是普通迭代器的实现,它支持对指定空间存储的数据进行读写操作,也支持一系列的指针相关操作,解引用等等,我们其实可以发现,迭代器是模拟指针,但它的所有操作在表面上和原生指针的形式别无二致,但它实际上会多出别的动作,我们看不到它内部的动作,也不关心它的内部动作,我们需要的仅仅是它能和指针一样,这就是封装的好处
1.封装屏蔽底层差异和实现细节
2.提供统一的访问修改遍历方式
我们使用迭代器是为了更方便得寻找数据,并对数据进行读写操作,但我们有时仅仅希望,我们的数据只进行读,不进行写,为了更好的去读取数据,而百分百不去修饰它,我们往往会用const去修饰,使其不会被修改,const修饰指针变量却有额外的规定
const int*pa//代表pa指向的空间所存储的数据不会被修改,指针本身会被修改,这时的const修饰的是pa所指向的变量;
int*pa const//代表pa这个指针所保存的地址值不会被修改,这个地址所代表的空间中存储的数据是可以修改的,它仅仅是保护了指针变量,并未保护指针变量指向的空间
我们的迭代器是模拟指针的实现,从实现迭代器的代码来看,它的底层仍是指针,不过在外形式为_list_interator,如果我们单纯得只是在外面加上const,const修饰的会是指针变量,而不是去修饰指针所指向的空间
这是为什么呢?
_list_interator代表的是指针变量,这和你用const直接修饰int是一个道理,它让int变量不被改变,这里就是让指针变量不会被改变,但它原本指向的空间仍然会被改变。
如果我们仅仅是在原类中再次创建一个相同函数,并对其返回值类型加上const修饰,这也是没有用的,因为构不成函数重载,函数重载是指参数列表和参数排列顺序存在不同,而不是返回值类型不同。
这时候,我们就需要重新定义一个迭代器类,为const迭代器
cosnt迭代器
template<class T>
struct __list_const_iterator
{
typedef list_node<T> Node;
typedef __list_const_iterator<T> self;
Node* _node;
__list_const_iterator(Node* node)
:_node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
对迭代器的实现,代码量不大也不小,我们将它们当中两个不同的迭代器来使用,以应对不同的场景,但也可以省略,我们的迭代器是一个模板类,模板类的实例化参数不同,它们生成的模板类就是不同的模板,所以对普通迭代器,我们可以传一些参数,对cosnt迭代器,又传另外一些参数,以此来获得两个不同的迭代器
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* node)
:_node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
在上述代码中,我们对原本的迭代器类模板新添加了两个参数,一个是Ref,一个是Ptr,在实际应用中,我们就是靠这两个参数不同,来分化不同的迭代器
interator<T,T&,T*> 这是普通迭代器
interator<T,const T&,const T*> 这是cosnt迭代器
编译器会根据参数列表,实例化出两个不同的迭代器,受累的是编译器。
lsit类的实现
list中有许多函数,如尾插,头插,定点插等等,list因为是双向循环列表结构,它的插入和删除效率十分高效,空间利用率高,它的缺点就是不能随意访问,如果硬要达到vector那样的效果,效率会很低,同时,它的cpu缓存命中率很低,因为cpu读取数据是一段字节一段字节读取的,而list是链表,它的数据存储的空间是不连续的,无法做到读取这段数据的时候缓存下一段数据,而vector可以,所以vector的命中率高于list。
template<class T>
class list
{
typedef List_Node<T> Node;
public:
typedef conductor<T,const T&,const T*> const_conductor;
typedef conductor<T, T&, T*> conductor;//模板参数列表不同,所实例化的类也是两个不一样的类,如list<int>和list<double>是两个不同的类
void empty_intit()
{
head = new Node;
head->prev = head;
head->next = head;
}
list()
{
empty_intit();
}
void clear()
{
conductor it = begin();
while (it != end())
{
it = erase(it);
}
}
Node* BuyNode(const T& val)
{
Node* newnode = new Node;
newnode->data = val;
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
void push_back(const T& val)
{
Node* tail = head->prev;
Node* newnode = BuyNode(val);
tail->next = newnode;
newnode->prev = tail;
newnode->next = head;
head->prev = newnode;
size++;
}
void pop_back()
{
Node* prev = head->prev->prev;
Node* tail = head->prev;
prev->next = head;
head->prev = prev;
delete tail;
tail = nullptr;
size--;
}
void insert(Node*pos,T& val = T())
{
Node* prev = pos->prev;
Node* newnode = BuyNode(val);
prev->next = newnode;
newnode->prev = prev;
pos->prev = newnode;
newnode->next = pos;
size++;
}
conductor earse(Node* pos)
{
Node* prev = pos->prev;
Node* next = pos->next;
prev->next = next;
next->prev = prev;
delete pos;
size--;
return next;
}
void push_front(const T& val)
{
insert(head->next, val);
}
void pop_front(Node* pos)
{
earse(head->next);
}
const_conductor begin()const
{
return const_conductor(head);
}
const_conductor end()const
{
return const_conductor(head->prev);
}
conductor begin()
{
return conductor(head->next);
}
conductor end()
{
return conductor(head->prev);
}
private:
Node*head=nullptr;
int size = 0;
};