C++list详解

递归何不归:个人主页
个人专栏 : 《C++庖丁解牛》《数据结构详解》
在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸
一,list是什么
list即是带头双向链表,是顺序表的一种,以链式结构实现。
二,list的迭代器
迭代器是一种可以遍历容器中元素的对象,提供了统一的访问接口。
但是不同的迭代器在操作上是存在差异的,
例如list 的迭代器受限于list的链式结构,无法实现vector 迭代器可以实现的+=、- =操作
2.1迭代器类型
我们这里详细说一下常见的迭代器类型
| 迭代器类型 | 方向 | 读写权限 | 支持的操作 | 典型容器 | 标签 |
|---|---|---|---|---|---|
| 输入迭代器 (Input Iterator) | 单向 (→) | 只读 (一次) | ++ *(读) == != |
istream_iterator |
input_iterator_tag |
| 输出迭代器 (Output Iterator) | 单向 (→) | 只写 (一次) | ++ *(写) |
ostream_iterator |
output_iterator_tag |
| 前向迭代器 (Forward Iterator) | 单向 (→) | 读写 (多次) | ++ * -> == != |
forward_list unordered_set |
forward_iterator_tag |
| 双向迭代器 (Bidirectional Iterator) | 双向 (↔) | 读写 | ++ -- * -> == != |
list set map |
bidirectional_iterator_tag |
| 随机访问迭代器 (Random Access Iterator) | 随机 (跳跃) | 读写 | + - += -= [] < > <= >= |
vector deque array |
random_access_iterator_tag |
我们可以一一对照,发现:
- vector 的迭代器是随机访问迭代器
- list 的迭代器是双向迭代器
三,简述emplace_back接口
emplace_back和push_back都是在尾部插入一个元素,但是emplace_back是可变参数模版,这意味着他可以支持隐式类型转换

四,杂项接口细节
4.1简单概述
| 接口 | 作用 |
|---|---|
| empty | 检测list是否为空,是返回true,否则返回false |
| size | 返回list中有效节点的个数 |
| front | 返回list的第一个节点中值的引用 |
| back | 返回list的最后一个节点中值的引用 |
| 接口 | 作用 |
|---|---|
| push_front | 在list首元素前插入值为val的元素 |
| pop_front | 删除list中第一个元素 |
| push_back | 在list尾部插入值为val的元素 |
| pop_back | 删除list中最后一个元素 |
| insert | 在list position 位置中插入值为val的元素 |
| erase | 删除list position位置的元素 |
| swap | 交换两个list中的元素,最好使用这个,使用std::swap会因为拷贝而产生较大的开销 |
| clear | 清空list中的有效元素 |
| merge | 合并链表,需要两个链表有序,底层有点类似于归并排序的逻辑,双指针遍历两个链表找小插入在返回拷贝。被合并的list会变空。 |
| unique | 去重,同样要求链表有序,原因是底层是默认相同的数据被放在一起。 |
| remove | 通过元素的值来移除元素。 |
此处还有一个splice接口,我们待会再讲
4.2细讲splice接口


这是splice函数的函数原型
splice的作用简单来说就是剪切,即将一个list中的一些节点转移到另一个list中去
cpp
void test_list1()
{
list<int> l1 = { 1,2,3,4,5 };
list<int> l2;
l2.splice(l2.begin(),l1, l1.begin(), l1.end());
}

可以看到,l1中的数据被转移到了l2中
五,list的排序效率问题
list并没有sort的函数重载 ,我们需要调list::sort来对list进行排序
但是
先将list拷贝到vector中进行排序再拷贝回来高
六,底层探究:手写一个list
6.1结构介绍
list主要实现的是三个部分:
- 1、单个的节点
- 2、迭代器
- 3、list本体
6.2结点类的实现
cpp
template<class T>
struct ListNode
{
ListNode(const T& val = T())
{
_val = val;
_pPre = nullptr;
_pNext = nullptr;
}
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
6.3迭代器的实现逻辑(重点)
迭代器的底层其实就是原生指针,但是迭代器的实现是一个封装过的类
为什么要将指针封装起来?答:方便++、--等函数的重载
6.3.0函数的声明和成员变量
此处存在一个问题:我们需要实现iterator和const_itreator两种迭代器,应该如何将这两种迭代器整合在一个class中呢
这里先埋下一个伏笔,后面再讲
cpp
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self;
}
6.3.1operator*
cpp
//Ref == T& or const T&
Ref operator*()
{
return _pNode->_val;
}
6.3.2operator++和--
cpp
Self& operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
Self operator++(int)
{
Self ret = *this;
_pNode = _pNode->_pNext;
return ret;
}
Self& operator--()
{
_pNode = _pNode->_pPre;
return *this;
}
Self operator--(int)
{
Self ret = *this;
_pNode = _pNode->_pPre;
return *this;
}
6.3.3operator==与!=
cpp
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
6.3.4operator->
这里还是有一点讲究的
cpp
//Ptr==const T* orT*
Ptr operator->()
{
return &_pNode->_val;
}
这里调用的时候其实是跳过了一个步骤:其实本来应该是两个解引用箭头才合理
此处是经过特殊处理
6.4list类的实现逻辑
list基于带头双向链表实现
list的成员变量包括哨兵结点_head 和 元素个数_size
6.4.1先前的迭代器问题
我们之前写迭代器的类的时候在类的模版中加入了更多的参数
cpp
template<class T, class Ref, class Ptr>
此时我们在list类这样写:
cpp
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
这实际上就是通过后两个参数来区分cosnt_iterator和iterator
就是将本来是我们做的工作交给了编译器 ,我们这里通过模版来实现大部分相似的功能,在通过参数区分这两种迭代器
6.4.2构造&拷贝构造&赋值重载&析构
cpp
list()
{
CreateHead();
}
list(int n, const T& value = T())
{
CreateHead();
for (int i = 0; i < n; i++)
{
push_back(value);
}
}
template <class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
//此处也不会破坏封装,还是不要直接访问来的好
++first;
}
}
list(const list<T>& l)
{
CreateHead();
//这里是还在构造过程中,所以如果直接使用this就会产生无限递归的问题
/*auto it = l.begin();
while (it != l.end())
{
push_back(it._pNode->_val);
++it;
}*/
for (auto it : l)
{
push_back(it);
}
}
list<T>& operator=( list<T> l)
{
swap(l);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
_size = 0;
_head->_pNext = _head;
_head->_pPre = _head;
}
void swap(list<T>& l)
{
std::swap(_head, l._head);
std::swap(_size, l._size);
}
private:
void CreateHead()
{
_head = new Node;
_head->_pNext = _head;
_head->_pPre = _head;
_size = 0;
}
6.4.3insert和erase的实现
cpp
iterator insert(iterator pos, const T& val)
{
PNode new_node = new Node(val);
Node* _pre = pos._pNode->_pPre;
Node* _next = pos._pNode;
_pre->_pNext = new_node;
new_node->_pPre = _pre;
_next->_pPre = new_node;
new_node->_pNext = _next;
_size++;
return iterator(new_node);
}
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
Node* _pre = pos._pNode->_pPre;
Node* _next = pos._pNode->_pNext;
_pre->_pNext = _next;
_next->_pPre = _pre;
delete pos._pNode;
_size--;
return iterator(_next);
}
6.5迭代器失效
由于list的链式结构,插入操作不会使迭代器失效,但是删除操作会导致迭代器失效
- insert操作后,迭代器仍然指向原来的位置
- erase操作后,当前位置的迭代器指向的空间被释放,迭代器失效,但是当前位置前后的迭代器没有失效
6.6特殊的构造方式(隐式类型转换)
我们常常可以看到这样的构造方法:
cpp
list<int> lt1 = { 1,2,3,4,5,6,7,8 };
这种方法实际上是先将{}里的数据写入到可以使用迭代器的initializer_list类型中 ,然后再使用initializer_list类型区间构造
cpp
void test_list4()
{
// 直接构造
list<int> lt0({ 1,2,3,4,5,6 });
// 隐式类型转换
list<int> lt1 = { 1,2,3,4,5,6,7,8 };
const list<int>& lt3 = { 1,2,3,4,5,6,7,8 };
func(lt0);
func({ 1,2,3,4,5,6 });
print_container(lt1);
//auto il = { 10, 20, 30 };
/* initializer_list<int> il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
cout << sizeof(il) << endl;*/
}
}
cpp
list(initializer_list<T> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
6.7按需实例化的体现
我们会发现,在我们没有调用模版的时候 ,模版中的一些非语法错误是不会被检查出来的
这是因为在这个模版没有被调用的时候 ,编译器是不会实例化这个模版的,这样的话就检查不出来一些**"不是很明显"**的错误
cpp
template<class Contianer>
void printf_contianer(const Contianer& con)
{
auto it = con.begin();
while (it != con.end())
{
*it += 10;
//如果没有使用这个模版,这里就不会报错
++it;
}
cout << endl;
for (auto e : con)
{
cout << e << " ";
}
cout << endl;
}