目录
前言
欢迎大家再次来到海盗猫的博客,今天讲解cpp中的list容器
在数据结构中,我们知道链表有单链表和双向链表,而我们今天要学习的list就是一个带有哨兵位的双向链表。
list文档:list - C++ Reference
list的使用
讲解list相关内容使用时的注意点和相关知识点
迭代器引申
在之前的容器学习中,我们已经知道迭代器是一种用来遍历容器的类型,而在vector和string的模拟实现中,iterator迭代器是由原生指针typedef而来,所以可以直接对迭代器进行自增自减以及加减整数个数的距离,以此来让迭代器指向容器的下一个元素。
这是因为string和vector的底层空间是一个连续的结构,但list链表的每个节点的空间是随机的,迭代器不再能随意的加减,说明list与前两者的迭代器类型本质不同,这就引出了迭代器的分类,
迭代器的分类:
迭代器按照不同的功能,性质分类:
随机迭代器为双向和单向迭代器的父类,其使用时为包含关系:
迭代器的性质类型决定了该容器可以使用的算法库#include <algorithm>中的算法
排序算法为例:
算法库中的sort就只能使用随机迭代器来访问,因为其底层需要对迭代器使用+-操作;双向迭代器和单向迭代器没有实现这两个操作;
相应的:
元素去重(需要容器有序):
此处标为单向迭代器,实际是因为底层需要++操作,而双向和随机迭代器都实现了++操作,所以同样可以正常调用这个函数
查找:
而find中,参数为InputIterator,使用时理解为单向、双向、随机迭代器三种迭代器都可以使用
Operations接口

reverse逆置与算法库中的逆置效果相同;
cpp//ls为链表对象 ls.reverse(); reverse(ls.begin(), ls.end()); //效果相同sort是list自己实现的成员函数,因为算法库algorithm中的sort只能使用随机迭代器作为参数调用(见下文sort详解);
merge合并需要两个链表都为有序,且合并完成后,作为参数的链表会变为空。其底层逻辑为取小节点尾插到目标节点中;
cppvoid test_list3() { list<double> first, second; first.push_back(3.1); first.push_back(2.2); first.push_back(2.9); second.push_back(3.7); second.push_back(7.1); second.push_back(1.4); //任意一个链表无序都将导致merge出错 first.sort(); second.sort(); first.merge(second); for (auto i : first) { //cout << i << ' '; printf("%.1lf ", i); } }unique去重也同样需要链表有序,不然会出现删除不完全的情况(这是因为该函数底层默认认为链表为有序,若相同的数据没有挨在一起,就会识别成不重复)
remove删除,传一个值,若找到就会删除,没有则不执行操作
splice:可以将一个链表,一个链表节点,一个迭代器区间内的链表节点,转移(剪切)到调用对象的指定位置(被转移的位置数据消失);
测试代码:
cppvoid test_list4() { list<double> first, second; first.push_back(1.1); first.push_back(2.2); first.push_back(3.3); second.push_back(1.0); second.push_back(2.0); second.push_back(3.0); second.push_back(4.0); second.push_back(5.0); second.push_back(6.0); cout << "first: "; for (auto i : first) { printf("%.1lf ", i); } cout << endl; cout << "second: "; for (auto i : second) { printf("%.1lf ", i); } cout << endl; //剪切整个list first.splice(first.end(), second); ////剪切一个 //list<double>::iterator it = second.begin(); //int n = 1; //while (n--) it++; //first.splice(first.begin(), second, it); ////一个区间 //first.splice(first.begin(), second, it, second.end()); cout << "splice after: \n"; cout << "first: "; for (auto i : first) { printf("%.1lf ", i); } cout << endl; cout << "second: "; for (auto i : second) { printf("%.1lf ", i); } }
emplace_back初识
目前由于其底层涉及右值引用,所以目前只需要知道其 用法与push_back类似即可,而相较于push_back,多了一种用法
cpp
class A {
int _a1 = 0;
int _a2 = 0;
public:
A(const int& a1 = 0, const int& a2 = 0)
:_a1(a1),
_a2(a2)
{
cout << "调用:A(const int& a1 = 0, const int& a2 = 0)" << endl;
}
A(const A& a) {
_a1 = a._a1;
_a2 = a._a2;
cout << "调用:A(const A& a)" << endl;
}
};
void test_list1() {
list<A> ls;
A a1(1, 1);
ls.push_back(a1);
A a2(2, 2);
ls.push_back(a2);
//push_back只能用同类型对象来进行插入
//ls.push_back(3,3);
A a3(3, 3);
ls.emplace_back(a3);
//emplace_back就可以通过直接传入构造函数的参数来使用,会直接调用构造后插入
ls.emplace_back(4, 4);
}

可以看到emplace_back 不仅可以像push_back 一样,传入同类型对象来插入;还可以直接将默认构造的参数传给emplace_back ,其会自动调用构造后插入
sort及性能对比
由于算法库<algorithm>中sort不支持双向迭代器,所以list自己实现了一个sort成员函数来进行排序。


这样,即便list不支持调用库中的sort也可以进行排序了;但当数据较大时,直接调用list的成员函数sort却存在性能问题:
测试代码(VS2022 Release下):
cpp
void test_sort() {
srand(time(0));
const int N = 1000000;
list<int> ls1;
list<int> ls2;
for (int i = 0; i < N; i++) {
auto e = rand() + i;
ls1.push_back(e);
ls2.push_back(e);
}
int begin1 = clock();
//使用list迭代器区间构造vector
vector<int> v(ls1.begin(), ls1.end());
//利用vector迭代器为随机迭代器的特点,来调用算法库中的sort
sort(v.begin(), v.end());
//assign可以插入一个迭代器区间
ls1.assign(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
ls2.sort();
int end2 = clock();
cout << "list copt vector sort list: " << end1 - begin1 <<endl;
cout << "list sort: " << end2 - begin2;
}

可以看到,当数据量较大时,即便是将list转换为vector排序再转换回来,用时依然比直接使用list的成员函数sort来的效率高
sort延申less、greater的使用
由于在排序算法中,都默认为升序,所以可以使用仿函数less和greater来实现降序排列
less和greater都是类,可以定义出该类型的对象,作为参数传给sort函数,达到升序和降序排列的效果
cpp
void test_list2() {
list<int> ls;
ls.push_back(1);
ls.push_back(5);
ls.push_back(2);
ls.push_back(3);
ls.push_back(7);
ls.push_back(4);
ls.push_back(10);
//algorithm库中sort算法需要随机调用,但此处还时不会提示错误
//sort(ls.begin(), ls.end());//运行时出错
//less<int> le;//降序
//greater<int> ge;//升序
//ls.sort(ge);
ls.sort(greater<int>());//使用匿名对象
//ls.sort(less<int>());//使用匿名对象
for (auto i : ls) {
cout << i << ' ';
}
}
list模拟实现
由于库中的list实现为带有哨兵位的双向链表结构,且链表的空间结构不再是string和vector中的顺序结构,这将导致模拟实现时的许多不同。
成员变量
由于list为带有哨兵位的双向链表,所以对于list自身,只有一个哨兵位节点的指针Node* _head = nullptr;,而list节点都是通过单独封装一个类来实现的;也因此,list的默认构造函数只需要使用_head成员创建一个哨兵位节点即可
构造函数
list类型中只含有一个哨兵位节点指针,所以不管哪种构造前提都要先构造出哨兵位节点,因此使用一个函数来构造哨兵位节点,这样其他类型的构造也可以调用
empty_init
cpp//只用来构造出哨兵位节点 void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; }默认构造
cpplist() { empty_init(); }拷贝构造
cpplist(const list<T>& ls) { //构造一个哨兵位并将list中的节点尾插赋值到这个哨兵位后 //使用空构造 empty_init(); for (auto n : ls) { push_back(n); } }迭代器区间构造
cpp//迭代器区间构造 template <class Iterator> list(Iterator first, Iterator last) { empty_init(); while (first != last) { push_back(*first); ++first; } }initializer_list简易介绍及构造
c++11中引入了这个类型模板,用以表示大括号括起来的一组数据,这个类型也具有迭代器,而其他的容器都可以使用这种类型来进行初始化
cppauto il1 = { 1,2,3,4,5 }; auto il2 = { 'a','b','c','d','e'}; cout << typeid(il1).name() << endl; std::list<int> ls(il1); print_container(ls); std::vector<int> v(il1); print_container(v); std::string s(il2); print_container(s);所以我们模拟实现时,就可以直接使用initializer_list的迭代器来插入数据到链表
cpplist(initializer_list<T> il) { empty_init(); for (auto& i : il) { push_back(i); } }
节点类型
list中每个节点都是一个独立的空间,所以每次插入一个数据,都是创建一个新节点,再链入list中
cpp
template<class T>
struct _list_node
{
typedef _list_node<T> Node;
T _data;//存储数据
Node* _next;//下一个节点的指针
Node* _prev;//上一个节点的指针
//初始化节点
_list_node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
};
迭代器
list中,由于物理结构上每个节点的存储空间不再连续,如果像string和vector中(迭代器底层为原生指针)一样,直接对迭代器++ --等操作,将会导致结果错误;
所以此时list迭代器底层不再是简单的原生指针typedef而来;而是将一个节点的指针封装起来的类型,用以在其中用重载的方法,实现模拟原生指针的各种操作
模拟实现iterator:
cpp
template<class T>
struct _list_iterator {
typedef _list_node<T> Node;
typedef _list_iterator<T> Self;
Node* _node;
_list_iterator() = default;
_list_iterator(Node* node)
:_node(node)
{}
//operator*返回data数据
T& operator*() {
return _node->_data;
}
//operator->返回data数据的指针
T* operator->() {
return &_node->_data;
}
//前置++ --
Self& operator++() {
_node = _node->_next;
return *this;
}
Self& operator--() {
_node = _node->_prev;
return *this;
}
//后置++ --
Self operator++(int) {
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int) {
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator==(const Self& it) {
return _node == it._node;
}
bool operator!=(const Self& it) {
return _node != it._node;
}
};
然后在list类中typedef _list_iterator iterator
此时若想实现const_iterator可以直接复制iterator后修改类型名,将operator*和operator->的返回值加上const,再在list类中typedef _list_const_iterator const_iterator即可;
但这样的方法会,两个类的代码极其相似,仅仅是俩个重载函数的返回值有无const和类型名不同,导致代码冗余。而我们又知道,模板函数模板类不就是用来解决类似这样的问题的吗?由此我们可以得出,或许模板可以帮我们解决这个问题。
我们++引入两个新的模板参数Ref、Ptr让他们来分别对应operator*和operator->的返回值++:
cpp
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() = default;
_list_iterator(Node* node)
:_node(node)
{}
//operator*返回data数据
Ref operator*() {
return _node->_data;
}
//operator->返回data数据的指针
Ptr operator->() {
return &_node->_data;
}
//前置++ --
Self& operator++() {
_node = _node->_next;
return *this;
}
Self& operator--() {
_node = _node->_prev;
return *this;
}
//后置++ --
Self operator++(int) {
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int) {
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator==(const Self& it) {
return _node == it._node;
}
bool operator!=(const Self& it) {
return _node != it._node;
}
};
同时,在list的typedef中,将后两个参数的类型对应为T&和T*、const T&和const T*
cpp
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
这样,当外部调用iterator的begin和end接口时,就会传入T&和T*从而使迭代器的*和->的结果可修改;调用const_iterator的begin和end时,则传入const T&和const T*,致使*和->的返回结果不能修改,从而实现普通迭代器和const迭代器的区分。
insert和erase
cpp
//list的insert不会产生迭代器失效,但库中有返回值,返回新插入的节点的迭代器
iterator insert(iterator pos, const T& data) {
//创建新节点
Node* newNode = new Node(data);
//记录pos位置前节点
Node* prev_node = pos._node->_prev;
//链入指定位置
newNode->_next = pos._node;
newNode->_prev = prev_node;
prev_node->_next = newNode;
pos._node->_prev = newNode;
return newNode;
}
cpp
//erase后指向当前节点的迭代器失效(野指针),返回下一个节点的迭代器
iterator erase(iterator pos) {
assert(pos != end());
//记录删除节点的前后节点地址
Node* prev_node = pos._node->_prev;
Node* next_node = pos._node->_next;
delete pos._node;
prev_node->_next = next_node;
next_node->_prev = prev_node;
return next_node;
}
迭代器失效
list中insert不再有迭代器失效的问题,因为每个节点的空间不再连续,即便在pos位置插入新节点,原本迭代器指向的空间位置也不会发生改变,而且list也没有扩容这一说,但基于库中的实现逻辑,insert会返回插入的新节点的地址;
而erase,因为会删除pos位置的节点,再链接前后节点,这导致指向删除节点的位置的迭代器变为一个野指针,导致迭代器失效,所以erase后需要更新指针指向,erase会返回删除节点的下一个节点地址
后记
关于list相关的内容我们就介绍到这里,上文中没有提及的接口,有些现阶段掌握不住,有些则于之前的容器相似,较为简单,需要请查阅附完整代码来理解其底层
本期专栏:C++_海盗猫鸥的博客-CSDN博客
个人主页:海盗猫鸥-CSDN博客
本篇博客就到这里,我们下期见~
附完整代码
++list.h++
cpp
#pragma once
#include<iostream>
#include<assert.h>
using std::swap;
using std::initializer_list;
namespace hdmo{
//list节点
template<class T>
struct _list_node
{
typedef _list_node<T> Node;
T _data;//存储数据
Node* _next;//下一个节点的指针
Node* _prev;//上一个节点的指针
//初始化节点
_list_node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
};
//迭代器,一个被封装起来的节点指针
//引入额外的模板参数,用于作为operator*和operator->的返回值
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() = default;
_list_iterator(Node* node)
:_node(node)
{}
//operator*返回data数据
Ref operator*() {
return _node->_data;
}
//operator->返回data数据的指针
Ptr operator->() {
return &_node->_data;
}
//前置++ --
Self& operator++() {
_node = _node->_next;
return *this;
}
Self& operator--() {
_node = _node->_prev;
return *this;
}
//后置++ --
Self operator++(int) {
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int) {
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator==(const Self& it) {
return _node == it._node;
}
bool operator!=(const Self& it) {
return _node != it._node;
}
};
//template<class T>
//struct _list_const_iterator {
// typedef _list_node<T> Node;
// typedef _list_const_iterator<T> Self;
// Node* _node;
// _list_const_iterator() = default;
// _list_const_iterator(Node* node)
// :_node(node)
// {}
// //返回data数据
// const T& operator*(){
// return _node->_data;
// }
// const T* operator->(){
// return &_node->_data;
// }
// //前置++ --
// Self& operator++() {
// _node = _node->_next;
// return *this;
// }
// Self& operator--() {
// _node = _node->_prev;
// return *this;
// }
// //后置++ --
// Self operator++(int) {
// Self tmp(*this);
// _node = _node->_next;
// return tmp;
// }
// Self operator--(int) {
// Self tmp(*this);
// _node = _node->_prev;
// return tmp;
// }
// bool operator==(const Self& it) {
// return _node == it._node;
// }
// bool operator!=(const Self& it) {
// return _node != it._node;
// }
//};
//list主体
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;
//空构造,用于创建哨兵位
void empty_init() {
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
//默认构造
list() {
empty_init();
}
//拷贝构造
list(const list<T>& ls) {
//构造一个哨兵位并将list中的节点尾插赋值到这个哨兵位后
//_head = new Node;
//_head->_next = _head;
//_head->_prev = _head;
//使用空构造
empty_init();
for (auto& n : ls) {
push_back(n);
}
}
list(initializer_list<T> il) {
empty_init();
for (auto& i : il) {
push_back(i);
}
}
//迭代器区间构造
template <class Iterator>
list(Iterator first, Iterator last) {
empty_init();
while (first != last) {
push_back(*first);
++first;
}
}
//n个value构造
list(int n, const T& value = T()) {
empty_init();
while (n--) {
push_back(value);
}
}
//赋值
list<T>& operator=(list<T> ls) {
swap(ls);
return *this;
}
//析构
~list() {
//清楚所有节点,最后销毁哨兵位
clear();
delete _head;
_head = nullptr;
}
//交换,交换两个list的成员
void swap(list<T>& ls) {
std::swap(_head, ls._head);
}
iterator begin() {
return _head->_next;
}
iterator end() {
return _head;
}
const_iterator begin() const {
return _head->_next;
}
const_iterator end() const {
return _head;
}
void push_back(const T& data) {
////在哨兵位前插入
//Node* newNode = new Node(data);
//Node* tail = _head->_prev;//原本的尾节点
//newNode->_next = _head;
//newNode->_prev = tail;
//tail->_next = newNode;
//_head->_prev = newNode;
insert(end(), data);
}
void push_front(const T& data) {
insert(begin(), data);
}
void pop_back() {
erase(--end());
}
void pop_front() {
erase(begin());
}
//void insert(iterator pos, const T& data) {
// //创建新节点
// Node* newNode = new Node(data);
// //记录pos位置前节点
// Node* prev_node = pos._node->_prev;
// //链入指定位置
// newNode->_next = pos._node;
// newNode->_prev = prev_node;
// prev_node->_next = newNode;
// pos._node->_prev = newNode;
//}
//list的insert不会产生迭代器失效,但库中有返回值,返回新插入的节点的迭代器
iterator insert(iterator pos, const T& data) {
//创建新节点
Node* newNode = new Node(data);
//记录pos位置前节点
Node* prev_node = pos._node->_prev;
//链入指定位置
newNode->_next = pos._node;
newNode->_prev = prev_node;
prev_node->_next = newNode;
pos._node->_prev = newNode;
return newNode;
}
//void erase(iterator pos) {
// assert(pos != end());
// //记录删除节点的前后节点地址
// Node* prev_node = pos._node->_prev;
// Node* next_node = pos._node->_next;
// delete pos._node;
// prev_node->_next = next_node;
// next_node->_prev = prev_node;
//}
//erase后指向当前节点的迭代器失效(野指针),返回下一个节点的迭代器
iterator erase(iterator pos) {
assert(pos != end());
//记录删除节点的前后节点地址
Node* prev_node = pos._node->_prev;
Node* next_node = pos._node->_next;
delete pos._node;
prev_node->_next = next_node;
next_node->_prev = prev_node;
return next_node;
}
T& front() {
return _head->_next->_data;
}
const T& front()const {
return _head->_next->_data;
}
T& back() {
return _head->_prev->_data;
}
const T& back()const {
return _head->_prev->_data;
}
size_t size()const {
auto it = begin();
size_t n = 0;
while (it != end()) {
++n;
++it;
}
return n;
}
//删除所有数据节点
void clear() {
auto it = begin();
while (it != end()) {
it = erase(it);//erase的返回值就是指向下一个节点的
}
}
bool empty() {
return _head->_next == _head;
}
private:
Node* _head = nullptr;//哨兵位指针
};
}













