目录
前言
前边我们了解了list的一些使用及其注意事项,今天我们进一步深入学习一下list容器;本文的主要内容是list底层数据结构剖析以及list的模拟实现与封装;
list的结构与框架
list是一个带头双向循环链表,对list的模拟实现的重难点在于迭代器的实现,以及const迭代器的实现;
list实现整体需要封装三个部分:list_node(节点)、list_iterator(迭代器)、list(链表)
首先我们需要先定义一个节点类:
cpp
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)
{}
};
定义时的基本结构和C语言的很像,节点中存储两个指针,一个指向下一个节点,一个指向前一个节点,以及T类型的值(便于存储各种类型的数据);
其次就是 list_node 的构造函数,我们在list中new节点时需要调用 list_node 的构造函数(new只要对于内置类型:开空间+调用构造函数)
list的基本结构:
cpp
class list
{
typedef list_node<T> Node;
public:
void empty_init() // 初始化开一个头节点,同时也为了方便拷贝构造与构造函数复用
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
private:
Node* _head;
size_t _size;//记录链表长度
};
list结构初始化需要开一个头节点,并且指针都指向自己;
接下来我们实现一下push_back(尾插),这样我们就可以先上手测试list基本结构是否完善;
具体操作如下,可以根据图先动手尝试写一下:
具体代码如下:
cpp
void push_back(const T& x)
{
Node* newnode = new Node(x);
Node* tail = head->next;
// 插入节点
tail->next = newnode;
newnode->prev = tail;
newnode->_next = _head;
_head->prev = newnode;
_size++;
}
能够插入数据后,接下来就是遍历list,最基本的也就是迭代器遍历,前边我们使用迭代器的方法:
cpp
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << ' ';
it++;
}
vector是顺序结构,它有天然的迭代器,那list是链表怎么++进行遍历?list的 " ++ " 本质其实就是封装+运算符重载,我们需要重载" ++ ",每次调用" ++ "时迭代器都移动到下一个节点;
从上述迭代器的使用方法中可以看出,要想实现最基本的遍历迭代器起码要重载三部分:
!=、*(解引用)、++;
list迭代器
要想实现list的遍历,我们首先需要实现list的迭代器,为什么要实现迭代器?
list迭代器的实现最能体现的就是封装思想,封装屏蔽底层的差异和实现细节;其目的是为了和其他容器的遍历修改的方式保持一致;
cpp
template <class T>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T,Ref,Ptr> self;
Node* _node;
//构造函数
__list_iterator(Node* node)
:_node(node)
{}
};
我们使用list的节点指针来初始化构造迭代器对象,以上便是迭代器的基本框架,接下来就是迭代器的基本功能实现;
cpp
// 前置
self& operator++()
{
_node = _node->_next;
return *this;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
T& operator*()//解引用返回节点的数据即可
{
return _node->_data;
}
T* operator->()//通过指针访问成员,常用于自定义类型
{
return &_node->_data;
}
//迭代器比较,比较的是指针是否相等,如果指针相等,则它们指向同一节点
bool operator!=(const self& s)
{
return s._node != _node;
}
这些功能实现以后,我们还不能直接使用迭代器,我们需要把迭代器包装到我们实现的list当中;
在list里边实现begin( )、end( );
cpp
class list
{
public:
typedef list_node<T> Node;
typedef __list_iterator<T> iterator;
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
iterator begin();
iterator end();
private:
Node* _head;
size_t _size;
};
在实现begin( )、end( );之前,我们需要明确它们指向的位置是哪里?
cpp
iterator begin()
{
return _head->_next;
//返回时会自动调用iterator构造函数
}
iterator end()
{
return _head;
}
list的插入和删除
我们在list的基础上包装了一次iterator,那么接下来我们就在有迭代器的基础上实现insert和erase操作;
insert
根据传进来的迭代器的位置进行插入操作:
cpp
iterator insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
// 插入节点
newnode->_next = cur;
cur->_prev = newnode;
prev->_next = newnode;
newnode->_prev = prev;
_size++;
return iterator(newnode);
}
小tips:
双向循环链表在插入链接时很容易出现节点丢失的情况,为了尽可能的避免,这里推荐创建一个变量来记录当前节点的前一个位置;
erase
cpp
iterator erase(iterator pos)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
--_size;
return iterator(next);
}
注意:
erase之后迭代器会失效,因为迭代器指向的节点已经被删除释放,为了避免非法访问,这里我们应该返回下一个节点;insert根据客观性来讲,应该返回新插入节点;
有了插入和删除,在尾插、尾删、头插、头删的接口中都可以复用插入删除操作:
cpp
void push_back(const T& x)
{
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back(const T& x)
{
erase(--end());
}
void pop_front(const T& x)
{
erase(begin());
}
list析构函数和拷贝构造
有了迭代器析构函数和拷贝构造就会非常简单,我们可以复用前边实现的功能
析构函数
cpp
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//注意删除之后需要更新it(迭代器),erase自动返回下一个节点位置
}
}
//析构函数
~list()
{
clear();
delete _head;//注意释放头节点
_head = nullptr;
}
拷贝构造
cpp
list(list<T>& lt)
//这里应该是list(const list<T>& lt),但这样写需要const迭代器,const迭代器下面会进行实现
//所有这里先不用const修饰
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
赋值重载
这里说传统写法,在复用前边功能实现也是非常简单
cpp
list<int>& operator=(list<int>& lt)
{
if (this != <)
{
clear();
for (auto e : lt)
{
push_back(e);
}
}
return *this;
}
现代写法:
cpp
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list<int>& operator=(list<int> lt)
{
swap(lt);
return *this;
}
迭代器拷贝构造、析构函数实现问题
普通迭代器基本已经实现完毕,那我们来思考一下,迭代器要不要实现析构函数和拷贝构造?
切记,迭代器不需要实现析构函数,如果实现析构函数释放空间就会把list节点释放;把节点指针给迭代器只是为了上迭代器能够访问list;
迭代器的拷贝构造和赋值重载也不需要实现,迭代器存在的目的只是为了模拟指针去访问,如果自己实现进行深拷贝那还怎么访问list节点;
const迭代器
在实现之前我们需要先捋清楚const迭代器const修饰的是什么?
const迭代器修饰的是iterator指向的对象,对象不能修改;
在普通迭代器前边加上const修饰(const iterator),它修饰的是iterator迭代器,迭代器要能够修改,因为我们要使用 it++向后遍历,而我们需要的是内容不能被修改;
所以要想实现内容不能修改,我们需要重新实现一个类,不能单纯的使用const修饰普通迭代器;
我们先使用简单的方法,再封装一个和iterator相似的类,命名为const_iterator;
再封装一个类它的内容和iterator的代码很类似,这样代码复用率很低,这里我们选择使用模板来实现一个类模板,可供普通迭代器和const迭代器使用;
这里我们可以给iterator多添加两个模板参数:
cpp
template <class T, class Ref, class Ptr >
// Ref引用 Ptr指针
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T,Ref,Ptr> self;
Node* _node;
}
为什么要添加?
在iterator类中支持修改的接口就俩个:
- operator*()
- operator->()
区别就在于它们的返回值,普通迭代器返回的是T&、T*;
const_iterator返回的是const T&、const T*;
它们是完全不同的类型,如何做到一个函数可以返回两种类型的参数,唯有模板参数,所以这里我们添加两个参数,一个用来返回指针类型(T* 和 const T*),一个用来返回引用(T& 和 constT&) ;
这样我们只需修改两个接口:
cpp
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
修改之后我们就可以把它包装在自己实现的list中:
cpp
class list
{
public:
typedef list_node<T> Node;
typedef __list_iterator<T> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//...
iterator begin();
iterator end();
const_iterator begin() const;// 内容不需要修改只需对函数进行修饰,修改返回类型
//因为返回的list节点指针可以构造iterator,也可以构造const_iterator
const_iterator end() const; // 成员函数后加const修饰的是成员函数,
// 成员函数不可以修改对象的成员变量
private:
Node* _head;
size_t _size;
};
我们写一个函数来测试一下:
cpp
void print_list(const list<int>& lt)
{
list<int>::const_iterator it = lt.begin();
while (it != lt.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;
思考
这个测试接口只可以输出 list<int> 类型;那么问题来了,如何修改让它可以适用于任何类型?又如何让它能够遍历任何容器(vector、list ... 都可以使用)?
如何修改让它可以适用于list的任何类型?其实很简单,添加一个模板参数即可;
cpp
template<typename T>
void print_list(const list<T>& lt)
{
// 使用class时,list<T>未实例化的类模板,编译器不能识别进行实例化
// 前面加一个typename就是告诉编译器,这里是一个类型,等list<T>实例化后
// 再去类里面去取并实例化出迭代器对象
// 这也就是class和typename的区别
typename list<T>::const_iterator it = lt.begin();
while (it != lt.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
如何让它能够遍历任何容器(vector、list ... 都可以使用),这里只需要再抽象一层,T可以是任何数据类型,vecror<string>、list<int>都是数据类型,由于每个容器的迭代器使用方式都基本一致,所以我们可以不指定遍历容器的迭代器,相同的使用方式就可以遍历其他的容器:
cpp
template<typename Con>
void print_container(const Con& con)
{
typename T::const_iterator it = con.begin();
while (it != con.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
完整代码:list模拟实现
内含反向迭代器的实现,目前阶段可以忽视
总结
list模拟实现的目的就是为了更深刻的感受封装,以及泛型编程, list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;以上便是本文的全部内容,希望对你有帮助,感谢阅读!