【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比

简介: 【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比


1. 前言

本篇文章立足于上一篇文章:

list深度剖析(上)

请先阅读完上一篇文章后再阅读这篇文章!

本章重点:

本章着重讲解list的模拟实现

list模拟实现的重难点是迭代器的实现

和const迭代器的实现

最后总结list和vector的区间对比

注:我将在文章末尾分享list模式实现全部代码


2. list类的大致框架与结构

由于list类是一个带头双向循环链表

所以在写list类之前,应该先定义节点类

在真正的list类中会复用此类:

template<class T>
class list_node
{
public:
  T _data;
  list_node<T>* _next;
  list_node<T>* _prev;
  list_node(const T& x = T())
    :_data(x)
    , _next(nullptr)
    , _prev(nullptr)
  {}
};

这个类和用C语言实现链表的定义很像
节点中存储一个T模板类型的值和
上一个节点的地址和下一个节点的地址

在List类中,由于链表都是些链接关系
所以List类中的成员变量只需要定义一个
那就是头节点head,知道head的链接关系
就知道list类对象中存放的内容!

List类基本框架以及无参构造:

template<class T>
class List
{
  typedef list_node<T> node;
public:
  List()
  {
    _head = new node;
    _head->_next=_head;
    _head->_prev=_head;
  }
private:
  node* _head;
}

给头节点head开辟一份空间后

头节点的指向最开始都是指向自身:


3. List类的构造,析构,拷贝构造

无参构造函数以及写过了,我们模仿

STL库中的带参构造写一个用迭代器

区间初始化的构造函数:

void emptyinit()//创建并初始化哨兵位的头节点,方便给构造和拷贝构造复用
  {
    _head = new node;
    _head->_next = _head;
    _head->_prev = _head;
  }
template<class InputTterator>//有参的构造
List(InputTterator first, InputTterator last)
{
  emptyinit();
  while (first != last)
  {
    push_back(*first);
    first++;
  }
}

注:push_back先用着,后面会实现

在实现析构函数前,我们可以先实现clear函数
因为析构函数实际上可以复用clear函数:

void clear()//清空数据,除了头节点
{
  iterator it = begin();
  while (it != end())
  {
    it = erase(it);//erase会返回下一个节点的迭代器
  }
}
~List()//_head也要被删除掉
{
  clear();
  delete _head;
  _head = nullptr;
}

注:迭代器相关的函数先用着,后面会实现

拷贝构造函数的实现:(用lt1拷贝lt2)

//lt2(lt1)
List(const List<T>& lt)//完成深拷贝
{
  emptyinit();
  List<T> tmp(lt.begin(), lt.end());//用lt1的迭代器区间去构造一下tmp
  ::swap(_head, tmp._head);
}

此拷贝构造函数精妙的地方有两个:

  1. 它先定义一个临时变量tmp来接受
    lt1的所有值,然后再将临时变量tmp
    的head和lt2的head交换,这样一来
    lt2中的链接关系就和lt1相同了
    并且tmp变量是构造函数初始化的
    它是深拷贝,所以lt2对于lt1也是深拷贝
  2. tmp是临时变量,除了作用域会销毁
    也就是除了此拷贝构造函数后会销毁
    销毁时会调用析构函数,然而lt2的head
    以及和tmp的head交换了,所以tmp销毁
    时实际上是在帮原先的lt2销毁内存!

4. list的迭代器的实现

由于list的迭代器底层不是简单的指针

所以我们不能像实现vector一样直接在

list类定义迭代器和使用迭代器相关函数

要重新写一个迭代器类来实现功能:

template<class T>
struct __list_iterator
{
  typedef list_node<T> node;
  typedef __list_iterator iterator;
  node* _node;
}

这样重新写一个类的作用是将迭代器的
++,- -等操作在此类中实现,因为list中的
++实际上是node=node->next,然而list
中的==符号判断的是node1->data和
node2->data相不相同,如果迭代器和
list写在一个类中,实现此等操作会很麻烦


4.1 list迭代器的若干函数解析

1. 构造函数:

__list_iterator(node* nnode)
  :_node(nnode)
  {}

由于初始化列表会调用node的构造函数

所以list迭代器的构造函数可写可不写

2. 前置++/- -和后置++/- -

iterator& operator++()//前置++
{
  _node = _node->_next;
  return *this;
}
iterator& operator++(int)//后置++
{
  iterator tmp = *this;
  _node = _node->_next;
  return tmp;
}
iterator& operator--()//前置--
{
  _node = _node->_prev;
  return *this;
}
iterator& operator--(int)//后置--
{
  iterator tmp = *this;
  _node = _node->_prev;
  return tmp;
}

3. 等于和不等于函数解析:

bool operator!=(const iterator& it)const
{
  return _node != it._node;
}
bool operator==(const iterator& it)const
{
  return _node == it._node;
}

4.2 list迭代器的解引用和箭头操作

迭代器的使用就像指针一样,所以
解引用后应该直接得到节点的数据!

由于结构体变量除了可以用解引用

以外还能使用->,所以需要写两个函数:

//可读可写
T& operator*()
{
  return _node->_data;
}
//可读可写
T* operator->()
{
  return &(operator*());
}

解引用操作的应该没有问题

重点难点在这个箭头->函数

可以将这个函数一步一步拆分:

return &(_node->_data);

相当于返回了一个结构体指针

然而我们想要的并不是一个结构体指针

而是确切的值,蛋这样写编译器并不会报错

这是因为编译器为了代码的可读性
帮我们省略了一个箭头->
所以只需要一个箭头->就能访问数据!

注:省略的箭头可以将返回的结构体指针解引用

然而此结构体指针解引用后其实就是data


4.3 迭代器类映射到list类

当我们实现完迭代器类后

就可以在list类中复用迭代器类了:

template<class T>
class List
{
  typedef list_node<T> node;
  typedef __list_iterator<T> iterator;
private:
  node* _head;

有了迭代器类的加持后,list类中的
其他函数如begin和end就是歪瓜裂枣了

iterator begin()
{
  //iterator tmp(_head->_next);
  //return tmp;
  return iterator(_head->_next);//使用了匿名对象
}
iterator end()
{
  //iterator tmp(_head);
  //return tmp;
  return iterator(_head);
}

注:其他关于迭代器的函数会在最后放出

5. const迭代器实现深度剖析

既然list中的迭代器和vector中的不同

那么const迭代器的实现当然也不同

首先我们要明白一点,list的普通迭代器
和const迭代器的区别到底是什么?

const对象的剖析:

const迭代器是为const对象准备的
然而const对象的特征就是不能修改
那么什么操作会让对象的值变化呢?
答案很明显是解引用操作和箭头->
begin和end函数返回后也有可能被修改
注:返回值是T&和T*的函数都要注意

解决方案剖析:

大家可能第一时间想到再重新写一个
const迭代器的类,里面的实现和普通
迭代器都一样,唯一不同的是解引用函数
和箭头->函数的返回值是const对象


5.1 const迭代器实现详解

首先,不用再重新写一个类来实现

接下来的方案不要问为什么

不要问怎么想到的,是天才想到的:

在普通迭代器类中使用三个模板参数

为什么要这样做?

通过观察可以发现,只有两个函数不同

并且这两个函数的类型只有T&和T*

那么就专门为这两个类型设置两个模板

只有在编写这两个函数时用到这两个模板

其余函数还是正常的使用T来当类型

话不多说,上代码

template<class T, class Ref, class Ptr>
struct __list_iterator//迭代器不需要析函数
{
  typedef list_node<T> node;
  typedef __list_iterator iterator;
  node* _node;
}

模板中的Ref代表的是引用&
模板中的Ptr代表的是指针
*

现在重新来实现一下这两个函数:

//按模板参数来
Ref operator*()
{
  return _node->_data;
}
Ptr operator->()
{
  return &(operator*());
}

现在你脑子肯定是一篇空白

但是精髓的地方马上就来了,请耐住性子


5.2 const迭代器和list类的复用

当list类复用了此迭代器类后:

template<class T>
class List
{
  typedef list_node<T> node;
  typedef __list_iterator<T, T&, T*> iterator;
  typedef __list_iterator<T, const T&, const T*> const_iterator;
private:
  node* _head;
}

这样写出来后,普通迭代器和const迭代器

就被完美的区别开了,当传入const对象时

系统会把模板参数实例化为const T&

当传入的是普通对象时,系统会把模板参数

实例化为普通的T,T&和T*

begin和end函数的复写:

const_iterator begin()const
{
  return const_iterator(_head->_next);
}
iterator begin()
{
  return iterator(_head->_next);//使用了匿名对象
}
const_iterator end()const
{
  return const_iterator(_head);
}
iterator end()
{
  return iterator(_head);
}

5.3 const迭代器使用实例

现在,我们通过一个实例来感受这一过程:

void print_list(const list<int>& lt)
{
  auto it = lt.begin();
  while (it != lt.end())
  {
    //*it = 10;
    cout << *it << " ";
    ++it;
  }
  cout << endl;
}

此时的lt变量是const变量,实例化类时

会实例化为<T,const T&,const T*>

然后回退到迭代器类时,迭代器类的

模板参数Ref就对应:const T&

模板参数Ptr就对应:const T*

此时解引用函数的返回值就是const T&

如果你写:*it=10;那么就会报错!


6. list和vector的对比

详情请看下表:

目录 vector list
底层结构 顺序表,空间连续 带头双向循环链表
随机访问 支持随机访问,访问效率为O(1) 不支持随机访问,访问效率为O(N)
插入和删除 任一位置插入删除效率低,还需扩容 任一位置插入效率高
空间利用率 空间,缓存利用率高 不连续空间,容易造成内存碎片
迭代器 原生态的指针 对原生指针的封装
迭代器失效 插入和删除都会导致 只有删除会导致
使用场景 高效存储,支持随机访问 有大量插入和删除操作,不关心随机访问

注:这个表格不能死记硬背,要理解记忆


7. 总结以及代码分享

总的来说,list的底层实现较于vector

来说要复杂一点,这其中的底层原因

就是list的迭代器还需要一层封装,而

vector的迭代器不需要额外封装

C++的强大就在于把复杂的底层
全部封装起来了,而表面的使用上
list和vector并无太大区别,这就是
C++封装的魅力!

list模拟实现全部代码分享:

List模拟实现全部代码

注:全部代码中包含反向迭代器

目前阶段可以跳过不管


🔎 下期预告:栈和队列 🔍


相关文章
|
10月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
存储 DataX C语言
vector与list的简单介绍
vector是表示大小可以变化的数组的序列容器。就像数组一样,vector对其元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问其元素,并且与数组中的元素一样高效。但与数组不同的是,它们的大小可以动态变化,它们的存储由容器自动处理。在内部,vector使用动态分配的数组来存储其元素。当插入新元素时,可能需要重新分配此数组才能增大大小,这意味着分配一个新数组并将所有元素移动到该数组。
409 0
vector与list的简单介绍
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
428 2
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
863 73
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
756 3
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
1018 1
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
1055 1
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
Java API Apache
怎么在在 Java 中对List进行分区
本文介绍了如何将列表拆分为给定大小的子列表。尽管标准Java集合API未直接支持此功能,但Guava和Apache Commons Collections提供了相关API。
647 1
|
运维 关系型数据库 Java
PolarDB产品使用问题之使用List或Range分区表时,Java代码是否需要进行改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。

热门文章

最新文章