大纲:

1. list结构的介绍
cpp
namespace xiaoli
{
// 定义节点的结构
template <class T>
struct list_node
{
T data;
list_node<T>* next;
list_node<T>* prev;
list_node(const T& x = T())
:next(nullptr)
,prev(nullptr)
,data(x)
{
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
private:
Node* _head;
};
}
结构体成员介绍:data -- 存储数据;next -- 指向下一节点的指针;prev -- 指向上一节点的指针。
问题:为什么list中结点要单独定义?
1)list是双向链表,存储的数据不单单是数据,还有指针,封装为结构体便于更好的管理(即结点的核心任务是存储数据 + 维护前后指针)。
2)list容器的职责是管理结点集合,提供插入、删除、遍历等对外接口,修改结点结构不会影响容器的业务逻辑,反之亦然,这符合面向对象的设计原则,同时也跟C++库里面的设计保持一致!
3)设计为模板,是为了适配任何数据类型,更加方便操作。
注意:list含有哨兵位(即头结点)!!!
问题1:这个哨兵位是需要我们手动创建和释放的吗?
答案是:构造时必须手动创建哨兵位,析构时必须手动销毁哨兵位。标准库替你管了哨兵位,自己写的话必须亲手管,且要严格遵循 -- 先建哨兵位、后用链表,先清有效节点、后毁哨兵位 的顺序。
问题2:哨兵位是什么时候创建的?
答案是:刚开始的结构体我们对节点进行了初始化,然后再list的类定义了_head(后初始化)这个就是我们说的哨兵位。
2. 默认成员函数
2.1 构造函数
(1)无参构造
cpp
list()
:_head(new Node)
,_head->_next(_head)
,_head->_prev(_head)
{
}
这样写形式不是很好看,可以封装函数,当然这样也🆗。
cpp
empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
(2)构造n个val值
cpp
list(size_t n, const T* val = T())
{
empty_init();
for (size_t i = 0; i < n; i++)
{
push_back(i);
}
}
(3)初始化列表构造
cpp
list(initializer_list<T> lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
2.2 拷贝构造
拷贝构造:需要深拷贝,申请新的结点空间,通过更改结点的指向完成链接关系!!
这里的实现逻辑和push_back类似,直接复用即可!!
cpp
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
2.3 赋值运算符
这里直接采用现代写法,先构造一份临时对象,在和临时对象交换资源。(反正临时对象要销毁)
cpp
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
2.4 析构函数
析构函数:遍历链表,销毁每个节点中的 元素,**释放链表所有节点的内存,**这里无论是头删还是尾删都是一样的效率,最后清理哨兵位。
cpp
~list()
{
clear();
delete _head;
_head = nullptr;
}
3. 容器内容修改相关函数
3.1 push_front()

cpp
// 旧版
void push_front(const T& val)
{
// 提前保存好上一个节点
Node* tail = _head->_prev;
Node* newnode = new Node(val);
Node* next = _head->_next;
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = next;
next->_prev = newnode;
}
现代写法:直接套用insert,指定位置即可。
cpp
void push_front(const T& x)
{
insert(begin(), x);
}
3.2 pop_front()

cpp
void pop_front()
{
assert(_head != _head->_prev);
Node* tailNode = _head->_prev;
Node* cur = _head->_next;
Node* nextNode = cur->_next;
_head->_next = nextNode;
nextNode->_prev = _head;
delete cur;
}
现代写法:直接套用erase,指定位置即可。
cpp
void pop_front()
{
erase(begin());
}
3.3 push_back()

cpp
void push_back(const T& val)
{
Node* tail = _head->_prev;
Node* newnode = new Node(val);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
现代写法:直接套用insert,指定位置即可。
cpp
void push_back(const T& x)
{
insert(end(), x);
}
3.4 pop_back()

cpp
void pop_back()
{
assert(_head != _head->_next);
Node* tailNode = _head->_prev;
Node* newtail = tailNode->_prev;
newtail->_next = nullptr;
newtail->_prev = _head;
_head->_prev = newtail;
delete tailNode;
}
现代写法:
cpp
void pop_back()
{
erase(--end());
}
3.5 insert()

cpp
void insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
3.6 erase()

cpp
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* nextNode = cur->_next;
Node* prevNode = cur->_prev;
prevNode->_next = nextNode;
nextNode->_prev = prevNode;
delete cur;
return iterator(nextNode);
}
3.7 clear()
根据迭代器,获取哨兵位的结点,然后遍历链表进行结点的删除。
cpp
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
4. 迭代器
4.1 begin()和end()
首先先来看下面这个代码是否正确?
cpp
// 迭代器
typedef Node* iterator;
typedef const Node* const_iterator;
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
cpp
void test_2()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
list<int>::iterator it = l.begin();
while (it != l.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
如果按照这样去写的话,存在两个问题:
问题1:it是iterator,是指向结点的指针,那么按理说 *it 拿到的数据应该是结点,怎么可能拿到里面的数据data?
问题2:list不是vector,它的底层是双向带头链表,空间是不连续的,你怎么保证it++之后就能到达下一个结点?
问题3: *it 是一个node对象,如果没有为它重载<<运算符,代码会编译失败!
所以如何解决这个问题,内置类型会直接对指针进行解引用,但是这里的结果并不符合预期,那么我可以重定义实现重载啊,就像在之前实现日期类的时候,没有实现对于功能的运算符怎么办? -- 当时的解决办法也是运算符重载!!!!!
由于list里面++,--,*和->都需要实现重载,不妨封装为1个类,便于管理和理解。
所以正确的迭代器应该这样写:
cpp
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{
}
//int& operator*()
//{
// return _node->_data;
//}
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 *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self& operator--(int)
{
// 后置--,先保存后使用
Self tmp(*this);
_node = _node->_prev;
return *this;
}
Self operator!=(const Self& it)
{
return _node != it._node;
}
Self operator==(const Self& it)
{
return _node == it._node;
}
};
cpp
typedef list_iterator<T, T&, T*> iterator;
typedef const list_iterator<T, T&, T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin()
{
return const_iterator(_head->_next);
}
const_iterator end()
{
return const_iterator(_head);
}
注意:这里引入两个模板参数Ref和Ptr,是因为数据类型不一定是int,所以需要泛型化!!!同时还需要注意const修饰的是指向迭代器的内容,不是修饰迭代器本身!!!
4.2 迭代器失效的问题
迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。上一期vector的迭代器失效原因有2个:1)深拷贝的问题;2)删除元素时,编译器会对其标记,认为erase之后迭代器是失效的
这里list的迭代器失效主要是由于删除结点导致的!!!!!
解决办法:及时的更新it

正确更新如下:
cpp
void test_3()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
l.push_front(5);
xiaoli::list<int>::iterator it = l.begin();
while (it != l.end())
{
it = l.erase(it++);
}
}
到这里list的模拟实现就讲解完毕,下一期分享stack和queue的接口和模拟实现!!!