【C++初阶:STL —— list】list的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比 下

简介: 【C++初阶:STL —— list】list的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比

二、list的深度剖析及模拟实现

💨大概瞅下源码的大概框架

template <class T>
struct __list_node {
  typedef void* void_pointer;
  //其实感觉没必要搞成void*,后面还得强转
  void_pointer next;
  void_pointer prev;
  T data;
};
class list {
protected:
  typedef __list_node<T> list_node;
  typedef list_node* link_type;
protected:
  link_type node;
protected:
    link_type get_node() { return list_node_allocator::allocate(); }
protected:
  void empty_initialize() { 
  //体现了带头双向循环链表的特性
    node = get_node();
    node->next = node;
    node->prev = node;
  }
public:
   list() { empty_initialize(); }
}

💦 模拟实现list

💨 list.h

#pragma once
namespace bit
{
  template<class T>
  struct __list_node//一般前面使用__,表示给内部使用,为什么不使用内部类,因为不仅仅list要用它,迭代器也要用它(C++里还是比较少用内部类的)
  {
    __list_node<T>* _next;
    __list_node<T>* _prev;
    T _data;
    //这里支持无参的new Node,或者有参的new Node(x)
    __list_node(const T& x = T())
      : _next(nullptr)
      , _prev(nullptr)
      , _data(x)
    {}
  };
  //迭代器,用一个类去封装节点的指针,所以目前为止我们已知的迭代器的实现方式有两种:
  //a)原生指针(天然迭代器),它所指向的空间物理结构是连续的,也就是说只有string、vector才可以做天然迭代器(*就是当前位置的数据/++就是下一个位置/--就是下一个位置)
  //b)类(用一个类去封装原生指针,重载相关运算符,让这个类对象用起来像指针),它所指向的空间物理结构是不连续的(没有封装指针之前,*是结构体/++--的位置不明确)
  //这里使用类模板是因为里面需要使用__list_node节点,而它又是一个类模板
  //不适用const
  //template<class T>
  //struct __list_iterator
  //{
  //  typedef __list_iterator<T> self;
  //  typedef __list_node<T> Node;
  //  Node* _node;
  //  //这个类里包含一个节点的指针,然后就可以用一个节点的指针调用它的构造函数就可以构造一个迭代器,这里我们的迭代器begin()/end()是list里的成员函数,它们分别返回第一个有效节点和最后一个节点的下一个无效头节点
  //  __list_iterator(Node* node)
  //    : _node(node)
  //  {}
  //  //*it
  //  T& operator*()
  //  {
  //    return _node->_data;
  //  }
  //  //++it
  //  self& operator++()
  //  {
  //    _node = _node->_next;
  //    return *this;
  //  }
  //  //it++,tmp出了作用域销毁,传值返回,不能用引用
  //  self operator++(int)
  //  {
  //    //备份
  //    self tmp(*this);
  //    _node = _node->_next;
  //    return tmp;
  //  }
  //  //--it
  //  self& operator--()
  //  {
  //    _node = _node->_prev;
  //    return *this;
  //  }
  //  //it--
  //  self operator--(int)
  //  {
  //    self tmp(*this);
  //    _node = _node->_prev;
  //    return tmp;
  //  }
  //  //it+,它可以operator+(),但是效率很低,所以也就没有支持
  //  //lt.begin() != lt.end()
  //  bool operator!=(const self& it) const
  //  {
  //    return _node != it._node;
  //  }
  //  //lt.begin() == lt.end()
  //  bool operator==(const self& it) const
  //  {
  //    return _node == it._node;
  //  }
  //  //__list_iterator时有一个节点的指针,要不要去做深拷贝和析构函数呢?
  //  /*并不是说一个类里有节点的指针,就要做深拷贝和析构,要看这个空间是不是你的,你拿了一个指针构造了一个迭代器,这个节点是你迭代器的吗,节点是list的,不需要迭代器去析构,
  //  同时iterator it = begin();,这里需要的就是浅拷贝,begin()构造了一个迭代器对象(_head->_next),你把这个对象赋值给it,难道是要再深拷贝一个节点出来吗,所以说默认生成的浅拷贝也是有价值的
  //  所以我们不需要去写拷贝构造和赋值,因为默认生成的对于内置类型就会完成浅拷贝;不需要去写析构函数,因为节点是list的对象  */
  //};
  //常规的写法就是实现一个专属的__list_const_iterator类,这两个除了类名称、operator*()不一样,其它完全一样
  //template<class T>
  //struct __list_const_iterator
  //{
  //  typedef __list_const_iterator<T> self;
  //  typedef __list_node<T> Node;
  //  Node* _node;
  //  __list_const_iterator(Node* node)
  //    : _node(node)
  //  {}
  //  //*it
  //  const T& operator*()
  //  {
  //    return _node->_data;
  //  }
  //  //++it
  //  self& operator++()
  //  {
  //    _node = _node->_next;
  //    return *this;
  //  }
  //  //it++,tmp出了作用域销毁,传值返回,不能用引用
  //  self operator++(int)
  //  {
  //    //备份
  //    self tmp(*this);
  //    _node = _node->_next;
  //    return tmp;
  //  }
  //  //--it
  //  self& operator--()
  //  {
  //    _node = _node->_prev;
  //    return *this;
  //  }
  //  //it--
  //  self operator--(int)
  //  {
  //    self tmp(*this);
  //    _node = _node->_prev;
  //    return tmp;
  //  }
  //  //it+,它可以operator+(),但是效率很低,所以也就没有支持
  //  //lt.begin() != lt.end()
  //  bool operator!=(const self& it) const
  //  {
  //    return _node != it._node;
  //  }
  //  //lt.begin() == lt.end()
  //  bool operator==(const self& it) const
  //  {
  //    return _node == it._node;
  //  }
  //};
  //仰望大佬
  //iterator:<T, T&, T*>
  //const_iterator<T, const T&, const T*>
  template<class T, class Ref, class Ptr>
  struct __list_iterator
  {
    typedef __list_iterator<T, Ref, Ptr> self;
    typedef __list_node<T> Node;
    Node* _node;
    __list_iterator(Node* node)
      : _node(node)
    {}
    //*it
    Ref operator*()
    {
      return _node->_data;
    }
    //
    Ptr operator->()
    {
      return  &_node->_data;
    }
    //++it
    self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    //it++,tmp出了作用域销毁,传值返回,不能用引用
    self operator++(int)
    {
      //备份
      self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
    //--it
    self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    //it--
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
    //it+,它可以operator+(),但是效率很低,所以也就没有支持
    //lt.begin() != lt.end()
    bool operator!=(const self& it) const
    {
      return _node != it._node;
    }
    //lt.begin() == lt.end()
    bool operator==(const self& it) const
    {
      return _node == it._node;
    }
  };
  template<class T>
  class list
  {
    //这个不想让外面的人用,所以默认是私有的
    typedef __list_node<T> Node;
  public:
    //__list_iterator是什么名字并不重要,最终都会typedef为iterator,因为我们要用统一的方式去访问容器。比如vector<int>::iterator;你那个真正的迭代器是什么名称不重要
    //typedef __list_iterator<T> iterator;
    //typedef __list_const_iterator<T> const_iterator;
    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;
    iterator begin()
    {
      return iterator(_head->_next);
    }
    iterator end()
    {
      return iterator(_head);//最后一个节点的下一个位置,哨兵位
    }
    const_iterator begin() const
    {
      return const_iterator(_head->_next);
    }
    const_iterator end() const
    {
      return const_iterator(_head);
    }
    list()
    {
      //带头双向循环链表
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
    }
    //lt2(lt1)
    //传统写法
    /*list(const list<T>& lt)
    {
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
      for(const auto& e : lt)
      {
        push_back(e); 
      }
    }*/
    //现代写法,可以看到这里现代写法相比传统写法并没有讲到便宜,所以说现代写法也不一定是最优,需要灵活运用
    //它要先支持一个迭代器区间的才能写现代写法,同时这里支持其它容器的拷贝构造                                                                                                                                                                                                                                                                                  
    template<class InputIterator>
    list(InputIterator first, InputIterator last)
    {
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
      while (first != last)
      {
        push_back(*first);
        ++first;
      }
    }
    list(const list<T>& lt)
    {
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
      list<T> tmp(lt.begin(), lt.end());
      std::swap(_head, tmp._head);
    }
    //lt1 = lt2
    //传统写法
    /*list<T>& operator=(const list<T>& lt)
    {
      if(this != &lt)
      {
        clear();
        for(const auto& e : lt)
        {
          push_back(e); 
        } 
      }
      return *this;
    }*/
    //现代写法,对于赋值有一个原则,深拷贝现代写法一定非常简单(只要写了拷贝构造,赋值就一定能用现代写法,并且很简洁)
    list<T>& operator=(list<T> lt)
    {
      swap(_head, lt._head);
      return *this;
    }
    //对于~list,先实现clear
    ~list()
    {
      clear();
      //清除头节点
      delete _head;
      _head = nullptr;
    }
    void clear()
    {
      iterator it = begin();
      while(it != end())
      {
        //不清除头节点,直接复用
        it = erase(it);
      } 
    }
    void push_back(const T& x)
    {
      //同insert(_head, x);,end()是最后节点的下一个位置_head
      insert(end(), x);
    }
    void push_front(const T& x)
    {
      //insert(_head->_next, x);,begin()是头节点的下一个位置
      insert(begin(), x);
    }
    void pop_back()
    {
      //同erase(_head->_prev);
      erase(--end());
    }
    void pop_front()
    {
      //同erase(_head->_next);
      erase(begin());
    }
    iterator insert(iterator pos, const T& x)
    {
      //备份;这里就体现了为啥我们在设计__list_node和__list_iterator时为什么是struct,如果用class,这里就得用友源,但没必要
      Node* cur = pos._node;
      Node* prev = cur->_prev;
      Node* newnode = new Node(x);
      //关联
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = cur;
      cur->_prev = newnode;
      //双向链表的insert是不会失效的,返回新插入的节点,这里同 __list_iterator<T> (newnode),且这里是一个匿名对象。
      return iterator(newnode);
      //同上,newnode是Node*(__list_node<T>)类型,而insert的返回类型是iterator(__list_iterator<T>)也可以,这是因为单参数的构造函数支持隐式类型转换,如 A aa = 1; 或 string s = "hello";。但因为不太明确,所以不太推荐
      //return newnode;
    }
    //虽然在外面调用list实例化了lt,但是list里面的函数编译器会按需实例化(外面调用了哪个成员函数,就实例化哪个)
    //erase不是模板?这里要注意类模板里的成员函数都是函数模板,erase的参数iterator就是typedef出来的模板
    iterator erase(iterator pos)
    {
      //空链表(只剩头节点)不能erase
      assert(pos != end());
      //备份
      Node* cur = pos._node;
      Node* prev = cur->_prev;
      Node* next = cur->_next;
      //释放
      delete cur;
      //关联
      prev->_next = next;
      next->_prev = prev;
      //返回删除元素之后的位置,注意这里的pos已经是野指针了
      return iterator(next);
    }
    size_t size()
    {
      size_t n = 0;
      iterator it = begin();
      while (it != end())
      {
        ++it;
        ++n;
      }
      return n;
    }
    bool empty()
    {
      return begin() == end();
    }
  private:
    Node* _head;
  };
  //const迭代器一般出现的场景是在传参的时候
  void print_list(const list<int>& l)
  {
    list<int>::const_iterator it = l.begin();
    while (it != l.end())
    {
      //*it += 2;//err,表达式必须是可修改的左值
      cout << *it << " ";
      ++it;
    }
    cout << endl;
  }
  void test_list1()
  {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    print_list(lt);
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
      *it *= 2;
      cout << *it << " ";
      ++it;
    }
    cout << endl;
    for (auto e : lt)
    {
      cout << e << " ";
    }
    cout << endl;
  }
  struct TreeNode
  {
    struct TreeNode* _left;
    struct TreeNode* _right;
    int _val;
    TreeNode(int val = -1)
      : _left(nullptr)
      , _right(nullptr)
      , _val(val)
    {}
  };
  //ostream operator<<(ostream& TreeNode, TreeNode n){}
  void test_list2() 
  {
    list<TreeNode> lt;
    lt.push_back(TreeNode(1));
    lt.push_back(TreeNode(2));
    lt.push_back(TreeNode(3));
    lt.push_back(TreeNode(4));
    list<TreeNode>::iterator it = lt.begin();
    while (it != lt.end())
    {
      //*it调operator*,返回_node->_data;,_data是T类型,而这的T是TreeNode,是一个结构体类型的树节点,而*it后是TreeNode,我们说了内置类型可以直接cout输出,但是自定义类型不行,所以我们可以operator<<,或者不用重载(使用结构体的特性)
      //cout << *it << " ";
      //有些场景不适合用cout
      //cout << (*it)._val << " ";
      //这是结构体的访问方式(结构体和指针的访问方式,细节如下说明 3. List item)
      printf("val:%d,left:%p,right:%p\n", (*it)._val, (*it)._left, (*it)._right);
      //像指针一样就要去重载->,[int* p   *p] [TreeNode* p     p->_val]
      printf("val:%d,left:%p,right:%p\n", it->_val, it->_left, it->_right);
      ++it;
    }
    cout << endl;
  }
  void test_list3()
  {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    print_list(lt);
    lt.clear();
    lt.push_back(10);
    lt.push_back(20);
    lt.push_back(30);
    lt.push_back(40);
    print_list(lt);
  }
  void test_list4()
  {
    list<int> lt1;
    lt1.push_back(1);
    lt1.push_back(2);
    lt1.push_back(3);
    lt1.push_back(4);
    list<int> lt2(lt1);
    print_list(lt2);
    list<int> lt3(lt1.begin(), lt1.end());
    string s("hello");
    list<int> lt4(s.begin(), s.end());    
    for (auto e : lt3)
    {
      cout << e << " ";
    }
    cout << endl;
    for (auto e : lt4)
    {
      cout << e << " ";
    }
    cout << endl; 
    lt1 = lt4;
    for (auto e : lt1)
    {
      cout << e << " ";
    }
    cout << endl; 
  }
}

💨 test.cpp

#include<iostream>
#include<stdbool.h>
#include<assert.h>
using namespace std;
#include"list.h"
int main()
{
  bit::test_list1();
  bit::test_list2();
  bit::test_list3();
  bit::test_list4();
  return 0;
}

📝说明

  1. List item
    list 迭代器 ❓
    在 string、vector 里我们实现的都是简单迭代器,用的是原生指针。而对于 list,现在要遍历它,你用原生指针是不能实现的(++操作不会指向下一个节点),因为每个节点的地址是不连续的,并且毫无关联。
    我们瞅下源码,看下是它的迭代器是怎么设计的:可以看到它的迭代器不再是一个 Node*,而用了一个 __list_iterator 的类去封装节点的指针,它有三个模板参数(先不管)。对于 string 是 char*,对于 vector 是 T*,对于 list 是自定义类型,所以我们说迭代器是像指针一样的东西。

    这个类的核心成员是 Link_type node,Link_type 是 Node*

    本身类(对象)不能 ++,但是我们可以去重载它的 ++,也就是说 __list_iterator 不是指针,但是我们可以让它像指针一样


  2. List item
    const 迭代器的实现,对于 const 迭代器的实现,我们一般人是再写一个专属的 __list_const_iterator 类,这两个除了类名称、operator*() 不一样,其它完全一样,非常的冗余。但是好像也没有更好的办法了呀,怎么让 operator*() 一个返回 T&,一个返回 const T& 呢 ?这里我们瞅下高手写的源码是怎么实现的:
    可以看到如下源码的实现,它只实现了一个类,如是是普通迭代器 operator* 的返回值是 T&,如果是 const 迭代器,operator* 的返回值是 const T&

  3. List item
    list 的迭代器要像指针一样,除了需要重载 “*”,还需要重载 “->”。

  4. List item
    严格来说迭代器是分类型的,在我们的 STL 3.0 中找到 stl_iterator.h 文件,它里面把迭代器类型分为五种,并且它们构成继承关系:

    这里涉及 “类型萃取” 的概念,有兴趣可以去了解下,这里就不细谈了。

💦 对模拟的bite::list进行测试

参考 test.cpp 文件

三、list与vector的对比

在 32 位的机器下 vector 和 list 的迭代器所占多少字节 ❓

相比 vector 的迭代器,就逻辑上来说 list 的迭代器就要复杂一点,因为 vector 的迭代器是一个天然的迭代器,它是原生指针,但要注意原生指针要做天然迭代器的要求是指针指向的物理空间是连续的。而 list 不能做天然的迭代器,所以我们要将指针封装成一个类,让它完成指针的操作。而就物理层面上来说 vector 和 list 的迭代器没有更复杂,也就是它们所占用的空间是一样的。至此,我们就可以看到 C++ 运算符重载的能力。

所以在 32 位的机器下 vector 的迭代器占 4 个字节;list 的迭代器也占 4 个字节。

vector 与 list 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景也不同,其主要不同如下:

vector list
底层结构 动态顺序表,一段连续空间 带头结点的双向循环链表
随机访问 支持随机访问,访问某个元素效率 O(1) 不支持随机访问,访问某个元素效率 O(N)
插入和删除 任意位置插入和删除效率低,需要搬移元素,时间复杂度为 O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1)
空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器 原生态指针 对原生态指针(节点指针)进行封装
迭代器失效 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失数,删除时,当前迭代器需要重新赋值否则会失效 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问


相关文章
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
481 0
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
1083 5
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
476 1
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
|
算法 C++ 容器
C++之打造my vector篇(下)
C++之打造my vector篇(下)
|
存储 编译器 C++
C++之打造my vector篇(上)
C++之打造my vector篇(上)
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
620 12
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)