本次实现的list结构是带头双向循环链表,节点结构就是链表结构,所以本次的类主要有三个,一个节点类,一个链表类,还有剩下的迭代器类,为什么这次要将迭代器单独封装成一个类,是因为之前文章的容器的迭代器实现底层都是原生指针,对于像string,vector这种的容器底层是数组,遍历容器直接就可以通过指针来实现,所以就不用封装成一个类,但是对于list这一容器,底层不是连续的一段内存,都是一小块一小块的“节点”,照之前的实现思想是行不通的,所以要重新封装成一个类,来实现其功能。
对于链表这一结构,底层是由一个一个节点“连接”而成,所以节点的结构就是链表的结构。
// 节点结构
template<class T>
struct __list_node
{
// 属性
T _data;
__list_node<T>* _next;
__list_node<T>* _prve;
// 构造函数
// 泛型缺省值使用匿名对象
__list_node(const T& x = T())
:_data(x)
,_next(nullptr)
,_prve(nullptr)
{}
};
该类将它定义成模板类,以满足不同类型的数据使用此容器进行存储,并且此类使用struct定义,因为此类中的属性在后续需要经常访问,若使用访问限定符限制,使用起来较于麻烦;节点的属性有三:数据域,指向下一个节点的指针域和指向前一个节点的指针域;对于节点的构造,若创建有值对象,则使用此值给_data属性初始化,若创建的是空对象,则使用匿名对象的默认值给_data属性初始化,剩下两个指针域初始化为空即可。
本此实现的容器支持的迭代器行为并不是指向数组的指针,是指向一块物理地址非连续的内存空间,所以需要进行类的封装,从中实现迭代器的功能例如前置/后置++,解引用*等等。
// 迭代器
// Ref --- reference,引用,参考的意思
// self --- 自己的意思
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 temp(*this);
_node = _node->_next;
return temp;
}
// 前置--
Self& operator--()
{
_node = _node->_prve;
return *this;
}
// 后置--
Self operator--(int)
{
Self temp(*this);
_node = _node->_prve;
return *this;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
bool operator==(const Self& it) const
{
return _node == it._node;
}
};
上述iterator类,唯一的属性是节点类型的_node,以及一个构造函数,剩下的全部都是运算符重载,也就是对于list迭代器功能的重载。最重要的是此类是一个多参数模板类,因为迭代器不止正向迭代器,也有const修饰的正向迭代器,更有反向迭代器,例如解引用操作,没有const修饰的解引用运算符重载返回值类型,正常对象调用没有问题,但是const对象就调用不了,会显示没有相应的解引用运算重载,所以只是用一个模板参数是不够的,当然也可以重新写一个针对const对象的迭代器类,但是你写完后会发现也就只有解引用运算符重载的返回值类型有所不同,其他的代码都是一样的,就会有代码冗余,所以就再启用一个模板参数,用来针对const对象。
此类是链表的核心,主要是实现链表的各种方法。
// 链表结构
template<class T>
class list
{
public:
typedef __list_node<T> node;
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//typedef __list_const_iterator 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);
}
//…………
private:
node* _head; // 链表属性:头节点
size_t _size = 0; // 链表大小
};
此类也是定义成模板类,在第二三句typedef代码就是用来定义const迭代器和非const迭代器。
迭代器模仿的是一个指针的行为,所以解引用就是指针指向的数据/元素,而返回值类型Ref是一个模板参数,实际类型是T&或者const T&,使用模板参数是为了使const对象和非const对象都能使用此重载。
Ref operator*() // Ref - T& / const T&
{
return _node->_data;
}
箭头访问是对象为指针形式时使用的成员访问操作符,Ptr也是模板参数,其实际类型是T或者const T,使用模板参数是为了使const对象和非const对象都能使用此重载,并且在 C++ 中,箭头运算符(->)访问的是数据指针,但编译器会自动解引用它,最终效果是直接访问到数据,所以这里返回的是其数据指针。
Ptr operator->() // Ptr - T* / const T*
{
return &(_node->_data);
}
此重载的使用场景是在复杂类型下的输出场景,例如:
// 坐标
// 复杂的类类型
struct Pos
{
int _row; // 行
int _col; // 列
Pos(int row = 0, int col = 0)
:_row(row)
, _col(col)
{}
};
// main函数内:
dyj::list<Pos> lt1;
// 构造途径是(多参数的)隐式类型转换
// 拥有构造参数的相同构造函数即可实现
lt1.push_back({1,1});
lt1.push_back({2,2});
lt1.push_back({3,3});
lt1.push_back({4,4});
lt1.push_back({5,5});
// 迭代器遍历
dyj::list<Pos>::iterator it = lt1.begin();
//auto it = lt1.begin();
while (it != lt1.end())
{
//cout << (*it)._col << ":" << (*it)._row << " ";
cout << it->_col << ":" << it->_row << " ";
++it;
cout << endl;
}
上述是一个Pos类型的对象输出场景,其中Pos类中有行和列两个属性,以及包含两个属性的构造函数,面对这种复杂类型,只有一个解引用操作是不能直接读取对象的数据的,又由于Pos类定义的是一个公有类,当中成员可以使用成员访问操作符进行访问,点访问(本身运算符是无法重载的)可以直接使用,但是箭头访问没有重载,所以才需要重载此运算符。
下述四种重载都是迭代器必不可少的的操作,在访问一个迭代器位置的数据后,需要将其++或者- -,用来访问后一个或者前一个迭代器位置的数据,在此四种重载中,更加推荐使用前置++ / - -,因为对比一下,返回值类型上有&的拷贝次数少,函数体内没有对this的拷贝,所以更加推荐使用前置。
// 前置++
Self& operator++()
{
_node = _node->_next;
return *this;
}
// 后置++
Self operator++(int)
{
Self temp(*this);
_node = _node->_next;
return temp;
}
// 前置--
Self& operator--()
{
_node = _node->_prve;
return *this;
}
// 后置--
Self operator--(int)
{
Self temp(*this);
_node = _node->_prve;
return *this;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
bool operator==(const Self& it) const
{
return _node == it._node;
}
begin返回的是头节点的下一个节点位置,也就是第一个有效节点,end返回的是最后一个有效节点的下一个位置,也就是头节点。
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->_prve = _head;
}
下面四组代码依次是默认构造,初始化列表构造,拷贝构造还有析构函数。默认构造也就是上述空初始化方法;初始化列表构造是先创建一个initializer_list对象,再生成一个空初始化对象,将initializer_list对象的值依次push进空对象即可;拷贝构造和初始化列表构造高度相似,只是中间增加了一步清空操作;析构函数是将头节点和所有有效节点均释放的操作,在此中有效节点释放的操作封装成了清空方法。
// 构造函数
list()
{
empty_init();
}
// 初始化列表构造
list(initializer_list<T> it)
{
empty_init();
for(const auto& e : it)
{
push_back(e);
}
}
// 拷贝构造
list(const list<T>& lt)
{
empty_init();
clear();
for (const auto& e : lt)
{
push_back(e);
}
}
// 析构函数
// 所有的节点无一例外
~list()
{
clear();
delete _head;
_head = nullptr;
}
赋值运算符重载有两种写法,一种是普通写法,即先判断是否是自己给自己赋值,剩下的是拷贝构造的操作;另一种是新方法,直接使用swap交换即可。
void swap(const list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// 赋值运算符重载
list<T>& operator=(const list<T>& lt)
{
//// (1)老方法
//if (this != *it)
//{
// // 先清空
// clear();
// iterator it = lt.begin();
// // 下面遍历使用范围for也是可以的
// while (it != lt.end())
// {
// push_back(*it);
// }
//}
// (2)现代写法
swap(lt);
return *this;
}
尾插,头插可以使用老方法改变指针指向,但是这里只需要实现insert(任意位置插入方法)即可,直接复用此方法。重点实现insert方法即可,注意改变指针指向的时候不要影响整体链表的结构,以防在改变指针指向时访问不到节点,最后插入完成需要++size大小,更新一下。为了防止产生迭代器失效,需要返回插入位置的迭代器位置。
// 尾插
void push_back(const T& x)
{
// (1)老方法
/*node* newnode = new node(x);
// 三个指针:newnode(新节点),_head(头节点),_head->prve(尾节点)
newnode->_prve = _head->_prve;
_head->_prve->_next = newnode;
newnode->_next = _head;
_head->_prve = newnode;*/
// (2)复用insert方法
// end()指向的是头节点
insert(end(), x);
}
// 头插
void push_front(const T& x)
{
// 复用insert方法
// begin()指向的是第一个节点
insert(begin(), x);
}
// 任意位置插入
iterator insert(iterator pos,const T& x)
{
node* newnode = new node(x);
node* pcur = pos._node;
// pcur->prve,pcur,newnode
newnode->_next = pcur;
newnode->_prve = pcur->_prve;
pcur->_prve->_next = newnode;
pcur->_prve = newnode;
++_size;
return iterator(newnode);
}
实现思想上整体和插入的没有区别,只是操作从插入变成删除,删除任意位置的数据后释放此数据,并且- -size,更新大小,也是防止产生迭代器失效,需要返回删除位置的下一个迭代器位置。
// 尾删
void pop_back()
{
// 复用erase方法
// end()指向的是头节点,--end()就是尾节点
erase(--end());
}
// 头删
void pop_front()
{
// 复用erase方法
erase(begin());
}
// 任意位置删除
iterator erase(iterator pos)
{
node* pcur = pos._node;
node* pprve = pcur->_prve;
node* pnext = pcur->_next;
// pprve,pnext
pnext->_prve = pprve;
pprve->_next = pnext;
delete pcur;
--_size;
return iterator(pnext);
// 下面代码走的是单参数的隐式类型转换
//return pnext;
}
清空方法只清空有效节点,头节点不清空,只有在析构时头节点才清空。清空方法复用erase(任意位置删除方法),找到首尾有效节点迭代器位置,循环删除即可。
// 清空
// 不会清理头节点
void clear()
{
iterator it = begin();
while (it != end())
{
// 防止迭代器失效,更新it
it = erase(it);
}
}
返回链表的大小。
size_t size()
{
return _size;
}
本次对于容器list的实现,最重要的就是对于迭代器类的实现,封装我们需要的方法,重载等,模拟出迭代器行为,其次是对多参数模板的使用,针对const对象和非const对象,使用多参数模板进行区分。