介绍
list 是双向链表 ,擅长中间位置 O(1) 插入/删除 ,但牺牲了随机访问性能与内存紧凑性;当需要频繁在非尾端 增删元素时,list 是首选容器。
模拟实现
构造函数
成员初始化列表只能 初始化成员本身,不能在列表里访问成员的成员
cpp
template<class T>
struct myList_node
{
myList_node(const T& val = T())
:_val(val)
,_prev(nullptr)
,_next(nullptr)
{}
T _val;
myList_node* _prev;
myList_node* _next;
};
template<class T>
class myList {
typedef myList_node<T> Node;
public:
myList() // 初始化头结点
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
}
private:
Node* _head;
};
拷贝构造

遍历旧链表,逐个节点拷贝到新链表
如果 T 是申请了内存资源的自定义类型,那么这里会触发 T 的拷贝构造,只要 T 的 拷贝构造 实现了深拷贝的逻辑,这里构造新节点时就不需要考虑深拷贝问题
cpp
myList(const myList<T>& ml) // 拷贝构造
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
Node* cur = ml._head->_next; // 旧
Node* tail = _head; // 新
while (cur != ml._head)
{
Node* newNode = new Node(cur->_val); // 构造新节点
tail->_next = newNode; // 链接新节点
newNode->_prev = tail;
tail = tail->_next;
cur = cur->_next;
}
tail->_next = _head; // 最后链接头节点
_head->_prev = tail;
}
迭代器
list 将迭代器封装为单独的类,并在类中实现运算符重载
构造
目前使用指针来实现迭代器
cpp
template<class T>
struct _myList_iterator
{
typedef myList_node<T> Node;
Node* _node;
_myList_iterator(Node* node) // 使用节点指针构造迭代器对象
:_node(node)
{}
};
运算符重载
重载迭代器可能用到的运算符,使迭代器的运算符的功能符合我们的预期
cpp
_myList_iterator<T>& operator++() // 前置++
{
_node = _node->_next;
return *this;
}
_myList_iterator<T> operator++(int) // 后置++
{
_myList_iterator<T> tmp(_node); // 构造临时对象
_node = _node->_next;
return tmp;
}
_myList_iterator<T>& operator--() // 前置--
{
_node = _node->_prev;
return *this;
}
_myList_iterator<T> operator--(int) // 后置--
{
_myList_iterator<T> tmp(_node); // 构造临时对象
_node = _node->_prev;
return tmp;
}
T& operator*()
{
return _node->_val;
}
// 加上 const 让 const 的迭代器也可以调用
bool operator==(const _myList_iterator<T>& ml_it) const // 判断迭代器的指向 是否相等
{
return _node == ml_it._node;
}
bool operator!=(const _myList_iterator<T>& ml_it) const
{
return !(_node == ml_it._node);
}
另外,为迭代器类重载指针解引用运算符 -> 需要注意:
如果选择第一种,那么返回的是 T 的指针类型,随后可以继续接 -> 运算符来访问 T 中的成员;
如果选择第二种,那么返回的是 T 的引用,随后可以接 . 运算符来访问 T 中的成员;
cpp
T* operator->()
{
return &_node->_val;
}
T& operator->()
{
return _node->_val;
}
标准库实现的是第一种,那么实际上调用时,展开函数调用是这个样子:
cpp
_myList_iterator<T> it(_node);
it.operator->()->"T的成员"
这里就连续地使用了两个 -> 解引用运算符,编译器为了优化可读性,直接两个变一个:
cpp
_myList_iterator<T> it(_node);
it->"T的成员"
const 迭代器
const 迭代器无非就是不想让你修改 该迭代器 所指向的 对象 的 成员
那么在迭代器类中,涉及到对 迭代器 指向 对象 的操作的 方法,就是两个 解引用运算符的重载
将其 返回值类型 加上 const 属性 修饰即可
但是标准库中的实现:是为迭代器类添加了额外的 2 个模板参数,让迭代器类在实例化的时候,就确定该迭代器 是否为 const 迭代器
cpp
template<class T, class Ref, class Ptr>
struct _myList_iterator
{
typedef myList_node<T> Node;
Node* _node;
......
Ref operator*()
{
return _node->_val;
}
......
Ptr operator->()
{
return &_node->_val;
}
};
该迭代器实例化时传参示例如下:
cpp
_myList_iterator<string, string&, string*>;
_myList_iterator<string, const string&, const string*>;
在 list 中实例化
实例化迭代器
cpp
template<class T>
class myList {
typedef myList_node<T> Node;
typedef _myList_iterator<T, T&, T*> iterator;
typedef _myList_iterator<T, const T&, const T*> const_iterator;
public:
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);
}
析构函数
需要逐个释放节点,使用指针即可
cpp
~myList() {
Node* cur = _head->_next;
while (cur != _head) {
Node* next = cur->_next; // 先保存下一个
delete cur; // 再释放当前
cur = next; // 继续
}
delete _head;
_head = nullptr;
}
operator=
(copy-and-swap 的小技巧)
赋值运算符的重载,直接复用 拷贝构造 的逻辑:使用传值传参,这时形参是实参的拷贝,然后交换形参与被赋值对象的头节点指针;出了函数作用域, ml 会被自动调用析构函数释放掉
cpp
myList<T>& operator=(myList ml)
{
std::swap(_head, ml._head);
return *this;
}
insert
在迭代器 position 前,插入一个值为 val 的元素
cpp
iterator insert(iterator position, const T& val)
{
Node* cur = position->_node;
Node* curPrev = cur->_prev;
Node* newNode = new Node(val); // new 一个新节点
// 链接新节点
curPrev->_next = newNode;
newNode->_prev = curPrev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
push_back
复用 insert
cpp
void push_back(const T& val)
{
insert(end(), val);
}
erase
释放当前迭代器处的节点,并返回下个新节点的迭代器
但是注意不能删除头节点,破坏双链表结构
cpp
iterator erase(iterator position)
{
Node* cur = position._node;
if (cur == _head) // 不能删除头节点
{
return iterator(_head);
}
Node* curPrev = cur->_prev; // 链接
Node* curNext = cur->_next;
curPrev->_next = curNext;
curNext->_prev = curPrev;
delete cur;
return iterator(curNext);
}
pop_back
复用 erase,也要注意不能删除头节点
cpp
void pop_back()
{
if (_head->_prev != _head)
{
erase(iterator(_head->_prev));
}
}
resize
void resize (size_t n, T val = T());
resize 依然要分段考虑,如果 n <= size( ),将链表从后向前缩减至 size() == n;(复用pop_back)
如果 n > size(),就 push_back n 个值为 val 的节点
那么首先实现 size(),难免要遍历一遍链表;当然,也可以提前在 myList 类中定义成员变量 _size,来记录当前链表的size
cpp
size_t size()
{
size_t cnt = 0;
iterator it = begin();
while (it != end())
{
cnt++;
it++;
}
return cnt;
}
void resize(size_t n, T val = T())
{
size_t cur_size = size(); // 提前缓存 size()
while (cur_size > n)
{
pop_back();
cur_size--;
}
while (cur_size < n)
{
push_back(val);
cur_size++;
}
}
这里为避免每次循环都调用 size() ,提前缓存好即可(进一步提升性能还得是像标准库中一样,维护一个 _size 成员来记录)
小结
有了一些手撕容器的思路,要注意深拷贝问题,从 拷贝构造 到 赋值运算符重载;
元素 的 增 方法中,要注意构造对象时的操作;删 方法中,要注意对资源的深度清理
对迭代器的封装 :主要是为了实现让迭代器的 运算符重载 支持预期的功能;以及迭代器需要实现两套(const)
且在使用过程中,要注意迭代器的失效问题,在 vector 容器中扩容之后尤其注意;因为 list 不涉及扩容,所以迭代器失效问题不明显;但只要迭代器失效就不能再使用,否则行为未定义
注意被封装的迭代器中,对 -> 解引用运算符的重载 ,其实是为 list<string<T>> 这样的类型准备的;这样迭代器就可以通过 -> 解引用实现对 string<T> 成员的访问,实际上有两层解引用