前言
要模拟实现 list,必须要熟悉 list 的底层结构以及其接口的含义。list 的底层是带头双向循环链表,通过上一篇文章的学习,这些内容已基本掌握,现在我们来模拟实现 list 容器的主要接口。
与前面的 vector 类似,由于使用了模板,也只分成.cpp 和.h 两个文件。.cpp 文件里放节点类、迭代器类、list 类及其成员函数、测试函数的实现,在.h 文件里进行测试。
本文的重点是对三个类的区分与理解,以及迭代器类的实现。
一、节点类
1. 为什么定义节点结构体时使用 struct 而不是 class?
答:(1) 其实用 class 也可以,但是 class 与 struct 默认的访问限定不同。当没有声明公有、私有时,struct 内容默认是公有,class 内容默认私有,所以用 class 要加上 public。
(2) 当我们用 class 没有加上 public,也没有实例化对象时,编译不会报错(报私有成员的错误),因为模版是不会被细节编译的。只有当我们实例化出对象,模版才会被编译,并且类的实例化并不是对所有成员函数都实例化,而是调用哪个成员函数就实例化哪个,这叫做按需实例化。
2. 可用匿名对象初始化
如果 T 是自定义类型,则调用其默认构造,并且 T 是内置类型也升级成了有默认构造的概念了。
template <class T>
struct ListNode {
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T()) : _next(nullptr), _prev(nullptr), _data(data) {}
};
二、迭代器类
前面学习的 string 类和 vector 的迭代器用的是原生指针类型,即 T*。但是在 list 容器中是不能这样的,因为前面两者的底层物理空间是连续的,符合迭代器 ++ 与 -- 的行为。但是 list 是由一个一个节点构成的,物理空间不连续,Node* 的 ++ 和 -- 不符合迭代器的行为,无法遍历。
所以用一个类把 Node* 封装,就可以重载运算符,使得用起来像内置类型,但会转换成函数调用,继而控制 Node* 的行为。
1. 普通迭代器类的实现
遍历需要的核心运算符重载是 、!=、++ 和 ->。所以只需要利用带头双向循环链表的特性,对 Node 进行封装,从而控制 Node* 的行为。
class ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T> Self; // 名字变得简短
public:
Node* _node; // 定义一个节点指针
ListIterator(Node* node) : _node(node) {}
// 前置:返回之后的值
// ++it;
Self& operator++() {
_node = _node->_next;
return *this;
}
Self& --() {
_node = _node->_prev;
*;
}
Self ++() {
;
_node = _node->_next;
tmp;
}
Self --() {
;
_node = _node->_prev;
tmp;
}
T& *() { _node->_data; }
T* ->() { &_node->_data; }
!=( Self& it) { _node != it._node; }
==( Self& it) { _node == it._node; }
};


