C++初阶 -- 模拟实现list
一、list中结点的构成

从源码上看,list的成员变量是link_type node。但是这个link_type是什么类型呢?

link_type 是由list_node*重定义得到的,那list_node*又是什么类型呢?

list_node是由_list_node<T>这个含有类模板的类型重定义而来的
而这个_list_node类中分别定义了一个next指针和prev指针,其作用就是分别指向前一个结点和后一个结点。还定义了存储结点数据的变量。
总结:从源码上看,list的底层实际上是一个带头的双向循环链表。

二、list结点的模拟实现
上面就已经说过list的底层实际上是一个带头双向循环链表,那每一个结点中必定有一个指向上一个结点的指针,一个指向下一个结点的指针和存储结点数据的变量(前驱指针,后继指针,数据)。
对于结点的成员函数,我们只实现构造函数即可,析构可以在list的析构函数中一并释放。
// 节点的类模板
template<class T>
struct ListNode
{// 成员函数ListNode<T>* _next;ListNode<T>* _prev;T _data;// 带参的构造函数ListNode(const T& x = T()):_next(nullptr),_prev(nullptr),_data(x){}
};模板参数类型是内置类型就调用内置类型的默认构造函数,是自定义类型就调用自定义类型的默认构造函数。
三、list迭代器类的模拟实现
为什么list的迭代器要用类来模拟实现,而string、vector的迭代器却不用定义一个类来实现迭代器?
因为string和vetor的数据都是存储在一个连续的空间中,想要访问容器中存储的数据只需要使用指针进行++、--、*解引用等操作就能对相应位置的数据进行一系列的操作。所以string和vector的迭代器使用原生指针即可!!
而list的每个结点存放的空间并不是连续的,因此想要迭代器实现各种功能就需要对迭代器进行封装。

迭代器存在的意义:就是让使用者想访问容器内部的数据时不必关心底层的实现,可以使用简单的方式来对数据进行访问和操作!!!
因此list的结点并不符合迭代器的使用方式,所以就需要对迭代器进行封装,对迭代器内部的运算符进行重载,使得能像使用string和vector的迭代器一样使用list的迭代器。
例:当对结点进行++操作时,实际上在底层是 p = p->next 这样实现的。
总结:在list容器中,迭代器实际上就是指针。但由于list底层并不是一个连续的空间,所以++和--都无法完成工作,就连解引用*都与理想的结果不一样。于是就将原生指针封装到一个类中,在这个类中实现重载实现运算符的操作!!
源码




参数说明
为什么实现的迭代器模板有三个参数?
template<class T, class Ref, class Ptr>在实现list的迭代器时,我们肯定要实现出const和非const的迭代器
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;那Ref对应的就是引用类型,Ptr对应的是指针类型。
那为什么要有三个参数呢?
原因就是迭代器并不只能遍历结点的数据,还要获取结点的数据、地址。所以需要三个参数
成员对象
typedef __list_iterator<T, Ref, Ptr> self;
typedef ListNode<T> Node;
Node* _node; // 结点指针实现迭代器和结点的类用的都是struct,因为struct默认访问权限是public,能直接访问到类的成员变量。
构造函数
迭代器类实际上是对结点指针进行封装,所以只需要对结点指针进行初始化即可。
__list_iterator(Node* x):_node(x)
{}++运算符重载
1、前置++运算符
先将结点指针指向下一个结点。随后返回该结点指针。
// 前置++ -- 先++,再使用
self& operator++()
{_node = _node->_next;return *this;
}2、后置++运算符
先用结点指针构造一个临时对象,然后将结点指针指向下一个结点,最后返回这个临时对象。
// 后置++ -- 先使用,再++
self operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}--运算符重载
1、前置--运算符
先将结点指针指向上一个结点。随后返回该结点指针。
// 前置-- -- 先--,再使用
self& operator--()
{_node = _node->_prev;return *this;
}2、后置--运算符
先用结点指针构造一个临时对象,然后将结点指针指向上一个结点,最后返回这个临时对象。
// 后置-- -- 先使用,再--
self operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}*解引用运算符重载
使用解引用运算符是想得到当前结点的数据内容,所以只需要返回当前指向结点的数据内容即可。
// *解引用
/*T&*/
Ref operator*()
{return _node->_data;
}==运算符重载
想要判断两个迭代器是否相同,只需要判断两个迭代器中的结点指针是否相同即可。
bool operator==(const self& s)
{return s._node == _node;
}!=运算符重载
与==运算符相反,只需要判断两个迭代器中的结点指针是否不相同即可。
// 两个迭代器相比较
bool operator!=(const self& s)
{return s._node != _node;
}->运算符重载
使用->运算符的左操作数得是指针类型,因此对于->运算符的重载就只需要返回结点数据的地址即可。
但又有疑问了,不应该是使用两个->运算符才能正确访问到想要的数据吗。第一个->是调用重载的->获取到结点数据指针,第二个才是通过结点数据的指针来访问到对象中的变量。但是这样写观感太差了,于是编译器就为了增加代码的可读性,省略了一个->。
// ->重载
Ptr operator->()
{return &_node->_data;
}四、list的模拟实现
成员函数
private:// 头结点(哨兵位)Node* _head;构造函数
list是一个带头双向循环链表,那list的构造函数只需要初始化一个头结点并将前驱指针和后继指针都指向自己即可。

// 初识化哨兵位
void emp_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;
}// 无参的构造函数 -- 初始化头结点
list()
{emp_init();
}拷贝构造函数
先构造一个头结点,随后将被拷贝list的结点通过循环依此添加到新的头结点后。
// 拷贝构造
// lt2(lt1)
list(list<T>& lt)
{emp_init();for (auto i : lt){push_back(i);}
}赋值运算符重载函数
1、传统写法
先排除自己给自己赋值的情况,随后通过clear函数将待赋值的list内部结点清空,只留下头结点。随后将被赋值list的结点尾插到待赋值list的头结点后。
// 传统写法
list<T>& operator=(list<T>& lt)
{if (this != lt._head){clear();for (auto i : lt){push_back(i);}}
}2、现代写法
首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换即可。
这样做相当于将应该用clear清理的数据,通过交换函数交给了容器lt,而当该赋值运算符重载函数调用结束时,容器lt会自动销毁,并调用其析构函数进行清理。
// 现代写法
list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}析构函数
先调用clear函数将容器内部结点释放,随后释放头结点,最后将头结点指针置空。
// 析构函数
~list()
{clear();delete _head;_head = nullptr;
}clear函数
释放除头结点以外的所有结点。
// 清除数据
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}begin函数和end函数
begin函数是返回头结点的下一个有效结点的迭代器,而end函数返回的是最后一个有效结点的下一个结点的迭代器。
iterator begin()
{// 显示类型转换//return iterator(_head->_next);// 隐式类型转换return _head->_next;
}iterator end()
{return _head;
}const begin函数和const end函数
const_iterator begin() const
{return _head->_next;
}const_iterator end() const
{return _head;
}插入、删除函数
1、insert函数
给迭代器之前插入一个函数,随后返回插入后结点的迭代器。

// 插入节点
iterator insert(iterator pos, const T& x)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);// prev newnode cur prev ->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return newnode;
}2、erase函数
删除当前迭代器指向的结点,返回指向删除结点的下一个结点的迭代器。

// 删除节点
iterator erase(iterator pos)
{assert(pos != end());Node* cur = pos._node;Node* pre = cur->_prev;Node* next = cur->_next;pre->_next = next;next->_prev = pre;delete cur;return next;
}
3、push_back函数和push_front函数
push_front函数在第一个结点前插入一个结点
push_back函数在头结点前插入一个结点
// 尾插节点
void push_back(const T& x)
{// 创建一个新结点并初始化/*Node* newnode = new Node(x);Node* tail = _head->_prev;tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;*/insert(end(), x);
}// 头插节点
void push_front(const T& x)
{insert(begin(), x);
}4、pop_back函数和pop_front函数
pop_back函数删除头结点前的一个有效结点
pop_front函数删除第一个有效结点
// 尾删结点
void pop_back()
{erase(--end());
}// 头删结点
void pop_front()
{erase(begin());
}5、swap函数
// 交换
void swap(const list<T>& x)
{std::swap(_head, x._head);
}注意: 在此处调用库当中的swap函数需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。
