list模拟实现(简单版)【C++】
目录
前言
1. list的私有成员
2. 构造函数
2.1 list构造函数
3. list遍历
3.1 push_back
3.2 ListIterator模拟实现
3.2.1 成员变量_node
3.2.2 it++ 和 ++it
3.2.3 it-- 和 --it
3.2.4 *it
3.2.5 operator== 和 operator!=
3.2.6 operator->
3.3 begin 和 end
3.4 遍历测试
4. list 的增加和删除
4.1 empty 和 size
4.2 insert 和 erase
4.2.1 insert
4.2.2 erase
4.3 push_back(plus)、push_front 和 pop_back、pop_front
4.4 测试代码
5. const 类型迭代器
6. list析构函数 和 拷贝构造
6.1 析构函数
6.2 拷贝构造
6.3 operator=
前言
Q: 什么是list?
A: 参考标准库里面的解释std::list
list就是一个序列容器,支持常数级别的时间复杂度的插入和删除。list底层的数据结构是带头双向循环链表,这样每个数据元素就可以在内存中是非相邻的。
如果对带头双向循环链表不熟悉的话,请猛戳这里带头双向循环链表
接下来 list的模拟实现的简单版。
文件准备:
在 vs 2022中创建头文件和测试文件
然后在list.h文件中创建my_list 命名空间,在my_list来模拟实现list类,在test.cpp文件中创建主函数来调用测试接口。
因为list底层的数据结构是带头结点的双向循环链表,这里需要一个节点的数据结构。
#pragma once
#include<iostream>
#include<assert.h>using namespace std;namespace my_list
{//双向链表结构体节点模板template<typename T>struct ListNode {ListNode<T>* _next;ListNode<T>* _prev;T _data;//节点的构造函数 用来初始化节点数据ListNode(const T& x = T()):_next(nullptr),_prev(nullptr),_data(x){}};//类似双向链表类模板template<class T>class list {typedef ListNode<T> Node;public:private:Node* _head;};void test_list1(){}
}
为什么使用 struct ?
因为里面的数据需要公开的,因为list本质是带头双向循环链表,不公开list就没法使用。
值得注意的是,节点的构造函数中的 T()
表示类型 T
的默认构造值。
当调用不提供参数时,默认使用类型T的默认值。
主要分为内置类型和自定义类型
-
内置类型:
-
int()
→0
-
double()
→0.0
-
bool()
→false
-
char()
→'\0'
-
指针类型 →
nullptr
-
-
自定义类型:
-
调用该类型的默认构造函数
-
如果没有默认构造函数,编译会报错
-
1. list的私有成员
因为list本质是带头双向循环链表,所以需要一个节点指针指向头节点,还需要一个计数器_size来统计节点个数。
//list 类中的私有成员private:Node* _head; //指向头结点size_t _size; //记录结点个数
2. 构造函数
2.1 list构造函数
在不考虑内存池的情况下的list构造函数,首先创建一个节点,然后头节点的_next指向自己,最后头结点的_prev也指向自己。
list()
{_head = new Node; // 1. 创建一个头节点(哨兵节点)_head->_next = _head; // 2. 头节点的 next 指向自己_head->_prev = _head; // 3. 头节点的 prev 也指向自己_size = 0; // 4. 方便计算节点个数
}
3. list遍历
3.1 push_back
在实现遍历之前,首先保证list里面有元素,所以先实现一个尾插元素。这里与链表的尾插相似。
//插入一个数据//尾插数据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;_size++;}
使用带头节点的优势就是:当只有一个头节点(哨兵卫)的情况 和 其他情况 进行插入节点都是一样的。
3.2 ListIterator模拟实现
list的遍历需要使用迭代器去遍历。使用迭代器的意义就是不管底层是什么,都可以进行访问。
Q:这里的原生指针可以充当迭代器吗?
A:不可以
因为list和顺序表不一样,在顺序表中,原生指针是天然的迭代器(前提是T*指向的物理空间是连续的);
而list中的原生指针Node*指向的物理空间是不连续的(因为节点是new出来的,不能保证每一个节点的地址都是连续的)。
既然list的原生指针不可以的话,所以封装一个类用自定义类型去重载运算符,因为C++中的类和运算符重载可以去控制其行为。
迭代器用什么构造? 节点的指针就可以的,只不过是用类进行封装。
所以这要再命名空间my_list中额外写一个类进行控制。
3.2.1 成员变量_node
这里的使用_node来指向链表中的节点,这里要写成公有类,方便外部使用迭代器去调用。
template<class T>struct ListIterator {//自定义类型封装指针,去控制其行为typedef ListNode<T> Node; //模板类重命名为Nodetypedef ListIterator<T> Self; //模板类重命名为SelfNode* _node; // 指向当前迭代器所代表的链表节点的指针//构造函数ListIterator(Node* node) :_node(node)};
3.2.2 it++ 和 ++it
因为原生指针不可以充当迭代器,所以这里使用专门封装的类中的运算符重载来进行控制。
3.2.2.1 it++
it++, 这里需要考虑的是先使用后++,返回的是之前的节点,先保存原来的节点,再进行++。
//后置++,使用(int)来区分前置和后置Self operator++(int) {//后置++ 返回之前的值Self tmp(*this); //这里调用了拷贝构造,指针内置类型的浅拷贝,因为希望指向同一个空间//迭代器也不需要写析构,因为节点不属于迭代器,节点属于链表,所以这里不需要析构_node = _node->_next;return tmp;}
注意:
Q :为什么后置++ 返回T 而不是 T& (或者 为什么后置++是返回临时变量) ?
A :这里返回临时变量,因为后置++ 返回的是自增前的旧值,而旧值是一个临时对象(不能返回局部变量的引用)。前置++ 返回的是自增后的对象本身
3.2.2.2 ++it
++it, 前置++,返回++以后的节点,_node 的下一个节点。
//重载前置++Self& operator++() {_node = _node->_next;//前置++,返回++以后的值return *this;}
注意:
Q :前置++为什么返回引用?
A : 因为前置++ 返回的是自增后的对象本身(*this
),而 *this
的生命周期,不会立即销毁。
值得注意的是,这里使用 operator(int)++ 来进行区分 前置与后置。
3.2.3 it-- 和 --it
这里自减与上述自增类似。
// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--() {_node = _node->_prev;return *this;}
3.2.4 *it
这里需要注意的是,解引用来获取data,不能传值返回,传值返回的是data的拷贝,因为*it 有读和写的功能,传引用返回就可以进行读写data。
T& operator*() {return _node->_data;}
3.2.5 operator== 和 operator!=
这里只需比较节点的指针就可以,两个迭代器如果它们里面的指针是相同的,它们就是相等的,不相同就是不相等的。
bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}
3.2.6 operator->
这里为了提高可读性,数据访问时,由it.operator->()->_a1 直接变为 it->_a1。
//在C++11中支持多参数构造的隐式类型转换struct A {int _a1;int _a2;A(int a1 = 1,int a2 = 1):_a1(a1),_a2(a2){}};void test_list3() {list<A> lt;A aa1(2, 2);A aa2 = {3,3};lt.push_back(aa1);lt.push_back(A(2,2));lt.push_back({3,3}); //C++11多参数的隐式类型转换list<A>::iterator it = lt.begin();cout << it->_a1 << endl;cout << it.operator->()->_a1 << endl;}
3.3 begin 和 end
使用begin ,因为想使用_head->next 去构造节点,因为_head是私有的,所以使用公有的begin,返回第一个元素的迭代器。
begin返回头结点的下一个节点即可。
普通写法
iterator begin(){//1 普通版iterator it = _head->_next;return it;}
匿名对象写法
iterator begin() {//2 匿名对象版return iterator(_head->_next);}
单参数构造写法
iterator begin() {//3 单参数构造函数支持隐式类型转换//这里的迭代器就是单参数构造函数return _head->_next;//构造函数//ListIterator(Node * node)// :_node(node)//{}}
end返回头结点即可(因为list本质是带头节点的双向循环链表)。
iterator end() {//其他写法同begin类似return _head;}
3.4 遍历测试
这里可以在命名空间my_list中进行测试。
void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);//list的遍历需要使用迭代器list<int>::iterator it = lt.begin();while (it != lt.end()) {cout << *it << " ";++it;}cout << endl;}
输出结果 :
4. list 的增加和删除
4.1 empty 和 size
在实现list的增加和删除这里需要先实现,判空和计算节点个数的接口。
bool empty() const{return _size == 0;}size_t size() const{return _size;}
4.2 insert 和 erase
4.2.1 insert
这里要实现一个 在pos节点之前插入一个值为val的函数。
先用cur指向pos节点,再创建一个值为val的节点,再用prev指向cur的前一个节点。
void insert(iterator pos , const T& val) {Node* cur = pos._node; //cur指向pos位置的节点Node* newnode = new Node(val);Node* prev = cur->_prev;//在cur前面插入一个节点// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;}
4.2.2 erase
删除pos位置的节点,链表只需修改指针域即可。
iterator erase(iterator pos) {//避免空assert(!empty());//删除pos位置的节点//prev cur nextNode* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;//pos 失效 是 释放了pos位置的空间//为了避免失效,要返回一下节点的迭代器return iterator(next);}
值得注意的是,erase返回类型是iterator,避免迭代器失效,要返回下一个节点的迭代器。
4.3 push_back(plus)、push_front 和 pop_back、pop_front
这里借助上方实现的insert 和 erase 来进行实现。
//push_back 现代写法void push_back(const T& x){insert(end(),x);}//头插void push_front(const T& x) {insert(begin(),x);}//尾删void pop_back() {erase(--end()); //注意这里不能end()-1 ,因为这里是使用运算符重载来实现的}//头删void pop_front() {erase(begin());}
值得注意的是,pop_back去调用end()时,不能使用end-1 ,运算符重载只实现了operator--()。
4.4 测试代码
在test.cpp中进行调用,test_list2() 函数在my_list命名空间中进行实现。
void test_list2() {list<int> lt;lt.push_back(2);lt.push_front(1);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;//头删lt.erase(lt.begin());it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;//头删lt.pop_front();//尾删lt.pop_back();for (auto e : lt) {cout << e << " ";}cout << endl;cout << "元素个数:"<<lt.size() << endl;}
5. const 类型迭代器
我们知道:
const int* ptr; // const 在*的左边,修饰的是指针指向的数据不能被修改int* const ptr = nullptr; //const 在*右边,修饰的是指针不能被修改
我们知道权限可以缩小,但是不能放大。这里要实现的是迭代器指向的内容不能被修改。
具体实现:
方式一: 单独实现一个ListConstIterator去封装里面的迭代器指向的内容不能被修改。
template<class T>struct ListConstIterator{//自定义类型封装指针,去控制其行为typedef ListNode<T> Node; //模板类重命名为Nodetypedef ListConstIterator<T> Self; //模板类重名为SelfNode* _node; // 指向当前迭代器所代表的链表节点ListConstIterator(Node* node):_node(node){}//* const T& operator*(){return _node->_data;}//it->const T* operator->(){return &_node->_data;}//通过运算符重载控制其行为//重载前置++Self& operator++(){_node = _node->_next;//前置++,返回++以后的值return *this;}//后置++,使用(int)来区分前置和后置Self operator++(int){//后置++ 返回之前的值Self tmp(*this); _node = _node->_next;return tmp;}// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--(){_node = _node->_prev;return *this;}bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}};
方式二:
发现方式一的写法有一些代码冗余,因为里面具体只是一些函数的返回值类型与Iterator类不同,所以可以考虑使用模板参数来控制。
本质:写一个模板类,然后编译器实例化生成两个类。
template<class T,class Ref,class Ptr>struct ListIterator {//自定义类型封装指针,去控制其行为typedef ListNode<T> Node; //模板类重命名为Nodetypedef ListIterator<T,Ref,Ptr> Self; //模板类重名为SelfNode* _node; // 指向当前迭代器所代表的链表节点ListIterator(Node* node) :_node(node){}//* Ref operator*() {return _node->_data;}//it->Ptr operator->(){return &_node->_data;}//通过运算符重载控制其行为//重载前置++Self& operator++() {_node = _node->_next;//前置++,返回++以后的值return *this;}//后置++,使用(int)来区分前置和后置Self operator++(int) {//后置++ 返回之前的值Self tmp(*this);_node = _node->_next;return tmp;}// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--() {_node = _node->_prev;return *this;}bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}};
6. list析构函数 和 拷贝构造
6.1 析构函数
先实现一个clear,借助迭代器和erase删除所有数据(不含头节点)。
void clear() {//借助迭代器和eraseiterator it = begin();while (it != end()) {it = erase(it); //前提是erase处理了迭代器失效问题}}
再去调用clear实现析构。
~list() {clear();delete _head;_head = nullptr;}
6.2 拷贝构造
值得注意的是,list的拷贝构造需要手动去实现,因为:
当不写拷贝构造,list会使用默认的拷贝构造(浅拷贝--指向同一块空间),但是 对同一块空间进行析构两次是错误的,因为第一次析构后对象就已经不在了,第二次可能导致释放了不该释放的内存!
先初始化一个头节点,再复用push_back,把lt的节点拷贝进去。
void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}//list 构造函数list(){empty_init();}//lt1(lt2)list(const list<T>& lt){//先初始化一个头节点//再复用push_back,把lt的节点拷贝进去empty_init();for (auto& e : lt) {push_back(e);}}
需要析构,一般需要自己写深拷贝。
6.3 operator=
void swap(list<T>& lt) {//借助标准库函数中的swap来实现std::swap(_head,lt._head);std::swap(_size,lt._size);}//lt2 = lt1list<T>& operator=(list<T> lt) {swap(lt);return *this;}