【C++杂货铺】探索list的底层实现

在这里插入图片描述

文章目录

  • 一、list的介绍及使用
    • 1.1 list的介绍
    • 1.2 list的使用
      • 1.2.1 list的构造
      • 1.2.2 list iterator的使用
      • 1.2.3 list capacity(容量相关)
      • 1.2.4 list element access(元素访问)
      • 1.2.5 list modifiers(链表修改)
      • 1.2.6 list operation(对链表的一些操作)
  • 二、list的模拟实现
    • 2.1 list的节点
    • 2.2 list的成员变量
    • 2.3 list的迭代器
      • 2.3.1 普通迭代器
      • 2.3.2 const 迭代器
    • 2.4 list的成员函数
      • 2.4.1 构造函数
      • 2.4.2 拷贝构造函数
      • 2.4.3 赋值运算符重载
      • 2.4.4 push_back
      • 2.4.5 迭代器相关
      • 2.4.6 insert
      • 2.4.7 erase
      • 2.4.8 push_front
      • 2.4.9 pop_back
      • 2.4.10 pop_front
      • 2.4.11 size
      • 2.4.12 clear
      • 2.4.13 析构函数
  • 三、结语

一、list的介绍及使用

1.1 list的介绍

  • list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

  • list 的底层是双向链表结构,双向链表中的每个元素存储在互不相关的独立节点中,在节点中通过指针指向的前一个元素和后一个元素。

  • list 和 forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,已让其更简单高效。

  • 与其它的序列式容器相比(arry、vector、deque),list 通常在任意位置进行插入,移除元素的执行效率更好。

  • 与其它序列式容器相比,list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list 的第 5 个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,已保存每个节点的相关联信息。

1.2 list的使用

list 学习时一定要学会查看文档:list的文档介绍,list 在实际中非常重要,在实际中我们熟悉常用的接口就可以,下面列出了需要我们重点掌握的接口。

1.2.1 list的构造

构造函数接口说明
list()list 的默认构造,构造空的 list
list(size_type n, const value_type& val = value_type())构造的 list 中包含 n 个值为 val 的元素
list(const list& x)拷贝构造函数
list(InputIterator first, InputIterator last)用[first,last)区间中的元素构造 list

小Tips:size_type 表示一个无符号整数类型,value_type 是 list 的第一个模板参数,也就是要存储的数据类型。使用迭代器区间的构造函数是函数模板,只要是满足 Input 类型的迭代器都可以使用该构造函数。

void TestList1()
{list<int> l1;                         // 构造空的l1list<int> l2(4, 100);                 // l2中放4个值为100的元素list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3list<int> l4(l3);                    // 用l3拷贝构造l4// 以数组为迭代器区间构造l5int array[] = { 16,2,77,29 };list<int> l5(array, array + sizeof(array) / sizeof(int));// 列表格式初始化C++11list<int> l6{ 1,2,3,4,5 };// 用迭代器方式打印l5中的元素list<int>::iterator it = l5.begin();while (it != l5.end()){cout << *it << " ";++it;}cout << endl;// C++11范围for的方式遍历for (auto& e : l5)cout << e << " ";cout << endl;
}

1.2.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个像指针一样的东西,该指针指向 list 中的某个节点。

函数声明接口说明
begin() + end()返回第一个元素的迭代器 + 返回最后一个元素下一个位置的迭代器
rebegin() + ren()返回第一个元素的 reverse_iterator,即 end 位置,返回最后一个一个元素下一个位置的 reverse_iterator,即 begin 位置

注意:begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动。rbegin 与 rend 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动。由于 list 的底层物理空间并不连续,所以 list 的迭代器不再是原生指针,并且 list 的迭代器没有对 + 和 - 进行重载,只重载了 ++ 和 – ,因为空间不连续,重载 + 会比较复杂。即 l.begin() + 5 是不被允许的。

void PrintList(const list<int>& l)
{// 注意这里调用的是list的 begin() const,返回list的const_iterator对象for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it){cout << *it << " ";// *it = 10; 编译不通过}cout << endl;
}void TestList2()
{int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array + sizeof(array) / sizeof(array[0]));// 使用正向迭代器正向list中的元素// list<int>::iterator it = l.begin();   // C++98中语法auto it = l.begin();                     // C++11之后推荐写法while (it != l.end()){cout << *it << " ";++it;}cout << endl;// 使用反向迭代器逆向打印list中的元素// list<int>::reverse_iterator rit = l.rbegin();auto rit = l.rbegin();while (rit != l.rend()){cout << *rit << " ";++rit;}cout << endl;
}

注意:遍历链表只能使用迭代器和范围 for。

1.2.3 list capacity(容量相关)

函数声明接口说明
empty检测 list 是否为空,是返回 true,否则返回 false
size返回 list 中有效节点个数

1.2.4 list element access(元素访问)

函数声明接口说明
front返回 list 的第一个节点中值的引用
back返回 list 的最后一个节点中值的引用

1.2.5 list modifiers(链表修改)

函数声明接口说明
push_front在 list 的第一个节点前插入值为 val 的节点
pop_front删除 list 中第一个节点
push_back在 list 尾部插入一个值为 val 的节点
pop_back删除 list 中最后一个节点
insert在 list 的 position 位置中插入一个值为 val 的节点
erase删除 list position 位置的节点
swap交换两个 list 的节点
clear清空 list 中的有效元素

小Tips:insert 插入元素并不会导致迭代器失效,例如:相较于 vector 中的 insert,list 中的 insert 并不会去扩容挪动数据,而 vector 中的 insert 可能会进行扩容挪动数据,最终导致迭代器失效。list 中的删除元素接口会导致迭代器失效,失效的只有指向被删除节点的迭代器,其他迭代器不会受到影响。

1.2.6 list operation(对链表的一些操作)

函数声明接口说明
reverse对链表进行逆置
sort对链表中的元素进行排序(稳定排序)
merge对两个有序的链表进行归并,得到一个有序的链表
unique对链表中的元素去重
remove删除具有特定值的节点
splice将 A 链表中的节点转移到 B 链表

小Tips:链表逆置可以使用 list 自身的接口,也可以使用算法库中的 reverse,二者没有什么区别。链表排序只能使用 list 自身的 sort() 接口(底层是利用归并排序),不能使用算法库的 sort,因为算法库中的 sort 底层是通过快排来实现的,而快排中会涉及到三数取中需要迭代器 - 迭代器,链表不能很好的支持。虽然链表提供了排序接口,但是用链表对数据排序意义不大,效率太低了,更希望用 vector 来对数据进行排序。

void TestSort()
{srand(time(0));const int N = 5000000;vector<int> v;list<int> l;v.reserve(N);//提前开好空间for (int i = 0; i < N; i++){auto e = rand();v.push_back(e);l.push_back(e);}//开始比较vector 和 list 的排序int begin1 = clock();sort(v.begin(), v.end());int end1 = clock();int begin2 = clock();l.sort();int end2 = clock();printf("vector sort:%d\n", end1 - begin1);printf("list sort:%d\n", end2 - begin2);
}

在这里插入图片描述

扩展:可以从功能角度对迭代器分为以下 3 类:

迭代器类型功能
单向(InputIterator)支持 ++
双向(BidirectionalItreator)支持 ++/- -
随机(RandomAccessIterator)支持 ++ / - - / + / -

其中 forward_listunordered_xxx 都是单向迭代器;listmapset 都是双向迭代器;vectorstringdeque 都是随机迭代器。对迭代器的这种分类方式,是由容器的底层结构来决定的。

二、list的模拟实现

2.1 list的节点

template<class T>
struct ListNode
{ListNode<T>* _next;ListNode<T>* _prev;T _val;ListNode(const T& val = T()){_next = nullptr;_prev = nullptr;_val = val;}
};

2.2 list的成员变量

class list
{typedef ListNode<T> Node;
public://一些成员函数
private:Node* _head;
}

小Tips:typedef 会受到访问限定符的限制,这里没写默认是 private,意味着 Node 这个类型只能在 list 这个类里面使用。链表本质上是一种数据结构,我们只需要维护好一个链表的头节点即可,所以 list 的成员变量就只有一个头节点的指针。

2.3 list的迭代器

list 的迭代器不能再使用原生指针,如果 list 的迭代器使用原生指针的话,那对迭代器解引用得到的是一个节点,而我们希望对迭代器解引用可以得到节点里面存储的元素,并且 list 在底层的物理空间并不连续,如果使用原生指针作为 list 的迭代器,那对迭代器执行 ++ 操作,并不会让迭代器指向下一个节点。因此我们需要对 list 的迭代器进行封装,然后将一些运算符进行重载,以实现迭代器本该有的效果。

2.3.1 普通迭代器

template<class T>
struct _list_iterator
{typedef ListNode<T> Node;Node* _node;_list_iterator(Node* val){_node = val;}T& operator* (){return _node->_val;}T* operator-> ()//迭代器通过->应该指向节点中的元素,因此返回的是一个T类型的地址{return &(_node->_val);}bool operator!= (const _list_iterator<T>& right){return _node != right._node;}_list_iterator<T> operator++(){_node = _node->_next;return *this;}_list_iterator<T> operator++(int){_list_iterator<T> tmp(this->_node);_node = _node->_next;return tmp;}
};

小Tips:这里的类名不能直接叫 iterator,因为每种容器的迭代器底层实现可能都有所不同,即可能会为每一种容器都单独实现一个迭代器类,如果都直接使用 iterator,会导致命名冲突。其次,迭代器类不需要我们自己写析构函数、拷贝构造函数、赋值运算符重载函数,直接使用默认生成的就可以,言外之意就是这里使用浅拷贝即可,因为迭代器只是一种工具,它不需要对资源进行释放清理,资源释放清理工作是在容器类中实现的,浅拷贝的问题就出在会对同一块空间释放两次,而迭代器无需对空间进行释放,所以浅拷贝是满足我们需求的。

2.3.2 const 迭代器

上面我们实现了普通迭代器,那 const 迭代器该如何实现呢?直接在容器类里面写上一句 typedef const _list_iterator<T> const_iterator 可以嘛?答案是不可以,const 迭代器本质是限制迭代器指向的内容不能修改,而 const 迭代器自身可以修改,它可以指向其他节点。前面这种写法,const 限制的就是迭代器本身,会让迭代器无法实现 ++ 等操作。那如何控制迭代指向的内容不能修改呢?可以通过控制 operator* 的返回值来实现。但是仅仅只有返回值类型不同,是无法构成函数重载的。那要怎样才能在一个类里面实现两个 operator* 让他俩一个返回普通的 T&,一个返回 const T& 呢?一般人可能想着那就再单独写一个 _list_const_iterator 的类,这样也行,就是会比较冗余,我们可以通过在普通迭代器的基础上,再传递一个模板参数,让编译器来帮们生成呀。除此之外, operator->也需要实现 const 版本,因此还需要第三个模板参数。

template<class T,class Ref, class Ptr>
struct _list_iterator
{typedef ListNode<T> Node;typedef _list_iterator<T, Ref, Ptr> self;Node* _node;_list_iterator(Node* val){_node = val;}Ref operator* (){return _node->_val;}Ptr operator-> (){return &(_node->_val);}bool operator!= (const self& right) const{return _node != right._node;}bool operator== (const self& right) const{return _node == right._node;}self operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(this->_node);_node = _node->_next;return tmp;}self operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}
};
//operator->的使用场景
struct A
{A(int a = 0, int b = 0){_a = a;_b = b;}int _a;int _b;
};void Textlist3()
{wcy::list<A> l;l.push_back(A(1, 2));l.push_back(A(3, 4));l.push_back(A(5, 6));l.push_back(A(7, 8));wcy::list<A>::iterator it = l.begin();while (it != l.end()){cout << it->_a << ',' << it->_b << " ";cout << endl;it++;}
}

小Tips:上面代码中的 it->_a 会去调用 operator->,返回一个 A 类型的指针,所以这里应该是两个 ->,即 it->->_a ,但是编译器进行了优化,只需要一个 -> 即可。

2.4 list的成员函数

2.4.1 构造函数

list()
{_head = new Node;_head->_prev = _head;_head->_next = _next;
}

小Tips:list 本质上是一个带头双向循环链表。

2.4.2 拷贝构造函数

list(const list& ll)
//list(const list<T>& ll)
{_head = new Node;_head->_prev = _head;_head->_next = _head;for (auto& e : ll){push_back(e);}
}

2.4.3 赋值运算符重载

void swap(list<T> l2)
{std::swap(_head, l2._head);
}list& operator=(const list ll)
//list<T>& operator=(const list<T> ll)
{//现代写法swap(ll);return *this;
}

小Tips:构造函数和赋值运算符重载函数的形参和返回值类型可以只写类名 list,无需写完整的类型 list<T>,但是不推荐这样写,容易造成混淆,其次现代写法和常规写法在效率上没有任何区别,只是将本来需要我们做的事情交给了编译器去做。

2.4.4 push_back

void push_back(const T& val)
{//先找尾Node* tail = _head;while (tail->_next != _head){tail = tail->_next;}//插入元素Node* newnode = new Node(val);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;
}

2.4.5 迭代器相关

iterator begin()
{return _head->_next;//单参数的构造函数支持隐式类型转换
}iterator end()
{return _head;
}const_iterator begin() const
{return _head->_next;//单参数的构造函数支持隐式类型转换
}const_iterator end() const
{return _head;
}

2.4.6 insert

iterator insert(iterator pos, const T& val)
{//找到 pos 位置的前一个位置Node* cur = pos._node;Node* prev = cur->_prev;//插入元素Node* newnode = new Node(val);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return newnode;
}

2.4.7 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;cur = nullptr;return next;
}

2.4.8 push_front

void push_front(const T& val)
{insert(begin(), val);
}

2.4.9 pop_back

void pop_back()
{erase(--end());
}

2.4.10 pop_front

void pop_front()
{erase(begin());
}

2.4.11 size

size_t size()
{size_t sz = 0;iterator it = begin();while (it != end()){it++;sz++;}return sz;
}

2.4.12 clear

void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}

2.4.13 析构函数

~list()
{clear();delete _head;_head = nullptr;
}

小Tips:clear 和 析构函数的主要区别在于是否释放头节点。

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/122676.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站监控系统最佳实践之静态资源采样上报

作者 观测云 产品服务部门 深圳团队 朱端畅 背景说明 通过 RUM 采集前端数据时&#xff0c;若采集的数据过多&#xff0c;可能会导致占用过多的网络带宽以及其他资源。特别是刚进入首页加载数据时&#xff0c;可能会调用几十次甚至更多次 v1/write/rum?precisionms数据采集接…

swagger---接口文档管理生成管理工具

Swagger–接口生成工具 使用Swagger你只需要按照它的规范去定义接口及接口相关的信息&#xff0c;再通过Swagger衍生出来的一系列项目和工具&#xff0c; 就可以做到生成各种格式的接口文档&#xff0c;以及在线接口调试页面等等。 官网: https://lswagger.io/knife4j是为Jav…

数学建模:Logistic回归预测

&#x1f506; 文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 数学建模&#xff1a;Logistic回归预测 Logistic回归预测 logistic方程的定义&#xff1a; x t 1 c a e b t x_{t}\frac{1}{cae^{bt}}\quad xt​caebt1​ d x d t − a b e b t ( c a e b t ) 2 >…

Java 内部类

目录 一、什么是内部类及为何要有内部类 二、四种内部类 1.成员内部类 成员内部类定义&#xff1a; 获取成员内部类对象的方法&#xff1a; 成员内部类获取外部类变量: 额外&#xff1a; 2.局部内部类 局部内部类定义: 如何实现内部类当中的方法&#xff1a; 3.静态内…

Java复习-20-接口(3)- 代理设计模式

代理设计模式(Proxy) 功能&#xff1a;可以帮助用户将所有的开发注意力只集中在核心业务功能的处理上。 代理模式(Proxy Pattern)是一种结构性模式。代理模式为一个对象提供了一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标目标对象&#xff0c;可以在目…

自动化测试系列 —— UI自动化测试

UI 测试是一种测试类型&#xff0c;也称为用户界面测试&#xff0c;通过该测试&#xff0c;我们检查应用程序的界面是否工作正常或是否存在任何妨碍用户行为且不符合书面规格的 BUG。了解用户将如何在用户和网站之间进行交互以执行 UI 测试至关重要&#xff0c;通过执行 UI 测试…

AR产业变革中的“关键先生”和“关键力量”

今年6月的WWDC大会上&#xff0c;苹果发布了头显产品Vision Pro&#xff0c;苹果CEO库克形容它&#xff1a; 开启了空间计算时代。 AR产业曾红极一时&#xff0c;但因为一些技术硬伤又减弱了声量&#xff0c;整个产业在起伏中前行。必须承认&#xff0c;这次苹果发布Vision P…

zabbix监控网络设备和zabbix proxy

监控linux主机 [rootrocky8 conf]# yum -y install net-snmp vim /etc/snmp/snmpd.conf com2sec notConfigUser default 123456##修改此行,设置团体密码,默认为public,此处 改为123456 view systemview included .1. ##添加此行,自定义授权,否则 zabbix 无法获取数据 [rootr…

什么合同管理系统?4类合同管理软件评测

说到合同管理系统&#xff0c;前提还是弄清楚合同有哪些类型&#xff0c;合同管理有那些痛点&#xff0c;才好对症下药。 一、合同的类型和合同管理的痛点 从法律角度来说&#xff0c;合同可以分为&#xff1a;有名合同与无名合同、单务合同与双务合同、有偿合同与无偿合同、…

conda环境安装opencv带cuda版本

主要是cmake编译选项需要修改 以下两个选项按照自己情况修改 -D OPENCV_EXTRA_MODULES_PATH../opencv_contrib/modules \ -D CUDA_TOOLKIT_ROOT_DIR/usr/local/cuda-12.2 \ 其中/home/lixin/anaconda3/envs/stereo 改成你自己的conda环境 cmake -D CMAKE_BUILD_TYPER…

MySQL阻塞与死锁

MySQL阻塞与死锁 阻塞 因为不同锁之间的兼容性关系&#xff0c;在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源&#xff0c;这就是阻塞。 # 查看等待时间 show variables like innodb_lock_wait_timeout; SETinnodb_lock_wait_timeout60; # 是否在等待…

Java版企业电子招标采购系统源码Spring Cloud + Spring Boot +二次开发+ MybatisPlus + Redis

功能描述 1、门户管理&#xff1a;所有用户可在门户页面查看所有的公告信息及相关的通知信息。主要板块包含&#xff1a;招标公告、非招标公告、系统通知、政策法规。 2、立项管理&#xff1a;企业用户可对需要采购的项目进行立项申请&#xff0c;并提交审批&#xff0c;查看…

两个类文件,实现根据返回的id显示对应的人员信息增强返回

后台与前台交互时&#xff0c;由于后台存放的原始信息可能就是一些id或code&#xff0c;要经过相应的转换才能很好地在前台展示。 比如&#xff1a; select id, user_id from user 直接返回给前台时&#xff0c;前台可能要根据你这个user_id作为参数&#xff0c;再请求一次后…

【方法】如何取消7Z压缩文件加密的密码?

给7Z格式的压缩文件设置了密码&#xff0c;后续又不需要了&#xff0c;如何取消呢&#xff1f;如果忘记密码&#xff0c;还能取消吗&#xff1f;下面小编来分享一下方法。 首先&#xff0c;我们需要使用支持7Z格式的解压缩软件&#xff0c;如果使用的是WinRAR或者7-Zip解压缩软…

Docker从认识到实践再到底层原理(二-2)|Namespace+cgroups

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

OceanBase社区版4.x核心技术解密

数字化时代&#xff0c;各行各业的数据量呈现爆发式增长&#xff0c;对于海量数据价值的挖掘和应用&#xff0c;正成为推动创新的主要力量&#xff0c;与此同时&#xff0c;数据计算复杂度正在提升。在此背景下&#xff0c;对于数据处理的基石数据库而言&#xff0c;正面临市场…

数学建模--非多项式拟合法的Python实现

目录 1.算法异同区别 2.算法核心步骤 3.算法核心代码 4.算法效果展示 1.算法异同区别 #*************************************************************************************************************# 方法区别探究 1.对于多项式拟合你需要大致知道这些点的分布&#xf…

jsvmp逆向(补环境篇)

书接上回 上篇文章写到tx的jsxmp的算法逆向&#xff0c;文章链接在这里。初试jsvmp加密 。但是可能有伙伴觉得不够详细。 这里放一个大佬的文章链接。 https://www.52pojie.cn/thread-1521480-1-1.html 。其实就是一个变形的xtea加密。 大佬的文章已经讲了很清楚了&#…

Spring MVC入门必读:注解、参数传递、返回值和页面跳转

一、常用注解 1.1.RequestMapping 作用&#xff1a;用于映射请求路径与处理请求的方法之间的关系&#xff0c;可以用在类或方法上 。 标注在方法上 用于方法上&#xff0c;表示在类的父路径下追加方法上注解中的地址将会访问到该方法 Controller public class HelloContro…

Redis集群服务器

集群简介 试想有一家餐厅&#xff0c;如果顾客人数较少&#xff0c;那么餐厅只需要一个服务员即可&#xff0c;如图1。但是&#xff0c;当顾客人数非常多时&#xff0c;一个服务员是绝对不够的&#xff0c;如图2。此时&#xff0c;餐厅需要雇用更多的服务员来解决大量访问&…