【C++】list 简介与模拟实现(详解)
文章目录
- 上文链接
- 一、list 类简介
- 二、迭代器
- 1. 迭代器的分类
- 2. list 中的迭代器
- 三、模拟实现
- 1. 整体框架
- 2. 构造函数
- (1) 节点的构造函数
- (2) 迭代器的构造函数
- (3) 链表的构造函数
- 3. 迭代器中的运算符重载
- (1) 解引用
- (2) ->
- (3) 前缀++
- (4) 后缀++
- (5) 前缀--
- (6) 后缀--
- (7) ==
- (8) !=
- 3. const迭代器
- (1) 对比普通迭代器
- (2) 模板优化
- 4. 链表中获取迭代器
- (1) begin
- (2) end
- 5. 链表的修改操作
- (1) insert
- (2) push_back
- (3) push_front
- (4) erase
- (5) swap
- 6. 链表容量相关
- (1) size
- (2) clear
- 7. 链表的拷贝构造函数
- 8. 链表的赋值重载
- 9. 链表的析构函数
- 10. 完整代码
上文链接
- 【C++】vector 的模拟实现(详解)
一、list 类简介
- list 参考文档:list - C++ Reference
list 是 C++ STL 库中的一个容器,它是一个模板类,可以认为它是一个带头的双向循环链表。
二、迭代器
1. 迭代器的分类
在前面的学习中我们了解了迭代器,他是一个类似于指针一样的东西。STL 中的容器中都有一个自己的迭代器类型,而迭代器从功能的角度可以分为三种:单向迭代器、双向迭代器和随机迭代器。
迭代器类型 | 典型容器 | 支持的操作 |
---|---|---|
单向迭代器(forward) | forward_list (单链表) / unordered_map… | ++ |
双向迭代器(bidirectional) | list / map… | ++ / -- |
随机迭代器(random access) | string / vector / deque… | ++ / -- / + / - |
一个容器的迭代器类型是什么取决于该容器的底层结构,比如之前学过的 vector 和 string 它们的物理空间是连续的,从一个位置就可以快速 +
到另外一个位置,所以它能够支持 +
和 -
的操作,是随机迭代器。而像链表这样的结构它想要 +
到后面的位置只能一个一个位置地移动,效率较低,所以没有提供 +
和 -
这样的操作。
在许多参数列表中,参数的类型名字就暗示了要传何种类型的迭代器。比如说算法库中的 reverse 函数:
从名字上可以看出,迭代器应该传双向迭代器。同时这里传随机迭代器也可以,因为随机迭代器支持双向迭代器的所有操作,即 ++
和 --
。
2. list 中的迭代器
在之前学习 string 和 vector 的时候我们的迭代器都是用指针来模拟实现的,那么这里的 list 是否可行呢?答案是否定的。因为 string 和 vector 的结构是连续的,用指针解引用就是当前位置的数据,对指针进行 ++
就是下一个位置的指针。但是对于一个链表的节点的指针 Node*
而言,用指针解引用就不是当前位置的数据,而是节点。同样,对指针进行 ++
操作更不是下一个节点的地址。因此我们不能用普通的指针去实现链表的迭代器。
所以在 STL 库中,用了一个类对节点的指针进行封装,在这个类中,重载了 *
和 ++
等运算符,使得我们可以直接对链表的迭代器进行 *
之后直接访问到链表中的数据;对迭代器进行 ++
之后移动到下一个节点处。而这整个类就是链表的迭代器。
下面是某版本 STL 中 list.h 的部分源码:
struct __list_iterator { // 用一个类去封装作为链表的迭代器typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;typedef __list_iterator<T, Ref, Ptr> self;typedef bidirectional_iterator_tag iterator_category;typedef T value_type;typedef Ptr pointer;typedef Ref reference;typedef __list_node<T>* link_type;typedef size_t size_type;typedef ptrdiff_t difference_type;link_type node; // link_type 是链表节点的指针__list_iterator(link_type x) : node(x) {}__list_iterator() {}__list_iterator(const iterator& x) : node(x.node) {}bool operator==(const self& x) const { return node == x.node; }bool operator!=(const self& x) const { return node != x.node; }reference operator*() const { return (*node).data; }#ifndef __SGI_STL_NO_ARROW_OPERATORpointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */self& operator++() { node = (link_type)((*node).next);return *this;}self operator++(int) { self tmp = *this;++*this;return tmp;}self& operator--() { node = (link_type)((*node).prev);return *this;}self operator--(int) { self tmp = *this;--*this;return tmp;}
};
注:由于 C++ 中
struct
升级为了类,在struct
中也可以定义函数。当我们不用访问限定符限制一个类中的任何变量或者函数的时候我们一般可以把这个类定义在一个struct
中,比如说上面所看到的链表迭代器的封装。如果需要访问限定符限制,则定义在class
中。
三、模拟实现
1. 整体框架
// list.h
namespace mine
{// 链表的节点template<class T>struct list_node{list_node* _next; // 指向下一个节点list_node* _prev; // 指向前一个节点T _data;// ...};// 迭代器template<class T>struct list_iterator{typedef list_node<T> Node; // 节点typedef list_iterator<T> Self; // 迭代器Node* _node; // 节点的指针// ...};// 链表template<class T>class list{// typedef 后的名称也受访问限定符的限制,这里的 Node 是私有的typedef list_node<T> Node;public:typedef list_iterator<T> iterator;// ...private:Node* _head;size_t _size;};
}
2. 构造函数
(1) 节点的构造函数
节点的构造函数相当于我们 new 一个节点的时候初始化数据,可以给定一个值 x
进行初始化,如果不传参数则采用默认值。
list_node(const T& x = T()):_next(nullptr),_prev(nullptr),data(x)
{}
(2) 迭代器的构造函数
生成一个节点对应的迭代器,只需要传入该节点的指针即可。
list_iterator(Node* node):_node(node)
{}
(3) 链表的构造函数
首先需要我们开辟一个节点作为头节点 (哨兵节点),由于是双向带头的循环链表,所以我们需要让 _next
和 _prev
都指向自己。
list()
{_head = new node;_head->_next = _head;_head->_prev = _head;_size = 0;
}
为了方便起见,我们可以额外写一个 empty_init
函数来完成这里的初始化过程,这样的话之后的拷贝构造函数也可以利用此函数。
void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}list()
{empty_init();
}
3. 迭代器中的运算符重载
(1) 解引用
对迭代器解引用的目的是想要访问到链表中节点的数据而不是节点本身,因此我们返回节点中的 _data
即可。
T& operator*()
{return _node->_data;
}
(2) ->
这里直接说结论了,正确的 ->
运算符重载写法如下:
T* operator->()
{return &_node->_data;
}
但是关于这个运算符需要补充讲解一个点。
struct AA
{int _a1;int _a2;AA(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}
};void test()
mine::list<AA> lt1;
lt1.push_back({ 1, 1 });
lt1.push_back({ 2, 2 });
lt1.push_back({ 3, 3 });mine::list<AA>::iterator lit1 = lt1.begin();
while (lit1 != lt1.end())
{cout << lit1->_a1 << endl; cout << lit1->_a2 << endl;++lit1;
}
cout << endl;
- 运行结果
1
1
2
2
3
3
按理来说根据 ->
的重载的写法,返回的是节点数据的指针,那么正确的写法应该是 lit->->_a1
才能访问到 AA
中的数据才对。但是现在只用了一个 ->
。这里就是编译器所做的优化:省略了一个 ->
,目的是增加可读性。
显式地写两个 ->
编译器会报错,但是我们可以显式地调用 ->
的重载函数,然后再用 ->
。
lit1->->_a1; // ERROR
lit1.operator->()->_a1; // OK
(3) 前缀++
前缀 ++
操作将迭代器移动至当前节点的下一个节点。
Self& operator++()
{_node = _node->_next;return *this;
}
(4) 后缀++
后缀 ++
操作将迭代器移动至当前节点的下一个节点,但表达式的结果是当前节点。所以我们可以先用一个临时变量记录当前节点,将迭代器移动过后返回这个临时变量即可。
注意这里不能传引用返回!因为 tmp
是函数中创建的一个临时对象,出了这个函数就销毁了。用引用返回的话会导致引用变成 “野引用”。
Self operator++(int) // 不能用引用返回
{Self tmp(*this);_node = _node->_next;return tmp;
}
(5) 前缀–
Self& operator--()
{_node = _node->_prev;return *this;
}
(6) 后缀–
Self operator--(int) // 不能用引用返回
{Self tmp(*this);_node = _node->_prev;return tmp;
}
(7) ==
bool operator==(const Self& s) const
{return _node == s._node;
}
(8) !=
bool operator!=(const Self& s) const
{return _node != s._node;
}
3. const迭代器
(1) 对比普通迭代器
const迭代器的要求是迭代器所指向的内容不能修改,而不是迭代器本身不能修改。所以我们不能单纯地用 const iterator
来表示 const迭代器,因为它表示的是迭代器本身不能修改。所以我们需要单独实现一个和普通迭代器 iterator
高度相似的类作为 const迭代器。
- 命名
// struct list_iterator
struct list_const_iterator// typedef list_iterator<T> Self;
typedef list_const_iterator<T> Self;// 在 list 类中额外为const迭代器typedef
typedef list_const_iterator<T> const_iterator;
- 解引用运算符重载
由于迭代器所指向的内容不能修改,而解引用返回的内容正是所指向的内容,所以在 const迭代器中,解引用操作符重载函数的返回值类型改为了 const T&
。
// T& operator*()
// {
// return _node->_data;
// }const T& operator*() // 返回值多加了一个const
{return _node->_data;
}
->
运算符重载
这个函数同理,在 const迭代器中我们需要对它的返回值类型进行修改,以达到迭代器指向的内容不可修改的目的。
// T* operator->()
// {
// return &_node->_data;
// }const T* operator->()
{return &_node->_data;
}
const迭代器:
template<class T>
struct list_const_iterator // const版本的迭代器
{typedef list_node<T> Node;typedef list_const_iterator<T> Self;Node* _node;list_const_iterator(Node* node):_node(node){}const T& operator*() {return _node->_data;}const T* operator->(){return &_node->_data;}// 其他函数的实现与普通迭代器一致
};
(2) 模板优化
仔细观察了上面的 const迭代器之后发现,从功能的角度来说,两个迭代器只有一个不同:就是只有解引用和 ->
操作符重载的返回类型不同,其他完全一样,代码复用率很低。因此我们考虑采用模板来优化。
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;Ref operator*() // 由于两个迭代器的返回类型不同,所以这里的返回类型设置为一个模板{return _node->_data;}Ptr operator->() // 此处同理{return &_node->_data;}// ...
};template<class T>
class list
{typedef list_node<T> Node;
public:typedef list_iterator<T, T&, T*> iterator; // 传引用/指针返回的就是普通迭代器typedef list_iterator<T, const T&, const T*> const_iterator; // 传const引用/指针的就是const迭代器// ...
};
4. 链表中获取迭代器
(1) begin
获取第一个有效节点(头节点的下一个节点)的迭代器。
// typedef list_iterator<T> iterator;iterator begin()
{return iterator(_head->_next);
}
const迭代器版本:
const_iterator begin() const
{return const_iterator(_head->_next);
}
(2) end
获取最后一个有效节点下一个节点(头节点)的迭代器。
iterator end()
{return iterator(_head);
}
const迭代器版本:
const_iterator end() const
{return const_iterator(_head);
}
5. 链表的修改操作
(1) insert
在 pos 位置之前插入一个节点。
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;++_size;
}
(2) push_back
尾插一个节点。可以像下面这样老老实实地写:
push_back(const T& x)
{Node* tail = _head->_prev;Node* newnode = new Node(x);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;++_size;
}
更简洁的写法是直接复用 insert 函数:
void push_back(const T& x)
{insert(end(), x);
}
(3) push_front
头插一个节点。直接复用 insert 即可。
void push_front(const T& x)
{insert(begin(), x);
}
- 测试
void test_list_1()
{mine::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);mine::list<int>::iterator it = l1.begin();while (it != l1.end()){*it += 10;cout << *it << " ";++it;}cout << endl;l1.push_front(1);l1.push_front(2);l1.push_front(3);l1.push_front(4);for (auto e : l1){cout << e << " ";}cout << endl;
}int main()
{test_list_1();return 0;
}
- 输出
11 12 13 14
4 3 2 1 11 12 13 14
(4) erase
删除 pos 位置的节点。
void erase(iterator pos)
{assert(pos != end()); // 注意不能把哨兵位的节点删除了,所以断言一下Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;
}
上面的写法已经把基本的 erase 逻辑实现了,但是还存在一个问题就是迭代器失效,我们来看下面这样的例子。。
void test_list_2()
{mine::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto e : lt) cout << e << " ";cout << endl;mine::list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0){lt.erase(it);}else{++it;}}for (auto e : lt) cout << e << " ";cout << endl;
}int main()
{test_list_2();return 0;
}
- 运行结果
为什么会导致访问错误?就是因为我们删除了该节点之后,迭代器变成了一个“野指针”。再进行 ++
操作编译器就会报错。所以为了解决这个问题,当我们 erase 之后需要对迭代器进行重新赋值,赋值为被删除节点的下一个节点的迭代器。
iterator erase(iterator pos)
{assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return iterator(next); // 返回下一个节点的迭代器// return next // 也可以这样写,这样写就是隐式类型转换
}
因此 while 循环中的逻辑就变成了 lt = lt.erase(it)
。
while (it != lt.end())
{if (*it % 2 == 0){// lt.erase(it);it = lt.erase(it);}else{++it;}
}
- 修改之后运行结果
1 2 3 4
1 3
(5) swap
交换两个链表。只需交换两个链表的头节点的指针 _head
和 _size
即可。交换这两个内置类型用库中的 swap
函数即可。
void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}
6. 链表容量相关
(1) size
返回链表节点的个数(头节点除外)。
size_t size()
{return _size;
}
(2) clear
清空链表中的节点(头节点除外)。
void clear()
{auto it = begin();while (it != end()) // 遍历链表通过 erase 删除节点{it = erase(it);}
}
7. 链表的拷贝构造函数
list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}
8. 链表的赋值重载
list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}
9. 链表的析构函数
我们只需要自己实现链表的析构函数,而不需要自己显式地实现链表节点和迭代器的析构函数。因为编译器默认生成的析构函数就能够做到析构节点和迭代器并且不会造成内存泄漏。但是链表不行,因为链表中存储了节点的指针,编译器默认生成的析构函数不能释放指针所指向的空间,会造成内存泄漏的问题。所以我们需要手动地将链表节点释放再置空指针,避免出现内存泄漏的问题。
~list()
{clear();delete _head;_head = nullptr;
}
10. 完整代码
#pragma once
#include<iostream>
#include<cassert>using namespace std;namespace mine
{template<class T>struct list_node{list_node* _next;list_node* _prev;T _data;list_node(const T& x = T()):_next(nullptr),_prev(nullptr) ,_data(x){}};template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){}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 tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator==(const Self& s) const{return _node == s._node;}bool operator!=(const Self& s) const{return _node != s._node;}};//template<class T>//struct list_const_iterator//{// typedef list_node<T> Node;// typedef list_const_iterator<T> Self;// Node* _node;// list_const_iterator(Node* node)// :_node(node)// {}// const T& operator*()// {// return _node->_data;// }// Self& operator++()// {// _node = _node->_next;// return *this;// }// Self operator++(int)// {// Self tmp(*this);// _node = _node->_next;// return tmp;// }// Self& operator--()// {// _node = _node->_prev;// return *this;// }// Self operator--(int)// {// Self tmp(*this);// _node = _node->_prev;// return tmp;// }// bool operator==(const Self& s) const// {// return _node == s._node;// }// bool operator!=(const Self& s) const// {// return _node != s._node;// }//};template<class T>class list{typedef list_node<T> Node;public:typedef list_iterator<T, T&, T*> iterator;//typedef list_const_iterator<T> const_iterator;typedef list_iterator<T, const T&, const T*> const_iterator;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);}void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){auto it = begin();while (it != end()){it = erase(it);}}size_t size(){return _size;}//void push_back(const T& x)//{// Node* tail = _head->_prev;// Node* newnode = new Node(x);// tail->_next = newnode;// newnode->_prev = tail;// newnode->_next = _head;// _head->_prev = newnode;//}void push_front(const T& x){insert(begin(), x);}void push_back(const T& x){insert(end(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}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;++_size;}iterator erase(iterator pos){assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return iterator(next);}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}private:Node* _head;size_t _size;};
}