【C++详解】STL-list模拟实现(深度剖析list迭代器,类模板未实例化取嵌套类型问题)
文章目录
- 一、定义list结点
- 二、模拟实现list
- 默认构造
- 析构
- push_back
- list迭代器
- 初步框架
- 析构/ 拷贝构造/赋值运算符重载
- 完善初步框架
- const迭代器
- 迭代器第二阶段(Ref)
- 重载operator->
- insert
- erase
- 复用insert/erase实现头/尾插、头/尾删
- clear(析构复用clear)
- 拷贝构造
- 多参数构造函数
- 赋值运算符重载
- size
- 三、类模板未实例化取嵌套类型
- 四、梳理list结构
- 五、源码
一、定义list结点
我们定义类默认用class,但是当我们定义一个类不想让它的成员被访问限定符限制为私有时,可以用struct来定义。
还需要写一个结点的构造函数,因为后面我们创建结点时一般都要传参,显示写了构造函数才好接受参数。
template<class T>
struct list_node
{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x),_next(nullptr),_prev(nullptr){}
};
这里为什么要把结点定义成共有呢?因为list的各种操作要频繁访问结点的成员变量,如果我们不把结点定义成共有,那访问结点的成员函数就要用友元,既然这样还不如直接把结点定义成共有。list对外访问时不会展示结点的结构,外部访问list都是操作的迭代器,所以也不用担心结构暴露的问题。
后面我们在list里最好把list结点重定义一下,因为结点类型有模板,很容易把模板写漏。
二、模拟实现list
定义完list结点后,就要开始模拟实现list了,首先应该关注它的成员变量,标准库里的成员变量是结点指针,这里也模仿标准库定义成员变量。
private:Node* _head;
默认构造
list(){_head->data = new Node;_head->next = _head;_head->prev = _head;}
析构
~list(){iterator it = begin();while (it != end()){//更新迭代器,解决迭代器失效it = erase(it);}//最后要手动删掉_headdelete _head;_head = nullptr;}
push_back
void push_back(const T& x)
{//创建新结点Node* newnode = new Node(x);//找尾Node* tail = _head->_prev;//插入newnodenewnode->_next = _head;newnode->_prev = tail;tail->_next = newnode;//这里要改变_head->_prev不能用tail,因为tail变了不影响_head->_prev_head->_prev = newnode;
}
list迭代器
初步框架
我们前面介绍string和vector,它们的迭代器都是用的原生指针,因为它们的物理结构是连续的,所以迭代器++,–,直接用原生指针就可以实现想要的效果(迭代器的行为本质就是模拟原生指针),但是list是链式结构,底层物理结构是不连续的,这时候原生指针Node*就无法达到我们想要的效果,这时候就需要自定义一个类型去封装Node*,让Node*为这个自定义类型的成员变量,然后重载运算符++,–,,就可以控制迭代器的行为达到和原生指针一样的效果。
所以这里我们要自定义一个类型:_iterator_list,这里有个惯例,当一个类型我们不想对外暴露时,可以在类型名前面加_,和成员变量类似。之所以不对外暴露是因为容器迭代器都会统一typedef成iterator,外部不会直接调用_iterator_list。
那我们先把Node*封装为_iterator_list:
template<class T>struct __list_iterator{typedef list_node<T> Node;Node* _node;__list_iterator(Node* node) :_node(node){}//引用返回,减少拷贝,并且可以修改返回值T& operator*(){return _node->_data;}__list_iterator<T>& operator++() {_node = _node->_next;return *this;}bool operator!=(const __list_iterator<T>& it) const{return _node != it._node;}};
首先它的成员变量是_Node,第一个接口是带参的构造函数,后面在list里实现begin()和end()返回迭代器是会用到。
然后就是重载*,不重载的话返回的就是结点本身,这不符合预期,所以要重载成返回结点的数据,最后要引用返回,减少拷贝也可以修改返回值。
operator++,把_node = _node->_next,然后返回迭代器本身,要引用返回,减少拷贝。
最后是operator!=,有两个细节:
第一是参数要加const,因为参数一般是end()的返回值,而begin()end()都是传值返回(返回对象是在begin()end()的函数栈帧里创建的,一但退出栈帧对象就会销毁,所以不能传引用返回),传值返回的临时对象具有常性,所以operator!=的参数是用引用接受就要加const。
第二是比较的是结点本身,而不是结点里的数据。
然后在list里的public域里typedef__list_iterator,因为iterator是要暴露给外部的。
typedef __list_iterator<T> iterator;
还要在list里要加上begin()end(),因为只有在容器里才能找到头结点和尾结点。
iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}
有了上面的基础,我们就可以实现一些迭代器的遍历功能了:
析构/ 拷贝构造/赋值运算符重载
迭代器里有结点的指针,那么迭代器要写析构释放这个结点吗?
我们首先要明白,结点是属于list的,不是属于迭代器的,也就是说当迭代器出了它的作用销毁后,不会一并把结点也销毁,就相当于迭代器有结点的访问使用权,但是它的生死是归list管,所以不用在迭代器里写结点的析构。
另外拷贝构造和赋值运算符重载也不用写,因为这里我们要的就是浅拷贝,而且出了函数栈帧不会析构,所以也不会发生同一块空间析构两次的问题,比如下面:
wusaqi::list<int>::iterator it1 = l1.begin();
while (it1 != l1.end())
{cout << *it1 << " ";++it1;
}
迭代器it1指向的结点就应该是l1.begin()返回的迭代器指向的结点。 这也变相说明了两点;
1、不是类型里有指针指向资源就一定要写析构释放这个资源,写拷贝构造和赋值重载拷贝资源。
2、析构和拷贝构造、赋值运算符重载是在一起的,要么都需要写,要么都不需要写。
完善初步框架
等于:(不修改成员变量(this指向的内容)的接口尽量后面加const)
bool operator==(const __list_iterator<T>& it) const
{return _node == it._node;
}
后置++:(不能引用返回,因为tmp在该函数栈帧内创建,退出栈帧tmp会释放)
__list_iterator<T> operator++(int)
{__list_iterator tmp = __list_iterator(*this);_node = _node->_next;return tmp;
}
前置–/后置–:
__list_iterator<T>& operator--()
{_node = _node->_prev;return *this;
}__list_iterator<T> operator--(int)
{__list_iterator tmp = __list_iterator(*this);_node = _node->_prev;return tmp;
}
const迭代器
我们调用print函数需要const迭代器:
void print(const list<int>& lt)
{wusaqi::list<int>::const_iterator it1 = lt.begin();while (it1 != lt.end()){cout << *it1 << " ";++it1;}cout << endl;
}
我们现在想实现一个const迭代器,首先不能直接在迭代器前面加const,这样是修饰迭代器本身不能被修改,我们想要的是迭代器指向的内容不能被修改。
(vector 和 string直接typedef const T* const_iterator就行了,因为这样const是在*左边,修饰的就是指向的内容不能修改,但是list就无法实现,它typedef const iterator const_iterator 就是修饰的迭代器本身无法修改)
(在普通迭代器里重载一个const T& operator*() const 也不行,因为只有被const修饰过的迭代器才能调用这个函数,但是迭代器被const修饰后就不能迭代调用++了,所以只能用下面的方法,定义两个类,在operator*那里动手脚)
那要想一想如何能操作迭代器指向的内容呢?当然是operator*,所以就需要在operator*这里做修改,只要把operator*的返回值从T&改为 const T&就行了,这样就是修饰返回值本身无法被修改。
但是不能直接在原位置加const,这样所有返回值都不能被修改了,我们想要的是普通迭代器调用返回普通迭代器,const迭代器调用返回const迭代器,所以我们还要写一个__list_const_iterator的类,改变这个类的operator*的返回值,其他不用改。
//const修饰this指针可加可不加,const迭代器不是const对象,它能被修改,//加了权限缩小而已const T& operator*(){return _node->_data;}
在list里需要额外写两个const的begin()和end(),普通迭代器调用普通begin()和end()返回普通迭代器,const迭代器调用const begin()和end() 返回const迭代器:
const_iterator begin() const
{return const_iterator(_head->_next);
}const_iterator end() const{return const_iterator(_head);
}
迭代器第二阶段(Ref)
我们现在实现了两个类来解决const迭代器的问题,但是如果不想定义两个类可不可以呢?
当然可以,因为两个类之间就只有operator*的返回值有区别,所以我们可以在迭代器里再定义一个模板参数,用来实例化不同的operator*的返回值,也就是用的同一个类模板实例化两个类型。
template<class T, class Ref>Ref operator*()
{return _node->_data;
}
在list里typedef的两个类其实就是用的同一个类模板实例化的两个类型:
typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;
但是加了一个模板参数后迭代器里的参数:__list_iterator< T > 都需要被改成__list_iterator<T,
T&>,这里也有一个写代码的习惯,在类里对于自己类类型比如这里的__list_iterator< T >
,我们习惯把它typedef成Self,这样如果后续还需要加模板参数直接在typedef的地方修改就行了。
typedef __list_iterator<T, Ref> Self;
重载operator->
前面ist的data类型用的是内置类型,接下来介绍一下当data为自定义类型时需要扩展那些接口。
struct Pos
{int _row;int _col;Pos(int row = 0, int col = 0):_row(row),_col(col){}
};wusaqi::list<Pos> lt;
lt.push_back({ 1,1 });
lt.push_back({ 2,2 });
lt.push_back({ 3,3 });
lt.push_back({ 4,4 });
list<Pos>::iterator it = lt.begin();
while (it != lt.end())
{cout << (*it)._row << ":" << (*it)._col << endl;cout << it->_row << ":" << it->_col << endl;it++;
}
我们上面定义了一个自定义类型Pos,我们用迭代器遍历list时用 *it 可以实现,因为我们重载了operator*,但是用it-> 就会报错,我们没有重载对应的函数,下面我们就来在迭代器里重载这个operator-> 。
T* operator->()
{return &_node->_data;
}
这里的重载返回的是_data的指针,感觉还缺点东西,我们要访问的是_data里的数据,应该还要用operator-> 的返回值再 ->访问数据,所以实际上应该是下面的第一排代码的思路,只不过编译器为了可读性,第二排也可以实现一样的效果,而且不能手动写两个箭头。
cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;
//为了可读性,省略了一个->
cout << it->_row << ":" << it->_col << endl;
cout << it->->_row << ":" << it->->_col << endl;//错误写法,会报错
operator->也有可能被const对象调用,那么就应该返回const T*(修饰指向的内容),所以这里我们又要多加一个模板参数:
template<class T, class Ref, class Ptr>Ptr operator->()
{return &_node->_data;
}
在list里:
typedef __list_iterator<T, const T&, const T*> const_iterator;
insert
iterator isert(iterator pos, const T& val)
{Node* cur = pos._node;Node* newnode = new Node(val);Node* prev = cur->_prev; //用临时变量避免后面用两个箭头prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;//单参数构造的隐式类型转换return newnode;
}
插入需要操作结点指针,所以先拿到当前迭代器的结点指针,虽然插入不会扩容出现迭代器失效,但是标准规定插入后要返回插入结点指针的迭代器。
这里补充一点,因为这里是list内部,所以可以拿到迭代器的成员函数_node,虽然外部也可以拿到list的迭代器对象,然后通过迭代器对象访问迭代器的成员变量,但是每个编译器的迭代器的成员变量名称都不同,所以在工程中我们不会通过迭代器访问迭代器的成员变量,如果访问了代码就没有通用性了。
erase
iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;cur = nullptr;//单参数构造的隐式类型转换return next;
}
删除过后要释放掉删除的结点,然后要返回删除结点的下一个结点位置的迭代器,用来更新迭代器解决迭代器失效。
复用insert/erase实现头/尾插、头/尾删
void push_back(const T& x)
{insert(end(), x);
}void push_front(const T& x)
{insert(begin(), x);
}void pop_back()
{erase(--end());
}void pop_front()
{erase(begin());
}
尾插在end()之前插入,头插在begin()之前插入,尾删删除end()的前一个结点,所以要end()–,头删删除begin()位置结点。
clear(析构复用clear)
clear相比析构它不用释放哨兵位,所以析构=clear+释放哨兵位。
clear()
{iterator it = begin();while (it != end()){//更新迭代器,解决迭代器失效it = erase(it);}
}~list()
{clear();delete _head;_head = nullptr;
}
拷贝构造
在list内部需要显示写析构、拷贝构造,首先创建一个哨兵位结点,然后把另一个list用范围for挨个遍历结点,然后尾插到当前list。
这里构造函数需要创建一个空的哨兵位,拷贝构造也需要,所以这里将空初始化直接封装成函数。
void empty_Init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;
}
//构造
list()
{empty_Init();
}
//拷贝构造
list(const list<T>& lt)
{empty_Init();//范围for加const底层会替换成const迭代器for (const auto& ch : lt){push_back(ch);}
}
多参数构造函数
list(const initializer_list<T>& i1)
{empty_Init();//这里遍历initializer_list的值插入listfor (const auto& ch : i1){push_back(ch);}
}
前面拷贝构造是遍历list的值尾插,这里是遍历initializer_list的值尾插,注意如果参数是引用的话要用const修饰,因为i1有可能是隐式类型转换过来的,临时变量具有常性。
赋值运算符重载
为了支持连续赋值,比如:(l1=l2)=l3,所以赋值运算符重载不会传引用返回。
传统写法:
首先要判断是否是自己给自己赋值,不是的话就先清除除了哨兵位的其他结点,然后遍历lt尾插到当前链表。
list<T>& operator=(const list<T>& lt)
{//判断是否是自己给自己赋值if (this != <) //两个指针变量之间的比较{//清掉剩余结点,留下哨兵位clear();for (auto ch : lt){push_back(ch);}}return *this;
}
现代写法:
当lt3想赋值给lt1时,首先lt3传值调用operator=,所以先拷贝构造一个临时对象lt,lt1想要这个lt,所以把lt1的头结点指针和临时变量it的头结点指针交换一下就行了,lt出了作用域还会调用析构函数带走以前li1的资源。
//现代写法 参数要改变,所以参数不加const
void swap(list<T>& lt)
{std::swap(_head, lt._head);
}
// lt1 = lt3
list<T>& operator=(list<T>& lt)
{swap(lt);return *this;
}
size
我们想知道list的结点个数首先可以写一个接口,遍历整个list,但是这样时间复杂度就是O(n)了,这里还有一个巧妙的办法,在list里定义一个成员变量_size,给一个缺省值0,因为我们所有的插入删除操作都是复用的insert和erase,所以在这两个接口分别加上size++
和 size–就行了,还要在swap位置加上交换两个对象的_size。
size_t size()
{return _size;
}
三、类模板未实例化取嵌套类型
template<class T>
void print(const list<T>& lt)
{list<T>::const_iterator it1 = lt.begin();while (it1 != lt.end()){cout << *it1 << " ";++it1;}cout << endl;
}wusaqi::list<int> l1 = { 10,20,30,40 };
print(l1);wusaqi::list<double> l2 = { 10.1,20.2,30.3,40.4 };
print(l2);
这里我们写了一个针对list的泛型化print, 但是这样写编译不会通过,问题出在:
list<T>::const_iterator it1 = lt.begin();
因为这里list没有实例化,还是类模板,那么规定编译器不能到类模板中找后面的const_iterator,不能去找编译器就分不清const_iterator是嵌套类型还是静态成员变量,因为嵌套类型还是静态成员变量都可以用上面那样的域作用限定符的方式取到,如果是类型的话上面的语法就没问题,但是是静态成员变量的话就不符合语法规定,所以需要在前面加typename来告诉编译器这里的const_iterator我确认过了是嵌套类型,先让它编译通过,具体是什么类型等类模板实例化后再去取。
typename list<T>::const_iterator it1 = lt.begin();
还有一个简洁办法直接用auto就可以避开这个问题:
auto it1 = lt.begin();
四、梳理list结构
首先list是链式结构,所以需要定义每个结点的结构,结点里有三个成员变量:存的值和指向前一个和后一个结点的指针,用 sruct list_node来定义。
接着是list本身,它里面的接口能控制链表各种各样的行为,成员变量是指向哨兵位的结点的指针_head,用class list 来定义。
除此之外,还需要提供一个方便外部访问、遍历、修改容器的结构——迭代器,迭代器模拟的是普通指针访问数组的行为(解引用是当前数据,++指向下一个数据),所以我们前面介绍的string和vector,因为底层本身就是数组结构,天生就可以用原生指针模拟迭代器的行为。但是这里的ist就无法达到想要的效果,Node*解引用不是我们想要的数据,而是一整个结点,++也无法到链表的下一个结点,因为地址之间不连续。所以因为直接用原生结构结点的指针达不到我们想要的行为,所以可以再定义一个类来封装结点的指针,在类里通过运算符重载来控制* ++ 等等的行为。
五、源码
list.h:
#pragma once
using namespace std;
#include <iostream>namespace wusaqi
{template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x), _next(nullptr), _prev(nullptr){}};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){}//引用返回,减少拷贝,并且可以修改返回值Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++() //返回值要加&吗?{_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& it) const{return _node != it._node;}bool operator==(const Self& it) const{return _node == it._node;}};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;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}void empty_Init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}list(){empty_Init();}~list(){clear();delete _head;_head = nullptr;}list(const list<T>& lt){empty_Init();//范围for加const底层会替换成const迭代器for (const auto& ch : lt){push_back(ch);}}list(const initializer_list<T>& i1){empty_Init();//这里遍历initializer_list的值插入listfor (const auto& ch : i1){push_back(ch);}}//传统写法//list<T>& operator=(const list<T>& lt)//{// //判断是否是自己给自己赋值// if (this != <) //两个指针变量之间的比较// {// //清掉剩余结点,留下哨兵位// clear();// for (auto ch : lt)// {// push_back(ch);// }// }// return *this;//}//现代写法 参数要改变,所以参数不加constvoid swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}// lt1 = lt3list<T>& operator=(list<T>& lt){swap(lt);return *this;}size_t size(){return _size;}void clear(){iterator it = begin();while (it != end()){//更新迭代器,解决迭代器失效it = erase(it);}}iterator insert(iterator pos, const T& val){Node* cur = pos._node;Node* newnode = new Node(val);Node* prev = cur->_prev; //用临时变量避免后面用两个箭头prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;_size++;//单参数构造的隐式类型转换return newnode;}iterator erase(iterator pos){Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;cur = nullptr;_size--;//单参数构造的隐式类型转换return next;}void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}private:Node* _head;size_t _size = 0;};
}
test.cpp:
#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
namespace wusaqi
{template<class T>void print(const list<T>& lt){typename list<T>::const_iterator it1 = lt.begin();//auto it1 = lt.begin(); //简洁办法while (it1 != lt.end()){cout << *it1 << " ";++it1;}cout << endl;}void test01(){wusaqi::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);wusaqi::list<int>::iterator it1 = l1.begin();while (it1 != l1.end()){cout << *it1 << " ";++it1;}cout << endl;print(l1);}typedef struct Pos{int _row;int _col;Pos(int row = 0, int col = 0):_row(row), _col(col){}}Pos;void test02(){wusaqi::list<Pos> lt;lt.push_back({ 1,1 });lt.push_back({ 2,2 });lt.push_back({ 3,3 });lt.push_back({ 4,4 });list<Pos>::iterator it = lt.begin();while (it != lt.end()){//cout << (*it)._row << ":" << (*it)._col << endl;//为了可读性,省略了一个->cout << it->_row << ":" << it->_col << endl;cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;it++;}}void test03(){wusaqi::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);auto it = l1.begin();l1.push_back(100);l1.push_back(100);print(l1);l1.push_front(200);l1.push_front(200);print(l1);l1.pop_back();l1.pop_back();l1.pop_back();print(l1);l1.pop_front();l1.pop_front();l1.pop_front();print(l1);/*l1.isert(it, 100);print(l1);l1.erase(it);print(l1);*/}void test04(){wusaqi::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);/*print(l1);auto it = l1.begin();list<int> l2 = l1;print(l2);*/wusaqi::list<int> l3 = { 10,20,30,40 };print(l3);l1 = l3;print(l1);cout << l1.size() << endl;cout << l3.size() << endl;}void test05(){wusaqi::list<int> l1 = { 10,20,30,40 };print(l1);wusaqi::list<double> l2 = { 10.1,20.2,30.3,40.4 };print(l2);}
}int main()
{//wusaqi::test01();//wusaqi::test02();//wusaqi::test03();//wusaqi::test04();wusaqi::test05();return 0;
}
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~