List的模拟实现
1、节点和链表的定义
由于C++实现的List是一个带头双向循环链表,所以我们这样设计节点:
cpp
template<class T>
struct List_node
{
T _data;
struct List_node* next;
struct List_node* prev;
// 构造
List_node(const T& val = T())// 匿名对象做参数
:_data(val)
,_next(nullptr)
,_prev(nullptr)
{}
};
这样初步设计链表:
cpp
template<class T>
class list
{
public:
using node = List_node<T>;// 重命名,更方便,防止漏类型参数T
// 构造
list()
:_head(new node)
,_size(0)
{
_head->_prev = _head;
_head->_next = _head;
}
private:
node* _head;// 头节点(哨兵位)
size_t _size;// 记录节点个数
};
2、push_back()
由于链表被创建时就有一个哨兵位,所以我们就不需要对链表是否为空的情况进行讨论,直接push_back:

cpp
// 尾插
void push_back(const T& val)
{
node* newnode = new node(val);
node* tail = _head->_prev;
newnode->_next = _head;
newnode->_prev = tail;
tail->_next = newnode;
_head->_prev = newnode;
++_size;
}
3、迭代器
我们实现List的迭代器,还能直接使用指针吗?
之前的vector,使用的是连续的int类型空间,我们将指针重命名成迭代器,就可以很好地完成自增、自减、访问数据。
那List呢?也是直接用指针吗?
List使用的是一个个节点。如果我们使用节点指针访问,得到的是整个节点而不是其中的数据,不符合逻辑;节点与节点之间也不是连续的,不能通过节点指针的+、-来定位某一个节点。
我们只能将List的节点封装成类,在类中通过运算符重载来重定义行为。
迭代器的设计是一种封装。封装的目的是隐藏底层的结构差异,以提供一个统一的方法访问容器。
类中可以通过运算符重载,重定义行为。
3.1、迭代器的设计
迭代器的本质是对节点指针的封装。
cpp
template<class T>
struct List_iterator// 实现为公有
{
using node = List_node<T>;// 重命名
node* _nodeptr;// 底层为节点指针
List_iterator(node* nodeptr = node*())
:_nodeptr(nodeptr)// 构造
{}
};
迭代器不需要析构。迭代器的拷贝只需浅拷贝。
我们加上几个重要的方法:重载*、重载前置++、重载!=。
cpp
template<class T>
struct List_iterator
{
using node = List_node<T>;
node* _nodeptr;
List_iterator(node* nodeptr)
:_nodeptr(nodeptr)
{}
T& operator*() { return _nodeptr->_data; }
List_iterator<T>& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
bool operator!=(const List_iterator<T> it)
{// *this与it一定不一样,this和it的_nodeptr可能一样
return _nodeptr != it._nodeptr;
}// 如果直接this != &it,会导致死循环
};
我们在List中简单实现begin(), end():
- begin():本质是指向哨兵位的下一个节点指针
- end():本质是指向哨兵位的节点指针
然后,我们使用范围for,结果是可以正常使用:

实现了迭代器,我们就要实现const_迭代器。
我们很容易想到:根据迭代器的实现,再实现一份const迭代器。
cpp
template<class T>
struct List_const_iterator
{
using node = List_node<T>;
node* _nodeptr;
List_const_iterator(node* nodeptr)
:_nodeptr(nodeptr)
{}
const T& operator*() { return _nodeptr->_data; }// 只有*重载不一样
List_const_iterator<T>& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
bool operator!=(const List_const_iterator<T> it)
{
return _nodeptr != it._nodeptr;
}
};
照抄出一份const迭代器,List_iterator都要改为List_const_iterator,太麻烦了。
我们可以做一步优化:迭代器的类名重命名成Self。
cpp
// 迭代器
template<class T>
struct List_iterator
{
using node = List_node<T>;
using Self = List_iterator<T>;
node* _nodeptr;
List_iterator(node* nodeptr)
:_nodeptr(nodeptr)
{}
T& operator*() { return _nodeptr->_data; }
Self& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }
};
// const迭代器
template<class T>
struct List_const_iterator
{
using node = List_node<T>;
using Self = List_const_iterator<T>;
node* _nodeptr;
List_const_iterator(node* nodeptr)
:_nodeptr(nodeptr)
{}
const T& operator*() { return _nodeptr->_data; }
Self& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }
};
这样一来,我们照抄的时候,就只需要改变一两个地方。这也体现了复用的思想。
值得注意的是,构造函数的函数名,不能用Self代替。
但是,我们是否可以再做一步优化?
乍一看,迭代器和const迭代器非常相似,只在解引用的返回值有区别,因为const迭代器指的是修饰const对象的迭代器,而const对象是不能被修改的。
我们能否合二为一,只在解引用的返回值的返回做区别?答案是:可以,在模板参数列表部分再加一个参数Ref。
cpp
// 迭代器
template<class T, class Ref>
struct List_iterator
{
using node = List_node<T>;
using Self = List_iterator;
node* _nodeptr;
List_iterator(node* nodeptr)
:_nodeptr(nodeptr)
{}
Ref operator*() { return _nodeptr->_data; }
Self& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
bool operator!=(const Self it) const
{
return _nodeptr != it._nodeptr;
}
};
然后,我们在List中,重命名出迭代器和const迭代器:
cpp
using iterator = List_iterator<T, T&>;
using const_iterator = List_iterator<T, const T&>;
我们就可以非常巧妙地利用一个模板,实例化出两个迭代器类。
3.2、迭代器其它方法的完善
重载前置++、--,重载后置++、--:
cpp
// 前置++
Self& operator++()
{
_nodeptr = _nodeptr->_next;
return *this;
}
// 后置++
Self operator++(int)
{
Self tmp = *this;
_nodeptr = _nodeptr->_next;
return tmp;
}
// 前置--
Self& operator--()
{
_nodeptr = _nodeptr->_prev;
return *this;
}
// 后置--
Self operator--(int)
{
Self tmp = *this;
_nodeptr = _nodeptr->_prev;
return tmp;
}
重载==、!=:
cpp
// 重载==
bool operator==(const Self& it) const { return _nodeptr == it._nodeptr; }
// 重载!=
bool operator!=(const Self it) const { return _nodeptr != it._nodeptr; }
List中的begin()、end():
cpp
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); }
4、插入、删除操作
插入:


cpp
// insert
iterator insert(iterator pos, const T& val)// 返回类型为iterator,防止迭代器失效
{
node* newnode = new node(val);
node* cur = pos._nodeptr;// pos是迭代器类,不能使用->,因为没有实现重载->
node* prev = cur->_prev;
newnode->_next = cur;
newnode->_prev = prev;
cur->_prev = newnode;
prev->_next = newnode;
++_size;
return iterator(newnode);
}
这样一来,我们的头插、尾插就可以复用:
cpp
// 尾插
void push_back(const T& val) { insert(_head, val); }
// 头插
void push_front(const T& val) { insert(_head->_next, val); }
删除:


cpp
// pop_back
void pop_back() { erase(_head->_prev); }
// pop_front()
void pop_front() { erase(_head->_next); }
5、链表的析构
const函数,const修饰的是*this。
实现析构,我们可以先实现clear():只保留链表的头节点。
我们可以使用迭代器实现:
cpp
// clear
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
析构函数在clear()的基础上,只需再销毁哨兵位:
cpp
// 析构
~list()
{
clear();
delete _head;
_head = nullptr;
}
6、拷贝构造与赋值重载
6.1、几个常见的构造方法
我们先来实现initializer_list构造:
cpp
// initializer_list
list(const initializer_list<T>& il)
{
for (auto& e : il)
{
push_back(e);
}
}
测试时,程序崩溃了。
原因是此处的构造,没有构造出哨兵位。所以之后的行为都是未定义的。
我们不妨另外定义一个函数,专门生成哨兵位,以后每个构造函数,都要调用这个函数:
cpp
void empty_init()
{
_head = new node(T());
_head->_prev = _head;
_head->_next = _head;
}
cpp
// initializer_list
list(const initializer_list<T>& il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}

我们在实现Print()打印时,如果直接这样写会导致编译错误:
我们可以简单理解出错的原因:编译器无法理解此处的it,到底是const_iterator对象,还是一个静态成员变量。
所以我们在
list<T>::const_iterator前加上typename,用来告诉编译器,const_iterator是一个类型。
然后来实现迭代器构造:
cpp
// 迭代器构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
再来实现n个值构造:
cpp
list(size_t n, const T& val = T())
{
empty_init();
for (size_t i = 0; i < n; ++i)
{
push_back(n);
}
}
由于我们无法采用更好的方式解决此时的n个值构造 与迭代器构造匹配混乱的问题,所以我们加上一个重载进行补救:
cpp
list(size_t n, const T& val = T())
{
empty_init();
for (size_t i = 0; i < n; ++i)
{
push_back(n);
}
}
list(int n, const T& val = T())
{
empty_init();
for (int i = 0; i < n; ++i)
{
push_back(n);
}
}
6.2、拷贝构造与赋值重载的一般写法
拷贝构造:
- 创建哨兵位
- 遍历,push_back
cpp
// 拷贝构造
list(const list<T>& li)
{
empty_init();
for (auto e : li)
{
push_back(e);
}
}

赋值重载:
- 避免自己给自己赋值
- 清理到留下哨兵位
- 遍历,push_back
cpp
// 赋值重载
list<T>& operator=(const list<T>& li)
{
if (this != &li)
{
clear();
for (auto e : li)
{
push_back(e);
}
}
return *this;
}

6.2、拷贝构造与赋值重载的现代写法
拷贝构造与赋值重载现代写法的核心:抢夺别人的成果
拷贝构造:
cpp
list(const list<T>& li)
{
empty_init();// 不能使用tmp(li),这会调用拷贝构造,导致无穷递归
list<T> tmp(li.begin(), li.end());
swap(tmp);
}
赋值重载:
cpp
// 现代写法
list<T>& operator=(list<T> li)
{
clear();
swap(li);
return *this;
}
7、重载->与迭代器的完善
7.1、重载->
我们之前使用List,每一个节点保存的都是比较简单的内置类型。
那如果,每一个节点存入一个自定义类型呢:
cpp
struct A
{
int _a1;
int _a2;
A(int a1 = 0, int a2 = 0)
:_a1(a1)
,_a2(a2)
{}
};
我们只需传入多个参数,进行隐式类型转换,就可以完成一些简单的初始化、插入等操作:
cpp
l1.push_back({ 1, 1 });
l1.push_back({ 2, 2 });
l1.push_back({ 3, 3 });
l1.push_back({ 4, 4 });
使用迭代器打印看看呢?

显然,直接打印A类型的对象是不行的。
我们只能依次打印出A类型成员:

这样就不太简洁,因为我们会很自然地理解为:(*[指针]).的行为等价于[指针]->。
所以这里需要我们实现重载->:
cpp
T* operator->() { return &_nodeptr->_data; }

乍一看,我们可能对当前->的行为有点费解:
cpp
cout << it->_a1 << ":" << it->_a2 << endl;
其实当前->会被处理,这是处理后的模样:
cpp
cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;
迭代器it,通过重载->,访问到当前it指向链表节点存储的A对象(的指针),再通过A对象的指针,访问到A对象下的成员。

7.2、迭代器的完善
有了迭代器的重载->,就需要有const迭代器的重载->。
再写一个重载->吗?不需要。我们借助模板:
cpp
template<class T, class Ref, class Ptr>
struct List_iterator
{
// 重载->
Ptr operator->() { return &_nodeptr->_data; }
};
template<class T>
class list
{
public:
using iterator = List_iterator<T, T&, T*>;// 实例化出了两种迭代器类
using const_iterator = List_iterator<T, const T&, const T*>;
};


