C++ STL容器(二) —— list 底层剖析

计划写几篇关于C++ STL容器底层剖析的文章,主要基于的是MSVC的实现,本篇先从比较简单的 list 入手,个人感觉文章更偏于代码的具体实现,而不是原理的讲解,所以前置需要你了解链表的相关算法,如果有问题欢迎评论区指出。


文章目录

  • 数据结构
  • UML 类图
  • 代码解析
    • 默认构造函数
    • 插入节点
    • 删除节点
    • 清空容器
    • 析构函数
  • 小结


数据结构

之前知道的C++ STL容器里的 list 是双向链表,看完 MSVC 里的实现,更准确地说其 list 是一个双向循环的链表,并且有一个哨兵节点。


UML 类图

MSVC 内部的实现还是比较复杂的,所以在深入代码实现之前,先梳理下 list 相关的 UML 类图,这里 list 封装了操作这个双向链表的方法,且可以看作是双向链表+分配器的组合,而实际的双向链表结构其实本体是 _List_val 其保存了链表的哨兵节点和链表的实际长度。而每个链表上的节点信息由 _List_node 类定义。

对于 list 中节点的插入也封装成了一个操作类 _List_node_emplace_op2


代码解析

由于相关的操作和代码很多,所以这里只挑一些我觉得比较常用的操作进行分析,管中窥豹一下了。

默认构造函数

先从默认构造函数入手,下面是源码:

    list() : _Mypair(_Zero_then_variadic_args_t{}) {_Alloc_sentinel_and_proxy();}

下面是 _MyPair 里的构造函数,首先匹配上的是 _Zero_then_variadic_args_t 为首参数的构造函数,其分别调用了 _Ty1 的无参构造函数,和 _Myval2 的有参构造函数,这里的 _Ty1 就是我们的分配器,我们不关注,_Myval2 就是保存了我们的双向链表 _List_val

    template <class... _Other2>constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2) noexcept(conjunction_v<is_nothrow_default_constructible<_Ty1>, is_nothrow_constructible<_Ty2, _Other2...>>): _Ty1(), _Myval2(_STD forward<_Other2>(_Val2)...) {}

而这里 _List_val 其实是没传参数的构造函数,具体代码如下,就是初始化链表长度为0,_Myhead 是哨兵节点指针,初始为NULL:

_List_val() noexcept : _Myhead(), _Mysize(0) {} // initialize data

回到 list(),后续调用 _Alloc_sentinel_and_proxy(),这里翻译就是分配哨兵和代理,本质上就是给哨兵节点分配内存,然后将其的 _Next_Prev 都指向自身,_Construct_in_place 具体实现是 ::new (static_cast<void*>(_STD addressof(_Obj))) _Ty(_STD forward<_Types>(_Args)...); 就是个 placement new 在已分配好的内存上构建对象,最后将 _Myhead 指向 _Newhead 我们就保存了哨兵节点的指针。这里还有一部分代码是关于容器代理的(和迭代器有关),博主还没有完全弄懂,等后续全部理解了,再出相关文章讲解,本篇只关注于 list 本身。

    void _Alloc_sentinel_and_proxy() {auto&& _Alproxy = _GET_PROXY_ALLOCATOR(_Alnode, _Getal());_Container_proxy_ptr<_Alty> _Proxy(_Alproxy, _Mypair._Myval2);auto& _Al     = _Getal();auto _Newhead = _Al.allocate(1);_Construct_in_place(_Newhead->_Next, _Newhead);_Construct_in_place(_Newhead->_Prev, _Newhead);_Mypair._Myval2._Myhead = _Newhead;_Proxy._Release();}

list<int> 为例看看 _Mypair 具体保存了什么。

插入节点

无论是 push_frontpush_backinsert 最后调用的都是 _Emplace 方法:

    void push_front(const _Ty& _Val) {_Emplace(_Mypair._Myval2._Myhead->_Next, _Val);}void push_back(const _Ty& _Val) {_Emplace(_Mypair._Myval2._Myhead, _Val);}iterator insert(const_iterator _Where, const _Ty& _Val) { // insert _Val at _Where
#if _ITERATOR_DEBUG_LEVEL == 2_STL_VERIFY(_Where._Getcont() == _STD addressof(_Mypair._Myval2), "list insert iterator outside range");
#endif // _ITERATOR_DEBUG_LEVEL == 2return _Make_iter(_Emplace(_Where._Ptr, _Val));}

那么我们深入看下 _Emplace 方法。

    template <class... _Valty>_Nodeptr _Emplace(const _Nodeptr _Where, _Valty&&... _Val) { // insert element at _Wheresize_type& _Mysize = _Mypair._Myval2._Mysize;if (_Mysize == max_size()) {_Xlength_error("list too long");}_List_node_emplace_op2<_Alnode> _Op{_Getal(), _STD forward<_Valty>(_Val)...};++_Mysize;return _Op._Transfer_before(_Where);}
  1. 首先是获取到链表的长度,看看是否超过了最大长度的限制,一般是 2 64 − 1 2^{64} - 1 2641
  2. 构造了一个操作类的对象。
  3. ++_Mysize 增加链表的长度。
  4. 调用 _Transfer_before 插入节点。

那么看下这个操作类的构造函数:

    template <class... _Valtys>explicit _List_node_emplace_op2(_Alnode& _Al_, _Valtys&&... _Vals) : _Alloc_construct_ptr<_Alnode>(_Al_) {this->_Allocate();_Alnode_traits::construct(this->_Al, _STD addressof(this->_Ptr->_Myval), _STD forward<_Valtys>(_Vals)...);}
  1. this->_Allocate() 就是用分配器分配一块_List_node大小的内存。
  2. 然后 construct 在这块内存上构造,就是把 _List_node._Myval 给设置好,但它的 _Prev_Next 都还未设置。

最后就是通过 _Op._Transfer_before(_Where) 设置好 _Prev_Next,本质就是一个简单的链表插入节点,在 _Insert_next 前插入节点,对于一开始无节点的链表(只有一个哨兵节点),就是插入到哨兵节点的前面。

    pointer _Transfer_before(const pointer _Insert_before) noexcept {const pointer _Insert_after = _Insert_before->_Prev;_Construct_in_place(this->_Ptr->_Next, _Insert_before);_Construct_in_place(this->_Ptr->_Prev, _Insert_after);const auto _Result    = this->_Ptr;this->_Ptr            = pointer{};_Insert_before->_Prev = _Result;_Insert_after->_Next  = _Result;return _Result;}

删除节点

对于 pop_frontpop_back 内部调用的是 _Unchecked_erase,这里哨兵节点的前驱节点是链表的最后一个节点,哨兵节点的后继节点是链表的第一个节点:

void pop_front() noexcept /* strengthened */ {
#if _CONTAINER_DEBUG_LEVEL > 0_STL_VERIFY(_Mypair._Myval2._Mysize != 0, "pop_front called on empty list");
#endif // _CONTAINER_DEBUG_LEVEL > 0_Unchecked_erase(_Mypair._Myval2._Myhead->_Next);}void pop_back() noexcept /* strengthened */ {
#if _CONTAINER_DEBUG_LEVEL > 0_STL_VERIFY(_Mypair._Myval2._Mysize != 0, "pop_back called on empty list");
#endif // _CONTAINER_DEBUG_LEVEL > 0_Unchecked_erase(_Mypair._Myval2._Myhead->_Prev);}

继续看下 _Unchecked_erase ,这里的 _Pnode 就是要删除的节点。

    _Nodeptr _Unchecked_erase(const _Nodeptr _Pnode) noexcept { // erase element at _Pnodeconst auto _Result = _Pnode->_Next;_Mypair._Myval2._Orphan_ptr2(_Pnode);--_Mypair._Myval2._Mysize;_Pnode->_Prev->_Next = _Result;_Result->_Prev       = _Pnode->_Prev;_Node::_Freenode(_Getal(), _Pnode);return _Result;}
  1. 首先获取到要删除节点的后继节点给 _Result
  2. _Orphan_ptr2 主要是在 Debug 下和迭代器有关的操作,后续出文章细说。
  3. 链表大小减 1。
  4. 经典的删除节点操作,让删除节点的前驱节点的后继指向 _Result,然后把 _Result 节点的前驱节点指向要删除节点的前驱节点
  5. _Node::_Freenode(_Getal(), _Pnode) 分配器回收内存

其中 _Node::_Freenode 具体代码如下:

    template <class _Alnode>static void _Freenode(_Alnode& _Al, _Nodeptr _Ptr) noexcept { // destroy all members in _Ptr and deallocate with _Alallocator_traits<_Alnode>::destroy(_Al, _STD addressof(_Ptr->_Myval));_Freenode0(_Al, _Ptr);}

destroy 就是调用 _List_node_Myval 的析构函数:

    template <class _Uty>static _CONSTEXPR20 void destroy(_Alloc&, _Uty* const _Ptr) {
#if _HAS_CXX20_STD destroy_at(_Ptr);
#else // ^^^ _HAS_CXX20 / !_HAS_CXX20 vvv_Ptr->~_Uty();
#endif // ^^^ !_HAS_CXX20 ^^^}

_Freenode0 一个是调用要删除节点 _Prev_Next 的析构函数(这里可能是封装成指针的类),然后分配器回收内存:

    template <class _Alnode>static void _Freenode0(_Alnode& _Al, _Nodeptr _Ptr) noexcept {// destroy pointer members in _Ptr and deallocate with _Alstatic_assert(is_same_v<typename _Alnode::value_type, _List_node>, "Bad _Freenode0 call");_Destroy_in_place(_Ptr->_Next);_Destroy_in_place(_Ptr->_Prev);allocator_traits<_Alnode>::deallocate(_Al, _Ptr, 1);}

对于另一个删除节点的方法 erase 本质也是调用了 _Freenode

    iterator erase(const const_iterator _Where) noexcept /* strengthened */ {
#if _ITERATOR_DEBUG_LEVEL == 2_STL_VERIFY(_Where._Getcont() == _STD addressof(_Mypair._Myval2), "list erase iterator outside range");
#endif // _ITERATOR_DEBUG_LEVEL == 2const auto _Result = _Where._Ptr->_Next;_Node::_Freenode(_Getal(), _Mypair._Myval2._Unlinknode(_Where._Ptr));return _Make_iter(_Result);}

_Freenode 前面已经分析过了,下面看下这里的 _Unlinknode,其实就是之前提到的删除节点的经典方式,不知道为啥前面的那部分代码不直接用 _Unlinknode 这个封装好的方法:

    _Nodeptr _Unlinknode(_Nodeptr _Pnode) noexcept { // unlink node at _Where from the list_Orphan_ptr2(_Pnode);_Pnode->_Prev->_Next = _Pnode->_Next;_Pnode->_Next->_Prev = _Pnode->_Prev;--_Mysize;return _Pnode;}

清空容器

下面再聊下也是常使用的一个操作 clear 清空容器:

    void clear() noexcept { // erase allauto& _My_data = _Mypair._Myval2;_My_data._Orphan_non_end();_Node::_Free_non_head(_Getal(), _My_data._Myhead);_My_data._Myhead->_Next = _My_data._Myhead;_My_data._Myhead->_Prev = _My_data._Myhead;_My_data._Mysize        = 0;}
  1. _Node::_Free_non_head 回收除了哨兵节点之外其他节点的内存。
  2. 将哨兵节点复原到初始状态,即它的 _Next_Prev 都指向自身。
  3. 链表长度归 0。

这里主要再看下 _Free_non_head 的内部实现:

    template <class _Alnode>static void _Free_non_head(_Alnode& _Al, _Nodeptr _Head) noexcept { // free a list starting at _First and terminated at nullptr_Head->_Prev->_Next = nullptr;auto _Pnode = _Head->_Next;for (_Nodeptr _Pnext; _Pnode; _Pnode = _Pnext) {_Pnext = _Pnode->_Next;_Freenode(_Al, _Pnode);}}
  1. 把哨兵节点的前驱节点的后继设为 nullptr。
  2. 找到哨兵节点的后继节点,也就是链表的第一个节点。
  3. 然后开始释放链表中的每一个节点。

析构函数

最后看下 list 的析构函数:

    ~list() noexcept {_Tidy();
#if _ITERATOR_DEBUG_LEVEL != 0 // TRANSITION, ABIauto&& _Alproxy = _GET_PROXY_ALLOCATOR(_Alnode, _Getal());_Delete_plain_internal(_Alproxy, _Mypair._Myval2._Myproxy);
#endif // _ITERATOR_DEBUG_LEVEL != 0}

_Tidy() 其实内部就和之前 clear 差不多,只不过这里还需要把哨兵节点也释放掉:

    void _Tidy() noexcept {auto& _Al      = _Getal();auto& _My_data = _Mypair._Myval2;_My_data._Orphan_all();_Node::_Free_non_head(_Al, _My_data._Myhead);_Node::_Freenode0(_Al, _My_data._Myhead);}

小结

本篇文章大致地讲解了下 list 的内部结构和一些基础操作的具体实现,但是代码框架还挺庞大的,尤其还会涉及到辅助 Debug 信息的代码。然后博主对迭代器失效的问题(MSVC的内部实现)还没有完全搞明白,所以后续想着还是把 MSVC 的迭代器的部分给搞清楚,更篇迭代器的文章,之后再继续更一些 vectorunordered_mappriority_queue 等MSVC的底层实现解析。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

长方形+ 下三角形的图形 css

<div class"transform">42.48%</div>//转化.transform {position: relative;width: 70px;height: 26px;background-color: #dcdfe6; /* 长方形的颜色 */display: flex;justify-content: center;align-items: center;font-family: PingFangTC-Medium;font…

Keil5 操作

目录 1.Debug&#xff08;软件模拟调试&#xff1a;&#xff09;&#xff1a; 2.代码提示设置&#xff1a; 3.添加. c与.h文件&#xff1a; 常用技巧 安装下载推荐&#xff1a;正点原子 1.Debug&#xff08;软件模拟调试&#xff1a;&#xff09;&#xff1a; 文章讲解 …

Selenium自动化安装教程

目录 提示&#xff1a; 一、安装Python运行环境 1. 找到官方网站 ​编辑 2. 找到下载页面 3. 双击安装包 ​编辑 4. 运行 hello world 二、安装 pycharm 1. 找到官方网站 ​编辑 2. 找到下载页面 3. 双击安装包 4. 运行 hello world 5. 字体设置 三、Python管理…

JavaWeb--小白笔记07:servlet对表单数据的简单处理

这里的servlet对表单数据的处理是指使用IDEA创建web工程&#xff0c;再创建html和class文件进行连接&#xff0c;实现html创建一个表单网页&#xff0c;我们对网页中的表单进行填充&#xff0c;可以通过class文件得到网页我们填充的内容进行打印到控制台。 一登录系统页面---h…

查找和排序(选择题)

查找 寻找最大/小项 n-1 排序 前三个的时间复杂度都是O(n^2),希尔排序是O(n^1.5). 在以上排序方法中&#xff0c;最坏情况下时间复杂度最小的是堆排序。 每经过一次元素的交换会产生新的逆序的是快速排序。

为什么越来越多的网工运维转行网络安全?_idc运维转网络安全工程师_系统运维转行网安

最近越来越多的网工运维小伙伴都在吐槽&#xff1a;干网工、运维多年&#xff0c;薪资还是5.6K&#xff0c;技术也遇瓶颈上不去&#xff0c;考虑转岗或者转行。其中大部分的网工运维小伙伴们纷纷瞄准了高薪高前景的网络安全工程师岗位 网络安全是怎样的岗位&#xff1f; 网络安…

2024重组胶原蛋白行业白皮书:从美业革新先锋到精准医疗动力源

从来源上看&#xff0c;胶原蛋白主要分为动物源胶原蛋白和重组胶原蛋白两大类。重组胶原蛋白相较于传统动物来源的胶原蛋白在生物活性、生物相容性、低免疫原性、降低漏检病原体风险、水溶性、无细胞毒性等方面表现出诸多优越性。随着胶原蛋白的来源和生产方式不断演变&#xf…

改进的yolov10 deepsort目标跟踪(yolo改进+最新算法+附代码和教程)

YOLOv10_DeepSORT&#xff1a;视频中的对象检测与跟踪 本仓库包含了使用YOLOv10对象检测模型和DeepSORT算法在视频中进行对象检测与跟踪的代码。YOLOv10是目前最先进的对象检测模型之一&#xff0c;而DeepSORT是一种基于深度学习的对象跟踪算法&#xff0c;它结合了外观信息和…

BOE(京东方)携故宫博物院举办2024“照亮成长路”公益项目落地仪式以创新科技赋能教育可持续发展

2024年9月20日&#xff0c;BOE&#xff08;京东方&#xff09;“照亮成长路”智慧教室落成暨百堂故宫传统文化公益课山西活动落地仪式在山西省太原市娄烦县实验小学隆重举行。自“照亮成长路”教育公益项目正式设立以来&#xff0c;BOE&#xff08;京东方&#xff09;持续以创新…

jenkins分布式构建

Jenkins分布式构建是一种将构建任务分散到多个机器上的方法&#xff0c;以提高构建效率和并行处理能力 1. 架构 主节点&#xff08;Master&#xff09;&#xff1a;负责管理构建任务、调度和监控所有从节点。从节点&#xff08;Slave&#xff09;&#xff1a;实际执行构建任务…

文件防泄漏方法有哪些|6个方法有效防止文件泄密

文件防泄漏是企业和组织保护其敏感信息和核心资产的重要手段。 以下是六个有效防止文件泄密的方法&#xff1a; 1. 文件加密 透明加密&#xff1a;使用专业的防泄密软件&#xff0c;如安企神等&#xff0c;对敏感文件进行透明加密处理。 这种加密方式在用户创建、编辑和保存…

DPDK 简易应用开发之路 4:基于Pipeline模型的DNS服务器

本机环境为 Ubuntu20.04 &#xff0c;dpdk-stable-20.11.10 使用scapy和wireshark发包抓包分析结果 完整代码见&#xff1a;github Pipeline模型 DPDK Pipeline模型是基于Data Plane Development Kit&#xff08;DPDK&#xff09;的高性能数据包处理框架。它通过将数据流分为多…

力扣46.全排列

一、题目 二、代码 class Solution {int[] nums;List<List<Integer>> ans new ArrayList<>();List<Integer> path new ArrayList<>();boolean[] onPath;public List<List<Integer>> permute(int[] nums) {this.nums nums;int n …

【GUI设计】基于图像分割的GUI系统(3),matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于Matlab的图像处理GUI系统&#xff08;3&#xff09;&#xff0c;用matlab实现。…

AH2212-12V转4.2V充电芯片

AH2212——12V转4.2V充电芯片&#xff0c;峰值2A输出编程电流&#xff0c;实现精准同步开关降压锂电池充电 随着科技的不断发展&#xff0c;移动电源、智能穿戴、电动工具等设备的应用越来越广泛&#xff0c;对电池充电芯片的需求也日益增大。本文将为您介绍一款高性能的充电芯…

与时间函数相关的那些事

在LuatOS中&#xff0c;获取时间函数用得最多的就是os.time()函数了。 接下来&#xff0c;我会讲一些与这个函数以及其他时间函数相关的知识。 一、时间戳相关 os.time()这个函数&#xff0c;只能获取当前时间戳&#xff1b;如果客户希望获取的是当前时间&#xff0c;即相应…

2024年【危险化学品生产单位安全生产管理人员】考试及危险化学品生产单位安全生产管理人员考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年危险化学品生产单位安全生产管理人员考试为正在备考危险化学品生产单位安全生产管理人员操作证的学员准备的理论考试专题&#xff0c;每个月更新的危险化学品生产单位安全生产管理人员考试题祝您顺利通过危险化…

开源实时多模态AI聊天机器人Moshi,语音对话延迟低至200毫秒!

开源实时多模态AI聊天机器人Moshi&#xff0c;语音对话延迟低至200毫秒&#xff01; 最近AI圈真是热闹非凡&#xff0c;继Meta发布Llama 3之后&#xff0c;各种开源大模型也是层出不穷。这不&#xff0c;法国一个非盈利AI研究实验室Kyutai&#xff0c;又搞了个大新闻&#xff0…

教你如何调用微信公众号模板消息发送接口

文章目录 前言准备工作代码实现获取accessToken调用模板消息发送接口前言 本文带你理解微信公众号模板消息发送接口的调用,面向的场景是你需要对你的公众号或者小程序用户发送公众号通知消息,没错,就算是小程序也是通过关联公众号,并且用户使用小程序时跳到公众号关注页关注…

C++ 进阶之路:非类型模板参数、模板特化与分离编译详解

目录 非类型模版参数 类型模板参数 非类型模板参数 非类型模板参数的使用 模板的特化 函数模板的特化 类模板的特化 全特化与偏特化 偏特化的其它情况 模板的分离编译 什么是分离编译 为什么要分离编译 为什么模板不能分离编译 普通的类和函数都是可以分离编译的…