STL 之 List简析

2023-10-31


前言

记得我的一位老师说过,

好的结构一定会有好的结果。

C++的STL是数据结构与算法的集大成者,而其中的list就是典型代表。现在我手中有三种源码,分别是侯捷老师名著中用到的经典的SGI版本,VS下的版本,以及SGI的比较新的版本(实际上就是GCC编译器使用的新版本)。本人实力有限,只能解释一部分内容。

list介绍

list,也就是所谓的链表,是一个节点一个节点通过指连在一起的数据结构。而链式结构在生活中也很常见,比如项链,车链子。它的好处自然就是便于调整长短。比起vector对于调整size的硬伤,list完美的化解这一问题。

list的节点

list既然是通过指针链接在一起,它的每个节点自然应该封装指针。事实上,源码就是这样做的。

// SGI
struct _List_node_base { //我称之为节点的base类
  _List_node_base* _M_next; // 指向后一个节点 
  _List_node_base* _M_prev; // 指向前一个节点
};
template <class _Tp>
struct _List_node : public _List_node_base { // 真正的list节点类,继承节点的base类。
  _Tp _M_data; //数据域
};

SGI版本的节点设计,用一个节点base类封装前驱指针和后继指针,然后再用一个存储数据的类去继承节点的base类。后面你会看到,迭代器,list都是如此设计。而在新版本中SGI依旧使用这种继承的方式。而VS不太相同。

// VS
template <class _Value_type, class _Voidptr>
struct _List_node { // list node
    using value_type = _Value_type; //using的用法在这里等同typedef
    using _Nodeptr   = _Rebind_pointer_t<_Voidptr, _List_node>;
    _Nodeptr _Next; // successor node, or first element if head
    _Nodeptr _Prev; // predecessor node, or last element if head
    _Value_type _Myval; // the stored value, unused if head
    ...};

英文注释来自VS源码自带。VS下的源码使用了C++11的using的新功能,这里不拓展。VS下的节点,将两个指针和数据域封装在了一起,没有使用继承的方式。

list的迭代器

SGI迭代器

// SGI
struct _List_iterator_base { //迭代器的base类
	//...
 _List_node_base* _M_node;
 	//...
 };
 
 template<class _Tp, class _Ref, class _Ptr>
struct _List_iterator : public _List_iterator_base { //继承base
  typedef _List_iterator<_Tp,_Tp&,_Tp*>             iterator; 
  typedef _List_iterator<_Tp,const _Tp&,const _Tp*> const_iterator;
  typedef _List_iterator<_Tp,_Ref,_Ptr>             _Self;
  // 构造函数
_List_iterator(_Node* __x) : _List_iterator_base(__x) {}
// operator*重载 和 operator->重载
  reference operator*() const { return ((_Node*) _M_node)->_M_data; }
  pointer operator->() const { return &(operator*()); } 
  _Self& operator++() {  // 前置++,返回reference
    this->_M_incr();
    return *this;
  }
  _Self operator++(int) {  //后置++,返回value
    _Self __tmp = *this;
    this->_M_incr();
    return __tmp;
    // ... 后面还有很多源码
  }
};

SGI版本的迭代器依旧是使用继承,而_List_iterator_base这个类里封装了_List_node_base的一个指针。这里我们可以大致看出,设计者想让迭代器的移动作为一个部分,迭代器的data作为一部分。而真正的迭代器设计的非常巧妙,因为迭代器有普通和const的,怎么区分呢?SGI在list的迭代器中增加了两个模板参数,分别代表引用和指针,传过来的是普通的Ref和Ptr就是iterator;传过来的是const的Ref和const的Ptr就是 const_iterator。
迭代器

  • 1

迭代器应该足够聪明,可以完成一个指针应该有的操作。所以重载了operator*和operator->。当对一个迭代器取星号,应该是拿到节点的值。
而operator->有一点意思。当我们在使用operator->时,编译器会做出一点特殊处理,它会再加上一个->,想象这样一个类,

class Data{
private:
	int _year = 0;
}

list<Data> ld;
ld.push_back(Data());

auto it = ld.begin();
cout << it->_year << endl; // 实际上是 it->->_year
  • 2

而链表迭代器的++应该是到下一个节点,iterator应该足够聪明能做到这一点。
int类型的前置++可以加上两次,所以链表的迭代器返回reference。
int类型的后置++不能被加上两次,所以链表的迭代器返回value。

int i = 0;
++++i; // right
i++++; // wrong

list<int>::iterator it = l.begin();
++++it; //right
it++++; //wrong

根据effectiveC++中提到的,链表的迭代器的前置++和后置++应该与内置类型高度一致。

VS下的迭代器的不同之处

SGI版本的迭代器设计已经是巧妙无比,但也有些许的问题,就是太多模板参数不好理解。而VS版本将list迭代器的模板参数降低为一个。

// VS  
template <class _Mylist>
class _List_const_iterator { //const迭代器实现
 _NODISCARD reference operator*() const noexcept; //这些成员方法我只保留了声明
  _NODISCARD pointer operator->() const noexcept; //因为实现与SGI版本的大抵相同
    _List_const_iterator& operator++() noexcept;  //主要剖析实现的思想
   _List_const_iterator operator++(int) noexcept;
 // ...
 };
 template <class _Mylist>
class _List_iterator : public _List_const_iterator<_Mylist> { // 普通迭代器,继承了const迭代器
 using _Mybase           = _List_const_iterator<_Mylist>; // typedef一下,_Mybase就是const迭代器

 _NODISCARD reference operator*() const noexcept {
        return const_cast<reference>(_Mybase::operator*()); //复用const迭代器的operator*
    }														// + 转型去掉返回值的const
    _NODISCARD pointer operator->() const noexcept {
        return pointer_traits<pointer>::pointer_to(**this); //这个没有复用,但是我认为可以复用
    }														//也有可能是代码比较短,没有必要。
    _List_iterator& operator++() noexcept {
        _Mybase::operator++(); //复用const迭代器的前置++
        return *this;
    }
    _List_iterator operator++(int) noexcept {
        _List_iterator _Tmp = *this;
        _Mybase::operator++(); //复用const迭代器的后置++
        return _Tmp;
    }

我们可以看到VS下的迭代器也是很巧妙!vs版为list的iterator和const_iterator分别写了一个类,这样就只需要传一个模板参数,然后在类里面定义出reference和pointer,这样就搞定了迭代器。

但是,有一个很大的问题,iterator和const_iterator两个类的成员方法是几乎一样的,除了参数和返回值。这有大量重复,所以更巧秒的地方就是vs版用普通迭代器去继承了const迭代器的类,然后复用const类中所有的成员方法。这实际上是const的复用问题,用非const成员去调用const成员。实际上在最新版本的GCC源码中,list的迭代器也被分成了普通和const,但是没有复用。

list整体结构

STL中的list是一个双向循环的链表,实际上,为了区分begin和end,专门加入了一个所谓的哨兵节点,这个手法也在红黑树中使用。而SGI和VS都还专门区分了头节点和一般节点,我觉得实在没有必要。
list结构

list的成员方法

template <class _Tp, class _Alloc>
class _List_base 
{
public:
 // ...
protected:
  _List_node<_Tp>* _M_node;
};

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class list : protected _List_base<_Tp, _Alloc> { // 继承list的base类
public:
  typedef _List_iterator<_Tp,_Tp&,_Tp*>             iterator; //普通迭代器
  typedef _List_iterator<_Tp,const _Tp&,const _Tp*> const_iterator; //const迭代器
public:
  explicit list(const allocator_type& __a = allocator_type()) : _Base(__a) {}
  
  iterator begin()             { return (_Node*)(_M_node->_M_next); } //返回iterator 
  const_iterator begin() const { return (_Node*)(_M_node->_M_next); } //返回const_iterator

  iterator end()             { return _M_node; } //返回iterator
  const_iterator end() const { return _M_node; } //返回const_iterator
  iterator insert(iterator __position, const _Tp& __x); //insert,这里我只给出了声明
  iterator insert(iterator __position) { return insert(__position, _Tp()); } //insert的重载,调用上面一个insert
   void push_front(const _Tp& __x) { insert(begin(), __x); } //复用insert
  void push_back(const _Tp& __x) { insert(end(), __x); } //复用insert
  // ...
  };

以上就是SGI版本list的大概实现,可以看到其中也有大量复用,而在VS下的list让我难以下咽,能力不足,不做讲解。
(全文完)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STL 之 List简析 的相关文章

  • 是否有一个好的习惯用法来处理替代输出流?

    我想编写一个简单的程序 根据传递给它的选项 可执行文件将输出打印到屏幕或文件 程序很简单 include
  • 从 Dropbox C# 下载文件[重复]

    这个问题在这里已经有答案了 我正在尝试下载 Dropbox 中的 pdf 文件 我需要将其保存到本地计算机中 可以是任何文件夹C Users User Desktop例如 这是我一直在使用的代码 public void DownloadPd
  • 在 C++ 中,std::string::push_back() 的摊余复杂度是 O(1) 吗?

    我知道标准指定它适用于向量 但是字符串呢 是的 它是摊销常数时间 请参见第 716 页的表 101本文件的 http www open std org jtc1 sc22 wg21 docs papers 2012 n3485 pdf 表
  • Windows CE 6.0 和运行时链接到调试 DLL /MDd

    我在 x86 PC 上使用 Windows CE 6 0 R3 我已经为该平台构建了 NK bin 和 SDK 但我有一些问题需要了解如何使用 MTd 调试 DLL 构建控制台应用程序 如果我尝试构建这个 main c with MDd i
  • 如何在Qt中更快地读取数据?

    Qt读取数据库比C 慢吗 我想我错过了一些东西 为了比较阅读速度 我在 Qt 中编写了以下内容 QElapsedTimer t t start int count 0 QString cs Driver SQL Server Server
  • 有没有办法让我的程序用更少的代码运行?

    我为学校作业编写了以下代码 它编译并打印所有正确的消息 但出于我自己的好奇心 我想知道我的代码是否可以缩短并且仍然有效 我尝试了 signal 而不是 sigaction 但我听说 sigaction 比 signal 更受青睐 此外 此任
  • 使用 pthread_cond_signal 优雅地终止线程被证明是有问题的

    我需要发射一堆线程 并希望优雅地将它们拉下来 我正在尝试使用pthread cond signal pthread cond wait实现这一目标 但遇到了问题 这是我的代码 首先是thread main static void thrma
  • 通过 EUSART PIC18F45K80 打印消息

    我正在尝试向 Docklight 发送串行消息 但始终收到空值 我正在使用带有 XC8 MPLAB X 的 PIC18F45K80 我的代码中的所有内容似乎都是正确的 但我想我错了 我该如何修复它 include
  • 我可以将特定警告视为错误吗?

    以下是我有时在学生代码中看到的模式的简化版本 bool foobar int a int b if a lt b return true 当然 真正的代码要复杂得多 Visual Studio 报告警告 C4715 并非所有控制路径都会返回
  • memccpy 返回比 src 起始地址更低的内存地址

    我有一个学校项目 我必须重新编码memccpy 功能 我使用 2 个程序来检查我的代码是否正常工作 第一个是只有一个主程序的小程序 第二个程序是另一个学生开发的 可以找到here https github com yyang42 mouli
  • 在 C++ 中初始化指针

    可以在声明时将指针分配给值吗 像这样的东西 int p 1000 是的 您可以在声明时初始化指向值的指针 但是您不能这样做 int p 1000 是个地址运算符 并且您不能将其应用于常量 尽管如果可以 那会很有趣 尝试使用另一个变量 int
  • Docker 不遵循构建目录中的符号链接

    我正在对一个应用程序进行 Docker 化 其中涉及通过 Clang 将二进制文件与其他 C 文件链接 我们维护二进制文件的符号链接版本 因为它们在整个代码库中使用 我的 Docker 构建目录包含整个代码库 包括源文件以及这些源文件的符号
  • C++ 中类型信息何时向后流动?

    我刚刚看了 Stephan T Lavavej 的演讲CppCon 2018关于 类模板参数推导 在哪里某个点 https youtu be H ut6j1BYU t 941他顺便说 在 C 中 类型信息几乎永远不会向后流动 我不得不说 几
  • 不可能的事情发生了!这是什么意思?

    我遇到了一个有趣的运行时错误 我认为这是某种内存泄漏 我写了以下程序 C Code include
  • 为什么我从 c# 到 js 得到不同的 MD5 哈希值?

    我有一个用于加密密码的 C 函数 System Security Cryptography MD5CryptoServiceProvider md5Provider new System Security Cryptography MD5C
  • 如何明智地解释这个编译器警告?

    当我执行这段代码时question https stackoverflow com a 51056490 2411320 我收到这个警告 warning format d expects argument of type int but a
  • 使用 Node.js 访问用 C++ 编写的 SDK

    我有一个用 C 语言编写的 SDK 可以与我的扫描仪设备进行通信 我需要开发一个可以访问扫描仪设备的电子应用程序 我知道有很多库可用于扫描仪 但我想使用这个 SDK 因为它允许我访问设备的完整功能 而且它是由设备制造商提供的 那么 有没有什
  • 在标准 C 中将 int 转换为 string

    我是 C 新手 我正在寻找一个可以调用函数进行转换的示例int串起来 我发现itoa但这不是标准 C 的一部分 我还发现sprintf str d aInt 但问题是我不知道所需的 str 的大小 因此 我如何传递输出字符串的正确大小 有多
  • C++ 项目编译为静态库,编译为动态库失败(链接器错误)。为什么?

    我有一个 VS2008 本机 C 项目 我希望将其编译为 DLL 它仅引用一个外部库 log4cplus lib 并使用其功能 当然也使用 log4cplus 的 h 文件 当我尝试将我的项目编译为静态库时 它成功了 当我尝试作为 DLL
  • 如何正确地将十六进制转义添加到字符串文字中?

    当你有C语言的字符串时 你可以在里面直接添加十六进制代码 char str abcde a b c d e 0x00 char str2 abc x12 x34 a b c 0x12 0x34 0x00 这两个示例在内存中都有 6 个字节

随机推荐

  • 基于Vue + Antd 搭建自己的博客后台管理系统

    博客后台管理 博客前台的项目地址 github com WqhForGitHu 前言 博客后台管理是基于 Vue Antd 实现的 Antd 确实是非常适合中后台应用的开发 有非常多的组件可以使用 非常多的组件可以使用 技术栈 Vue an
  • 将秒数转化为日期、时、分、秒

    1 说明 笔者最近在开发过程中 需要进行时间上的处理的地方比较多 有时候没有处理好导致出现各种的错误 这里主要是讲一下 如何时将秒数的时间转化为 yyyy MM dd HH mm ss 的格式 例如 2016 12 04 16 40 23的
  • 全备份、增量备份与差量备份

    基本概念 全备份 做的一个完整备份 差量备份 以上一次的全备份为基本做的备份 增量备份 以上一次全备份或增量备份为基本做的备份 看了概念以后是不是还是一头雾水 呵呵 正常 不过没关系 下面会举例说明 如果版本库不是很大 直接做全备份就好了
  • python--打字练习的成绩判定

    题目 模拟打字练习程序 假设original为原始内容 user Inputs为用户输入的内容 要求 用户输入的内容长度不得大于原始内容长度 若对应位置字符一致 则认为正确 否则 判定输入错误 最后成绩为 正确的字符数量 原始字符串长度 按
  • Jsoup解析Html获取内容

    在做自己的博客时遇到问题 文章列表需要文章内容的第一段作为列表的内容展示 但是编辑采用的是富文本编辑器 内容为html格式 这是上网搜到Jsoup可以解析html 希望能帮到需要的小伙陪 p span style font size 6 3
  • msvcr110.dll丢失的解决方法哪种好,推荐这个4种解决方法

    Msvcr110 dll是Microsoft Visual Studio 2012的运行时组件之一 这个DLL文件包含一些用于Windows操作系统的C 函数库 当程序需要这些函数时 它们会被加载到内存中 以便程序可以使用它们 当计算机提示
  • 程序员挣够了钱,到中年失业真的很可怕吗?

    最近一刷知乎全部都是大龄程序员失业危机 真的有这么可怕吗 程序员35岁就真的到了瓶颈期 我不这么认为 挣够了钱 当然不可怕 问题是没挣够啊 按题主的算法是 大城市薪资1w以上 45岁失业 工作20年可以挣够钱 那我们现在来算一下 20年12
  • 《HarmonyOS物联网应用开发》课程上线

    讲师简介 51CTO的学员们 大家好 我是51CTO学院的新晋讲师许思维 目前就职于江苏润和软件股份有限公司 任高级软件工程师一职 同时也是企业内训讲师 我擅长的领域包括Linux系统编程 单片机编程 以及Android App和Andro
  • multiple definition of `qMain(int, char**)'错误该怎么处理!

    原因 在 pro文件中重复使用了一些文件
  • ECMAScript5,6,7从基本语法到高级函数

    尚硅谷ES5 6 7教程 01 尚硅谷 ECMAScript入门介绍 01 尚硅谷 ECMAScript入门介绍
  • 下载chromium源码执行 generate_location_tags.py错误returned non-zero exit status 1

    今天下载chromium 碰到这个错误 以前也下载过 都很顺利 Error Command python3 src testing generate location tags py out src testing location tag
  • SVD分解的并行实现

    在之前的文章中 我对SVD进行了大致的了解 它在互联网高端领域中有广泛的应用 至于它的一些详细应 用以后再进一步学习 现在主要的问题是如何有效地实现SVD分解 接下来我会先用两种方法来实现SVD分 解 即基于双边Jacobi旋转的SVD和基
  • Jieba库的安装

    一 jieba库安装 jieba库是第三方库 不是安装包自带 需要通过pip指令安装 gt pip install jieba 或者 pip3 install jieba 方法一 直接安装 不建议使用 亲测安装很多python库的时候大家获
  • Hierarchical attentive knowledge graph embedding for personalized recommendation

    Hierarchical attentive knowledge graph embedding for personalized recommendation 文章目录 1 背景 2 模型 2 1 子图构建 2 2 Hierarchica
  • 第一个Java项目———Java实现简单图书管理系统(GUI)

    暑假写了个图书管理系统 编译器用的是eclipse 加入了WindowBuilder插件做界面 做的特丑 数据库用的是MySQL 实现了图书的查询 借阅 归还 删除 增加 用户的删除 查询 分为管理员和用户 源码地址 GitHub GitH
  • java项目中常用的分页对象Page

    在使用JAVA平台开发企业级应用时 常常会遇到分页的场景 而且每一个项目都有自己的分页方法 现在给出我自己总结的比较通用的分页对象 以供有需之人参考 package cn cgs corejava model persistent impo
  • 删除 Android Studio 的代理设置,让项目通过阿里云 maven jcenter 下载依赖资源

    如果你之前设置过 Android Studio 的 HTTP Proxy 然后又取消了代理设置 那么很有可能 Andoid Studio gradle 再次编译时仍然会走代理设置 造成依赖资源一直下载失败 1 删除 Android Stud
  • 用C语言计算1~20的阶乘之和

    昨天 2018 12 7 在做C语言的课后练习题的时候 有一道题要求我们计算1 20的阶乘之和 代码很快就写出来了 考虑到结果的值会比较大 而在Windows操作系统下 int 类型和 long 类型居然都是4个字节 C 中long类型是八
  • linux 下 java 单个class文件执行和多个class文件打包调用,执行

    银行项目 给个linux机器权限控制的比较厉害 之前有需求需要切割个日志文件 所以开始就写一个java文件 然后编译成class 直接运行 还算方便 后来需求越来越多 需要查询数据 需要操作excel 还因为字段处理等需要引入更多的累 打包
  • STL 之 List简析

    文章目录 前言 list介绍 list的节点 list的迭代器 SGI迭代器 VS下的迭代器的不同之处 list整体结构 list的成员方法 前言 记得我的一位老师说过 好的结构一定会有好的结果 C 的STL是数据结构与算法的集大成者 而其