🌏博客主页:PH_modest的博客主页
🚩当前专栏:C++跬步积累
💌其他专栏:
🔴 每日一题
🟡 Linux跬步积累
🟢 C语言跬步积累
🌈座右铭:广积粮,缓称王!
一、总揽(三个类和相关接口)
节点类(list_node)
template<calss T>
class list_node
{//成员对象list_node<T>* _pre;list_node<T>* _next;T _val;//成员函数//默认构造list_node(const T& val=T());
};
迭代器(list_iterator)
template<class T, class Ref, class Ptr>
class list_iterator
{
public://重命名typedef list_node<T> node;typedef list_iterator<T, Ref, Ptr> iterator;//成员变量node* _node;//构造函数list_iterator(node* x);//运算符重载iterator& operator++();iterator operator++(int);iterator& operator--();iterator operator--(int);bool operator==(const iterator& lt) const;bool operator!=(const iterator& lt) const;Ref operator*();Ptr operator->();
};
list类
//list类
template<class T>
class list
{typedef list_node<T> node;
public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//默认成员函数list();list(const list<T>& lt);~list();list<T>& operator=(const list<T>& lt);//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//访问容器相关函数T& front();T& back();const T& front() const ;const T& back() const ;//插入、删除函数void push_back(const T& x);void push_front(const T& x);void pop_back();void pop_front();iterator erase(iterator& it);iterator insert(iterator pos, const T& x);//其他函数void clear();void swap(list<T>& lt);private:node* _head;//指向链表头结点的指针
};
二、节点类的模拟实现
list底层的实现就是一个带头双向循环链表。
因此在实现list类之前需要先实现节点类。一个节点类需要存储三个信息:数据、前一个节点的地址、后一个节点的地址。所以成员函数就知道了:数据(_val)、前驱指针(_pre)、后继指针(_next)。
对于节点类的成员函数来说,我们只需要实现一个构造函数即可,因为这个节点类的作用就是根据数据来创建一个节点即可,而节点的释放则由list的析构函数来完成。
构造函数
list_node(const T& x = T()):_pre(nullptr), _next(nullptr), _val(x)
{}
构造函数主要是存储数据,当没有传递参数时,会调用所存储类型的默认构造所构造出来的值作为参数。
例如:数据类型为int时,会调用int类型的默认构造,会将_val初始化为0;
节点类总结
template<class T>
struct list_node
{//成员对象list_node<T>* _pre;list_node<T>* _next;T _val;//成员函数list_node(const T& x = T()):_pre(nullptr), _next(nullptr), _val(x){}
};
三、迭代器类的模拟实现
迭代器类的意义
与vector和string不同,他们可以直接使用原生指针,因为他们的数据是存储在一块连续的内存空间,我们可以直接通过指针进行自增、自减、解引用等操作来进行相关操作。
而对于list,各个节点在内存中的位置是随机的,不是连续的,所以我们不能直接通过节点指针的自增、自减。解引用等操作对相应节点的数据进行操作。
迭代器的意义: 让使用者可以不用关心容器的底层实现,可以用简单统一的方式对容器内部的数据进行访问,只需要将迭代器进行相应的封装即可。
迭代器的本质就是指针,既然这个原生指针不能满足我们的要求,那么我们就可以自己封装一个,对相关的操作符进行重载,让其满足相关的操作。(例如:当你使用list迭代器进行自增时,其实执行了ptr = ptr -> next;指向了下一个节点,也就是我们所希望的自增)
如果还觉得抽象,我再举个例子,原生指针就可以看成老虎,他们吃东西的时候是直接吃生肉;而迭代器封装就可以看成是人,我们吃东西的时候,需要将食物煮熟了再吃。老虎想要吃肉就可以直接吃生肉(原生指针进行相关操作时可以直接进行++、- -等操作),而我们人类吃东西时,需要将食物进行相关处理(list迭代器的相关操作符需要进行封装,以此来达到对应的操作)
总结: list迭代器类,实际上就是对节点指针进行了封装,对相关运算符进行了重载,使得节点指针的各种行为看起来和普通指针一样。
迭代器模板参数说明
我们这里使用了三个模版参数,这是为什么呢?
template<class T, class Ref, class Ptr>
在list的模拟实现中,需要有两种迭代器:普通迭代器和const迭代器。
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
不难看出,迭代器类的模版参数列表中的Ref和Ptr分别表示引用类型和指针类型。
通过下图对比一下不难看出,const和非const的类型只有三个不同,因此通过设置三个模版参数就可以将两种状态包含了。
构造函数
迭代器类就是对节点进行了封装,成员变量只有一个,就是节点指针,其构造函数直接根据所给的节点指针构造一个迭代器对象即可。
list_iterator(node* x):_node(x)
{}
++运算符重载
首先是前置++,前置++原本的作用是先将数据自增,然后返回自增之后的数据。那么对于节点指针的前置++,我们就应该让节点指针先指向后一个节点,然后返回“自增”后的节点指针。
iterator& operator++()//this指针的类型是iterator*
{_node = _node->_next;return *this;
}
对于后置++,我们应该先记录当前节点指针的指向,然后让节点指针指向后一个节点,最后返回“自增”前的节点指针即可。
iterator operator++(int)
{iterator tmp(*this);//这里会调用list_node的拷贝构造_node = _node->_next;return tmp;
}
- -运算符重载
思想和++类似,就不过多赘述。
前置- -:
iterator& operator--()
{_node = _node->pre;return *this;
}
后置- -:
iterator operator--(int)
{iterator tmp(*this);_node = _node->_pre;return tmp;
}
==运算符重载
当使用==运算符比较时,我们实际上是比较这两个迭代器是否是同一个位置的迭代器,也就是判断迭代器中的节点指针是否相同。
bool operator==(const iterator& lt) const
{return _node == lt._node;
}
!=运算符重载
!=运算符和==的作用相反,只需要判断迭代器中的节点指针的指向是否不同即可。
bool operator!=(const iterator& lt) const
{return _node != lt._node;
}
*运算符重载
当我们使用解引用操作符时,是想知道该位置的数据内容。因此,我们直接返回当前节点指针所指的数据即可,但这里需要使用引用返回,因为解引用后可能需要对数据进行修改。
Ref operator*()
{return _node->_val;
}
->运算符重载
某些情况下,我们使用迭代器时会用到->运算符。
例如下面的场景:
当list容器当中的每个节点存储的不是内置类型,而是自定义类型,例如日期类,当我们拿到一个位置的迭代器时,我们可能会使用->运算符访问Date的成员。
list<Date> lt;Date d1(2021, 8, 10);Date d2(1980, 4, 3);Date d3(1931, 6, 29);lt.push_back(d1);lt.push_back(d2);lt.push_back(d3);list<Date>::iterator pos = lt.begin();cout << pos->_year << endl;
所以对于->运算符的重载,我们直接返回节点当中所存储数据的地址即可。
Ptr operator->()
{return &_node->_val;//_val里面还有其他内容时:lt.operator->() -> _a;
}
注意点:
按道理来说这样的话我们使用->运算符的时候应该是这样的:
lt.operator->() -> _a;
应该有两个->,但是由于这样程序的可读性太差,所以编译器做了特殊处理,把这边优化了,省略了一个箭头。
四、list的模拟实现
默认成员函数
构造函数
list是一个带头双向循环链表,构造时,只需要申请一个头结点,然后让其前驱指针和后继指针都指向自己即可。
list()
{_head = new node;_head->_next = _head;_head->_pre = _head;
}
拷贝构造函数
拷贝构造函数就是根据所给list容器,拷贝构造出一个新的对象。先像默认构造那样创建一个指向自己的节点,然后使用迭代器遍历所给的容器,尾插到新构造的容器后就行。
list(const list<T>& lt)
{_head = new node;_head->_next = _head;_head->_pre = _head;for (const auto &it : lt){push_back(it);}
}
赋值运算符重载
方法一:传统写法
这是一种比较容易理解的写法,先调用clear函数清空,然后将容器lt中的数据通过迭代器的方式尾插到清空后的容器中。
list<T>& operator=(const list<T>& lt)
{if (this != <) //避免自己给自己赋值{clear(); //清空容器for (const auto& e : lt){push_back(e); }}return *this; //支持连续赋值
}
方法二:现代写法
首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造构造出一个list对象,然后调用swap函数将两个容器交换即可。
list<T>& operator=(list<T> lt) //编译器接收右值的时候自动调用其拷贝构造函数
{swap(lt); //交换这两个对象return *this; //支持连续赋值
}
这样做相当于将应该用clear清理的数据,通过交换函数交给了容器lt,而当该赋值运算符重载函数调用结束时,容器lt会自动销毁,并调用其析构函数进行清理。
析构函数
首先调用clear清空里面的数据,然后释放头结点,最后将头结点置空。
~list()
{clear();delete _head;_head = nullptr;
}
迭代器相关函数
begin和end
begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。
iterator begin()
{return iterator(_head->_next);
}
iterator end()
{return iterator(_head);
}
这里需要重载一下const对象的begin和end
const_iterator begin() const
{return const_iterator(_head->_next);
}
const_iterator end() const
{return const_iterator(_head);
}
访问容器相关函数
front和back
front和back分别是用于获取第一个有效数据和最后一个有效数据,所以只需要返回第一个有效数据的引用和最后一个有效数据的引用即可。
T& front()
{return *begin();
}
T& back()
{return *(--end());
}
当然也需要重载一下const对象的front和back。
const T& front() const
{return *begin(); //返回第一个有效数据的const引用
}
const T& back() const
{return *(--end()); //返回最后一个有效数据的const引用
}
插入和删除
insert
insert函数可以在所给迭代器之前插入一个新的节点。
iterator insert(iterator pos, const T& x)
{node* newnode = new node(x);node* cur = pos._node;node* pre = cur->_pre;pre->_next = newnode;newnode->_pre = pre;newnode->_next = cur;cur->_pre = newnode;return newnode;
}
erase
删除当前迭代器位置的节点。
iterator erase(iterator& it)
{assert(it != end());node* cur = it->_node;node* pre = cur->_pre;node* next = cur->_next;pre->_next = next;next->_pre = pre;delete cur;return next;
}
push_back和pop_back
方法一:原始版
//尾插
void push_back(const T& x)
{node* newnode = new node(x);node* tail = _head->_pre;tail->_next = newnode;newnode->_pre = tail;newnode->_next = _head;_head->_pre = newnode;
}
//尾删
void pop_back()
{node* tail = _head->_pre;_head->_pre = tail->_pre;tail->_pre->_next = _head;delete tail;tail = nullptr;
}
方法二:复用版
//尾插
void push_back(const T& x)
{insert(end(), x); //在头结点前插入结点
}
//尾删
void pop_back()
{erase(--end()); //删除头结点的前一个结点
}
push_front和pop_front
方法一:
//头插
void push_front(const T& x)
{node* newnode = new node(x);node* tmp = _head->_next;_head->_next = newnode;newnode->_pre = _head;newnode->_next = tmp;tmp->_pre = newnode;
}
//头删
void pop_front()
{node* front = _head->_next;_head->_next = front->_next;front->_next->_pre = _head;delete front;front = nullptr;
}
方法二:
//头插
void push_front(const T& x)
{insert(begin(), x); //在第一个有效结点前插入结点
}
//头删
void pop_front()
{erase(begin()); //删除第一个有效结点
}
其他函数
size
size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数。
size_t size() const
{size_t sz = 0; //统计有效数据个数const_iterator it = begin(); //获取第一个有效数据的迭代器while (it != end()) //通过遍历统计有效数据个数{sz++;it++;}return sz; //返回有效数据个数
}
resize
resize函数的规则:
- 若当前容器的size小于所给n,则尾插结点,直到size等于n为止。
- 若当前容器的size大于所给n,则只保留前n个有效数据。
实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结点。
这里实现resize的方法是,设置一个变量len,用于记录当前所遍历的数据个数,然后开始变量容器,在遍历过程中:
- 当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点释放即可。
- 当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可。
void resize(size_t n, const T& val = T())
{iterator it = begin(); //获取第一个有效数据的迭代器size_t len = 0; //记录当前所遍历的数据个数while (len < n&&it != end()){len++;it++;}if (len == n) //说明容器当中的有效数据个数大于或是等于n{while (it != end()) //只保留前n个有效数据{it = erase(it); //每次删除后接收下一个数据的迭代器}}else //说明容器当中的有效数据个数小于n{while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n{push_back(val);len++;}}
}
clear
clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。
void clear()
{iterator it = begin();while (it != end()){it=erase(it);}
}
swap
void swap(list<T>& lt)
{std::swap(_head, lt._head);
}
五、源代码
namespace my_list
{//首先创建节点类template<class T>struct list_node{//成员对象list_node<T>* _pre;list_node<T>* _next;T _val;//成员函数list_node(const T& x = T()):_pre(nullptr), _next(nullptr), _val(x){}};//迭代器实现template<class T, class Ref, class Ptr>class list_iterator{public:typedef list_node<T> node;typedef list_iterator<T, Ref, Ptr> iterator;node* _node;list_iterator(node* x):_node(x){}//++ititerator& operator++()//this指针的类型是iterator*{_node = _node->_next;return *this;}//it++iterator operator++(int){/*node* tmp = new node;tmp = _node;_node = _node->_next;return tmp;*/iterator tmp(*this);_node = _node->_next;return tmp;}//--ititerator& operator--(){_node = _node->pre;return *this;}//it--iterator operator--(int){iterator tmp(*this);_node = _node->_pre;return tmp;}//==bool operator==(const iterator& lt) const{return _node == lt._node;}//!=bool operator!=(const iterator& lt) const{return _node != lt._node;}//*Ref operator*(){return _node->_val;}//->Ptr operator->(){return &_node->_val;//_val里面还有其他内容时:lt.operator->() -> _a;}};//list类template<class T>class list{typedef list_node<T> node;public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//默认构造list(){_head = new node;_head->_next = _head;_head->_pre = _head;}//拷贝构造list(const list<T>& lt){_head = new node;_head->_next = _head;_head->_pre = _head;for (const auto& it : lt){push_back(it);}}//迭代器相关函数iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}//访问容器相关函数T& front(){//return _head->_next->_val;return *begin();}T& back(){//return _head->_pre->_val;return *(--end());}const T& front() const {return _head->_next->_val;}const T& back() const {return _head->_pre->_val;}//尾插void push_back(const T& x){/*node* newnode = new node(x);node* tail = _head->_pre;tail->_next = newnode;newnode->_pre = tail;newnode->_next = _head;_head->_pre = newnode;*/insert(end(), x);}//尾删void pop_back(){/*node* tail = _head->_pre;_head->_pre = tail->_pre;tail->_pre->_next = _head;delete tail;tail = nullptr;*/erase(--end());}//头插void push_front(const T& x){/*node* newnode = new node(x);node* tmp = _head->_next;_head->_next = newnode;newnode->_pre = _head;newnode->_next = tmp;tmp->_pre = newnode;*/insert(begin(), x);}//头删void pop_front(){/*node* front = _head->_next;_head->_next = front->_next;front->_next->_pre = _head;delete front;front = nullptr;*/erase(begin());}//清除void clear(){iterator it = begin();while (it != end()){it=erase(it);}}iterator erase(iterator& it){assert(it != end());node* cur = it->_node;node* pre = cur->_pre;node* next = cur->_next;pre->_next = next;next->_pre = pre;delete cur;//return this;return next;}iterator insert(iterator pos, const T& x){node* newnode = new node(x);node* cur = pos._node;node* pre = cur->_pre;pre->_next = newnode;newnode->_pre = pre;newnode->_next = cur;cur->_pre = newnode;return newnode;}size_t size() const{size_t sz = 0; //统计有效数据个数const_iterator it = begin(); //获取第一个有效数据的迭代器while (it != end()) //通过遍历统计有效数据个数{sz++;it++;}return sz; //返回有效数据个数}void resize(size_t n, const T& val = T()){iterator it = begin(); //获取第一个有效数据的迭代器size_t len = 0; //记录当前所遍历的数据个数while (len < n && it != end()){len++;it++;}if (len == n) //说明容器当中的有效数据个数大于或是等于n{while (it != end()) //只保留前n个有效数据{it = erase(it); //每次删除后接收下一个数据的迭代器}}else //说明容器当中的有效数据个数小于n{while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n{push_back(val);len++;}}}void swap(list<T>& lt){std::swap(_head, lt._head);}~list(){clear();delete _head;_head = nullptr;}private:node* _head;};void test1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto it : lt){std::cout << it << " ";}std::cout << std::endl;lt.pop_back();for (auto it : lt){std::cout << it << " ";}std::cout << std::endl;for (auto it : lt){std::cout << it << " ";}std::cout << std::endl;}
};