【C++】STL之list容器的模拟实现

简介: 【C++】STL之list容器的模拟实现

前言

本文章进入C++STL之list的模拟实现。


一、list的三个类的关系分析图

在STL标准库实现的list中,这个链表是一个== 双向带头循环链表==。

说明:

list是一个类,成员变量为_head

节点类node,是每一个节点。

list的迭代器也升级成了类,成员变量为node。

  • 把迭代器升级成类是为了能够重载++,–,*,!=等可以用在vector迭代器上的操作。

vector和list的区别

vector list
底层结构 是一块连续的空间 不是连续的空间
随机访问 支持下标随机访问 不支持下标随机访问
插入和删除 如果是头插头删,效率为O(n) ,如果插入时需要扩容,会付出更高的代价 头插头删都很方便,O(1)的效率
空间利用情况 底层为连续的动态空间,内存碎片小,空间利用率高 底层是一块一块不连续的空间,内存碎片多,空间利用率较低
迭代器 使用天然的原生迭代器 将迭代器封装起来再对外开放
迭代器失效 在进行插入删除时,插入/删除位置及其之后,空间不再属于自己,由于空间是连续的,其之后的迭代器全部失效 在插入时迭代器不会失效,只有在删除时迭代器会失效,且不会影响其他迭代器
使用场景 需要进行大量随机访问,需要高效存储,不关心插入删除。 大量插入删除操作时,不关心随机访问

vector在内存中是一块连续的地址空间,vector下标就是天然的迭代器。

list在内存中并不连续,所以不支持随机访问。为了能够让list完成诸如vector的操作,比如:

list<int>:: iterator it = lt1.begin();
while (it != lt1.end())
{
  cout << *it << " ";
  ++it;
}

我们对list的迭代器进行封装,重载各种操作符,以便完成上述的操作。

1.节点的成员变量以及构造函数

在数据结构之链表中,我们知道一个节点必须包含prev,next,val三个变量,在STL的list也是如此。

template<class T>
//class list_node 如果这样写,节点的所有成员都是私有的,无法直接访问
struct list_node
{
  list_node<T>* _prev;
  list_node<T>* _next;
  T _val;
  //节点的构造函数
  list_node(const T& val = T())
    :_prev(nullptr)
    , _next(nullptr)
    , _val(val)
  {}
};

注意:

1.在C++,struct升级成了类,但如果不标明,struct的所有成员都是公有的。

2.类名不是类型,不能使用list_node*来作为指针的类型。要使用模板来作为类型。

2.list的迭代器

需要注意的一个点:

  • list的迭代器是用来访问的,而不是用来管理节点的空间,所以list的空间是由自己管理和释放的,我们知道,程序崩溃主要就是同一块空间释放两次。
  • 所以list的迭代器在进行拷贝构造时可以使用浅拷贝,不会造成程序的崩溃。
list<int>:: iterator it = lt1.begin();

对这行代码来说,迭代器不是直接赋值给it的,因为迭代器升级成了类,而不是一个指针。这里会调用迭代器的拷贝构造函数。

list迭代器代码如下:

//迭代器也封装起来,形成我们熟悉的*it,++it
template<class T, class Ref, class Ptr>
//class __list_iterator 如果这样写,迭代器是私有的,无法直接使用
//迭代器使用类模板,为了重载Ref,Ptr,设置3个模板参数
struct __list_iterator
{
  typedef list_node<T> Node;
  typedef __list_iterator<T, Ref, Ptr> self;
  //等价于:
  //typedef __list_iterator<T, T&, T*> self;
  Node* _node; //成员
  __list_iterator(Node* node)
    :_node(node)
  {}
  //返回引用,可读可写
  Ref operator*()
  {
    return _node->_val;
  }
  //返回地址
  Ptr operator->()
  {
    return &_node->_val;
  }
  //返回迭代器本身
  self& operator++()
  {
    _node = _node->_next;
    return *this;
  }
  //后置++
  self operator++(int)
  {
    self tmp(*this);
    _node = _node->_next;
    return tmp;
  }
  self& operator--()
  {
    _node = _node->_prev;
    return *this;
  }
  //后置--
  self operator--(int)
  {
    self tmp(*this);
    _node = _node->_prev;
    return tmp;
  }
  bool operator!=(const self& it) const
  {
    return _node != it._node;
  }
  bool operator==(const self& it) const
  {
    return _node == it._node;
  }
};
  • 1.使用三个模板参数的原因:
  • 要求返回指针,如:用指针->访问成员的情况;或者返回迭代器本身的情况,如 ++ ,–操作。
  • 2.在重载operator->()时,返回的是val的地址,所以我们在调用该函数时,需要进行两次->操作才能访问成员变量。
    比如:
struct A
{
  A(int a1 = 0, int a2 = 0)
    :_a1(a1)
    , _a2(a2)
  {}
  int _a1;
  int _a2;
};
bit::list<A>lt2;
lt2.insert(lt2.begin(), A(1, 1));
lt2.insert(lt2.begin(), A(2, 2));
list<A>::iterator it = lt2.begin();
while (it != lt2.end())
{
  //迭代器重载了*,返回T&,也就是A本身
  cout << it->_a1 << " " << it->_a1 << endl;
  ++it;
}
cout << endl;

本质上,应该需要 it->->_al,it->->_a2才能够访问成员。

第一个it->是调用operator->重载,返回val的地址,第二个->是通过地址访问成员。

编译器为了简化操作,以及看起来没有那么别扭,对it->->_a1进行简化成了it->_a1。

  • 3.类模板中的Ref是Reference,是引用的意思,Ptr是Pointer,是指针的意思。通过这两个模板名可以知道迭代器需要支持&,和*的操作。
  • 4.迭代器被重命名成self,也就是迭代器本身,在++,–操作时,需要返回迭代器本身。

二、list的增删查改工作

2.1insert()

在pos位置之前插入val值。

首先要获取pos位置的前一个节点,记为posprev。

将新的节点插入即可。

iterator insert(iterator pos, const T& val)
{
  Node* newnode = new Node(val);
  Node* inspos = pos._node;
  Node* posprev = inspos->_prev;
  newnode->_next = inspos;
  inspos->_prev = newnode;
  newnode->_prev = posprev;
  posprev->_next = newnode;
  ++_size;
  return posprev;
}

list的插入没有迭代器的失效问题。

2.2erase()

删除pos位置的节点,并返回pos位置的下一个位置的迭代器。

iterator erase(iterator pos)
{
  assert(pos != end());
  Node* erapos = pos._node;
  Node* eraprev = erapos->_prev;
  Node* eranext = erapos->_next;
  eraprev->_next = eranext;
  eranext->_prev = eraprev;
  erapos->_prev = erapos->_next = nullptr;
  delete erapos;
  --_size;
  return eranext;
}

erase后pos位置的迭代器会失效,我们需要返回下一个位置的迭代器来防止继续访问出现失效的情况。

删除后如果迭代器不更新,继续访问会出现野指针问题。

2.3 push_back(),pop_back(),push_front(),pop_front()

这几个函数分别是:尾插,尾删,头插,头删,我们复用insert和erase即可完成。

void push_back(const T& x)
{
  insert(end(), x);
}
void pop_back()
{
  erase(--end());
}
void push_front(const T& x)
{
  insert(begin(), x);
}
void pop_front()
{
  erase(begin());
}

2.4clear()

该函数是将链表的所有节点全部释放,哨兵位头节点除外。

//把所有的节点都释放了,除了哨兵位的头节点
void clear()
{
  iterator it = begin();
  while (it != end())
  {
    //为防止迭代器失效,erase后会返回pos位置的下一个位置,所以it不需要++
    it = erase(it);
  }
  _size = 0;
}

注意:erase()删除pos节点后,会返回pos位置的下一个位置,所以这里不需要++it


三、list的默认成员函数

3.1 构造函数

首先申请一个哨兵位的头节点。

void empty_init()
{
  _head = new Node;
  _head->_prev = _head;
  _head->_next = _head;
  _size = 0;
}
//构造函数,构造出一个链表
list()
{
  empty_init();
}

2.2 拷贝构造

由于我们将迭代器进行了封装升级,现在可以使用++it等操作。

拷贝构造是深拷贝,先new一个_head哨兵位的头节点,再逐个节点进行尾插。

list(list<T>& lt)
{
  empty_init();
  list<T>::iterator it = lt.begin();
  while (it != lt.end())
  {
    push_back(*it);
    ++it;
    ++_size;
  }
}

2.3析构

调用clear函数释放所有空间,再释放头节点。

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

完整代码

namespace bit
{
  //c++不喜欢使用内部类,所以把节点类放在list外面
  //节点封装成类
  template<class T>
  //class list_node 如果这样写,节点的所有成员都是私有的,无法直接访问
  struct list_node
  {
    list_node<T>* _prev;
    list_node<T>* _next;
    T _val;
    //节点的构造函数
    list_node(const T& val = T())
      :_prev(nullptr)
      , _next(nullptr)
      , _val(val)
    {}
  };
  //迭代器也封装起来,形成我们熟悉的*it,++it
  template<class T, class Ref, class Ptr>
  //class __list_iterator 如果这样写,迭代器是私有的,无法直接使用
  //迭代器使用类模板,为了重载Ref,Ptr,设置3个模板参数
  struct __list_iterator
  {
    typedef list_node<T> Node;
    typedef __list_iterator<T, Ref, Ptr> self;
    //等价于:
    //typedef __list_iterator<T, T&, T*> self;
    Node* _node; //成员
    __list_iterator(Node* node)
      :_node(node)
    {}
    //返回引用,可读可写
    Ref operator*()
    {
      return _node->_val;
    }
    //返回地址
    Ptr operator->()
    {
      return &_node->_val;
    }
    //返回迭代器本身
    self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    //后置++
    self operator++(int)
    {
      self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
    self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    //后置--
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
    bool operator!=(const self& it) const
    {
      return _node != it._node;
    }
    bool operator==(const self& it) const
    {
      return _node == it._node;
    }
  };
  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;
    iterator begin()
    {
      //return iterator(_head->_next);
      return _head->_next; // 单参数的构造函数可以进行隐式类型转换,从节点的指针转换成一个类
    }
    iterator end()
    {
      //return iterator(_head);
      return _head;
    }
    const_iterator begin() const
    {
      return const_iterator(_head->_next);
      //return _head->_next; // 单参数的构造函数可以进行隐式类型转换,从节点的指针转换成一个类
    }
    const_iterator end() const
    {
      return const_iterator(_head);
      //return _head;
    }
    void empty_init()
    {
      _head = new Node;
      _head->_prev = _head;
      _head->_next = _head;
      _size = 0;
    }
    //构造函数,构造出一个链表
    list()
    {
      empty_init();
    }
    //拷贝构造
    //lt2(lt1)
    list(list<T>& lt)
    {
      empty_init();
      list<T>::iterator it = lt.begin();
      while (it != lt.end())
      {
        push_back(*it);
        ++it;
        ++_size;
      }
    }
    //析构
    ~list()
    {
      clear();
      delete _head;
      _head = nullptr;
    }
    //把所有的节点都释放了,除了哨兵位的头节点
    void clear()
    {
      iterator it = begin();
      while (it != end())
      {
        //为防止迭代器失效,erase后会返回pos位置的下一个位置,所以it不需要++
        it = erase(it);
      }
      _size = 0;
    }
    void swap(list<T>& tmp)
    {
      std::swap(_head, tmp._head);
      std::swap(_size, tmp._size);
    }
    //赋值运算符重载
    //先调用拷贝构造,再调用赋值重载
    //lt3 = lt1
    //lt1先拷贝构造给tmp
    list<T>& operator=(list<T> tmp)
    {
      swap(tmp);
      //出了作用域,调用析构
      return *this;
    }
    size_t size() const
    {
      return _size;
    }
    //尾插传过来的是一个数据,而不是一个节点
    void push_back(const T& x)
    {
      //Node* tail = _head->_prev;
      调用构造
      //Node* newnode = new Node(x);
      //tail->_next = newnode;
      //newnode->_prev = tail;
      //_head->_prev = newnode;
      //newnode->_next = _head;
      insert(end(), x);
    }
    void pop_back()
    {
      //Node* del = _head->_prev;
      //_head->_prev = del->_prev;
      //del->_prev->_next = _head;
      //del->_next = del->_prev = nullptr;
      //delete del;
      //左闭右开,需要--
      erase(--end());
    }
    void push_front(const T& x)
    {
      insert(begin(), x);
    }
    void pop_front()
    {
      erase(begin());
    }
    //在pos位置之前插入val
    //不会出现迭代器失效问题
    iterator insert(iterator pos, const T& val)
    {
      Node* newnode = new Node(val);
      Node* inspos = pos._node;
      Node* posprev = inspos->_prev;
      newnode->_next = inspos;
      inspos->_prev = newnode;
      newnode->_prev = posprev;
      posprev->_next = newnode;
      ++_size;
      return posprev;
    }
    //迭代器封装成了类,所以需要通过迭代器找到节点
    //防止迭代器失效,返回删除节点的下一个
    iterator erase(iterator pos)
    {
      assert(pos != end());
      Node* erapos = pos._node;
      Node* eraprev = erapos->_prev;
      Node* eranext = erapos->_next;
      eraprev->_next = eranext;
      eranext->_prev = eraprev;
      erapos->_prev = erapos->_next = nullptr;
      delete erapos;
      --_size;
      return eranext;
    }
  private:
    Node* _head;
    size_t _size;
  };
}

总结

本文章完成了list的常用接口的模拟实现。

相关文章
|
4天前
|
设计模式 算法 Java
【c++】STL之stack和queue详解
【c++】STL之stack和queue详解
6 1
|
10天前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
16 0
|
11天前
|
存储 语音技术 Python
语音识别,函数综合案例,黑马ATM,/t/t一个对不齐,用两个/t,数据容器入门,数据容器可以分为列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)
语音识别,函数综合案例,黑马ATM,/t/t一个对不齐,用两个/t,数据容器入门,数据容器可以分为列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)
|
12天前
|
存储 算法 数据处理
【C++】STL简介
**STL是C++标准库的关键部分,源于Alexander Stepanov的泛型编程研究。它提供了数据结构(如vector、list)和算法,是高效、通用的软件框架。STL始于惠普,后由SGI发展,现已成为C++1998标准的一部分并不断进化。它包括容器、迭代器、算法、仿函数、配接器和分配器六大组件,带来高效性、通用性和可扩展性,但也存在性能开销和学习难度。学习STL涉及理解底层数据结构、用法、实现和实践。推荐[cplusplus.com](https://cplusplus.com)作为学习资源。**
|
12天前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。
|
12天前
|
算法 前端开发 C++
C++基础知识(八:STL标准库 deque )
deque在C++的STL(Standard Template Library)中是一个非常强大的容器,它的全称是“Double-Ended Queue”,即双端队列。deque结合了数组和链表的优点,提供了在两端进行高效插入和删除操作的能力,同时保持了随机访问的特性。
|
12天前
|
存储 C++ 索引
C++基础知识(八:STL标准库 Map和multimap )
C++ 标准模板库(STL)中的 map 容器是一种非常有用的关联容器,用于存储键值对(key-value pairs)。在 map 中,每个元素都由一个键和一个值组成,其中键是唯一的,而值则可以重复。
|
26天前
|
前端开发 安全 数据库
Web架构&前后端分离站&Docker容器站&集成软件站&建站分配
Web架构&前后端分离站&Docker容器站&集成软件站&建站分配
|
9天前
|
边缘计算 物联网 开发者
什么是容器Docker?
什么是容器?容器,也叫Docker,是一个开源的容器化平台,用于开发、测试和部署应用程序。通过将软件打包为标准化的单元(容器),使得应用程序可以在任何地方一致地运行,不论是在开发者的本地机器上,还是在云计算平台上。Docker容器包含了应用程序运行所需的一切,包括代码、运行时、系统工具、系统库等,从而解决了“在我这里可以正常工作,但在服务器上不行”的问题。
33 1
|
13天前
|
安全 关系型数据库 开发者
Docker Compose凭借其简单易用的特性,已经成为开发者在构建和管理多容器应用时不可或缺的工具。
Docker Compose是容器编排利器,简化多容器应用管理。通过YAML文件定义服务、网络和卷,一键启动应用环境。核心概念包括服务(组件集合)、网络(灵活通信)、卷(数据持久化)。实战中,编写docker-compose.yml,如设置Nginx和Postgres服务,用`docker-compose up -d`启动。高级特性涉及依赖、环境变量、健康检查和数据持久化。最佳实践涵盖环境隔离、CI/CD、资源管理和安全措施。案例分析展示如何构建微服务应用栈,实现一键部署。Docker Compose助力开发者高效驾驭复杂容器场景。
30 1