【C++】List -- 详解(下)

简介: 【C++】List -- 详解(下)

【C++】List -- 详解(上)https://developer.aliyun.com/article/1514686?spm=a2c6h.13148508.setting.25.4b904f0ejdbHoA

二、STL_list 源码剖析

1、list 的底层结构

SGI 版本 STL 中 list 部分源码:

// 链表节点结构
template <class T>
struct __list_node {
    typedef void* void_pointer;
    void_pointer next;
    void_pointer prev;
    T data;
};
 
// 迭代器 -- 一个类封装了节点指针
template<class T, class Ref, class Ptr>
struct __list_iterator {
    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;
    typedef __list_iterator<T, Ref, Ptr> self;
    // ...
 
    typedef Ptr pointer;
    typedef Ref reference;
    // ...
 
    typedef __list_node<T>* link_type;
 
    link_type node; // 节点指针
    // ...
};
 
// 带头双向循环链表结构
template <class T, class Alloc = alloc>
class list {
protected:
    typedef __list_node<T> list_node; // 节点
    // ...
 
public:      
    typedef T value_type;
    typedef list_node* link_type; // 节点指针
    // ...
 
public:
    // 迭代器(这里的iterator和const_iterator是定义在list类域中的内嵌类型)
    typedef __list_iterator<T, T&, T*> iterator; // 传了T,T&,T*模板参数
    typedef __list_iterator<T, const T&, const T*> const_iterator; // 传了T,const T&,const T*模板参数
    // ...
 
protected:
    link_type node; // 成员变量,节点指针
    // ...
};

三、list的模拟实现

1、list 的节点结构

双向循环链表节点结构:一个数据域,两个指针。

namespace xyl
{
  // List的节点类
  template<class T>
  struct list_node
  {
    T _data;               // 数据域
    list_node<T>* _prev; // 前驱指针
    list_node<T>* _next; // 后继指针
 
    // 默认构造函数,T()是缺省值(T类型的默认构造函数)
    list_node(const T& x= T())
          :_data(x)
      ,_prev(nullptr)
      ,_next(nullptr)
    {}
  };
}

2、 list 的迭代器

前面学习的 string 和 vector 容器的正向迭代器其实就是原生指针,++ 可以直接走到下一个元素,而 list 的正向迭代器是用一个类,把节点指针封装起来,然后通过实现运算符重载函数,让迭代器具有像指针一样的行为,比如:重载了 * 运算符,实现了 *it 解引用返回节点中的存储的数据的引用,重载了 ++ 运算符,实现了 it++ 直接走到下一个元素。

list 正向迭代器的结构

namespace xyl
{
    // T:节点中数据的类型  Ref:节点中数据的引用  Ptr:节点中数据的指针(地址)
  template<class T, class Ref, class Ptr>
  struct __list_iterator
  {
    typedef list_node<T> Node; // 节点
    typedef __list_iterator<T, Ref, Ptr> self; // 自己这个类型
 
    Node* _node; // 节点指针
 
    // 构造函数
    __list_iterator(Node* node) // 用节点指针来构造
      :_node(node)
    {}
 
    // 让迭代器具有像指针一样的行为
    Ref operator*() // *it 本质是:it.operator*()
    {
      return _node->_val; // 返回节点中数据(对象)的引用/const常引用,具体返回什么取决于在list类域中定义迭代器时传给Ref的参数是什么
             // 我们想要得到的是节点中存储的数据(对象),而不是整个节点(节点中包含数据和2个指针)
    }
    Ptr operator->() // it-> 本质是:it.operator->()
    {
      return &(operator*()); // 返回节点中数据(对象)的指针/const常指针,具体返回什么取决于在list类域中定义迭代器时传给Ptr的参数是什么
    }
 
    // 让迭代器可以移动(双向)
        // ++it 本质是:it.operator++()
    Self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
        // it++ 本质是:it.operator++(0)
    Self operator++(int) // 不能用引用返回
    {
      Self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
        // --it 本质是:it.operator--()
    Self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
        // it-- 本质是:it.operator--(0)
    Self operator--(int) // 不能用引用返回
    {
      Self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
 
    // 让两个迭代器可以进行比较,判断两个迭代器是否相等,其实判断的是节点指针是否相等
    bool operator!=(const Self& l)const
    {
      return _node != l._node;
    }
    bool operator==(const Self& l)const
    {
      return _node != l._node;
    }
  };
}

迭代器有两种实现方式,具体应根据容器底层数据结构实现:

  1. 原生态指针比如:vector。
  2. 将原生态指针进行封装,因迭代器使用形式与指针完全相同。

因此在自定义的类中必须实现以下方法:

  1. 指针可以解引用,迭代器的类中必须重载 operator*()
  2. 指针可以通过 -> 访问其所指空间成员,迭代器类中必须重载 oprator->()
  3. 指针可以 ++ 向后移动,迭代器类中必须重载 operator++() 与 operator++(int)
  4. 至于operator--() / operator--(int) 释放需要重载,根据具体的结构来抉择,双向链表可以向前移动,所以需要重载,如果是 forward_list 就不需要重载 -- 。
  5. 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()

(1)如何实现 const 迭代器?

string 和 vector 的迭代器就是一个原生指针,我们给原生指针加上 const 就可以变成 const 迭代器,比如:

// T: 容器中存储的数据的类型
typedef T* iterator;
typedef const T* const_iterator;

但 list 的迭代器不是一个原生指针,而是一个类里面封装了原生指针,那如何实现 list 的 const 迭代器呢?

const 迭代器,顾名思义,就是不能改变的迭代器,是不能通过 * 和 -> 修改指向成员的,所以我们把 operator*() 和 operator->() 函数的返回值改成常引用即可。比如:

const T& operator*() ; // 返回节点中数据(对象)的const常引用

那我们需要实现一个 __list_const_iterator 的类吗?在类中重载一个返回值为常引用的 operator*() 函数。

普通写法需要这样,因为 T& operator*(); 和 const T& operator*(); 这两个函数只是返回值不同,无法放在一个类里面构成重载。

SGI 版本 stl_list 源码中是怎么样实现的:

通过增加模板参数来控制,普通迭代器传过来的参数就是 T&,const 迭代器传过来的参数就是 const T&。


(2)迭代器遍历 list 中节点的方式是什么?

如果迭代器管理的节点中的数据是内置类型

void test()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
 
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        // 对指向当前节点的迭代器解引用 *it 可以得到当前节点中存储的数据
        cout << *it << " "; // it.operator*()
        ++it;
    }
 
}

如果迭代器管理的节点中的数据是自定义类型 / 结构体

struct TreeNode
{
    int _val;
    struct TreeNode* _left;
    struct TreeNode* _right;
 
    TreeNode(int val = -1)
    :_val(val)
    ,_left(nullptr)
    ,_right(nullptr)
  {}
};
 
void test()
{
    list<TreeNode> lt;
    lt.push_back(TreeNode(1));
    lt.push_back(TreeNode(2));
    lt.push_back(TreeNode(3));
 
    list<TreeNode>::iterator it = lt.begin();
    while (it != lt.end())
    {
        /* 错误写法:
    * 对指向当前节点的迭代器解引用*it可以得到当前节点中存储的TreeNode结构体
    * TreeNode结构体中存储的数据_val是不能直接输出出来的
    * 除非重载了专门针对输出TreeNode结构体中数据的流插入运算符,比如:ostream& operator<<(ostream& out, const TreeNode node);
    */
        // cout << *it << " "; // error!!!
 
        /* 正确写法1:
    * 对指向当前节点的迭代器解引用*it得到当前节点中存储的TreeNode结构体
    * 然后用'.'再去访问 TreeNode 结构体中存储的数据_val
    */
        cout << (*it)._val << " ";
 
        /* 正确写法2:
    * 调用 operator->() 函数
    * 迭代器->,返回迭代器指向节点中存储的TreeNode结构体的地址(指针):TreeNode*
    * 然后该指针再使用'->'访问结构体中存储的数据_val
    * 代码为:it->->_val,但可读性太差,编译器进行了特殊处理,省略掉了一个箭头,保持了程序的可读性
    */
        // 注意:一般结构体的指针才会使用'->'来访问成员
        // 当迭代器管理的节点中的数据是结构体的时候,就可以用'->'
        cout << it->_val << " "; // 这种写法常用
        it++;
    }
}

(3)迭代器( __list_iterator 类)中需要写拷贝构造、赋值重载、析构函数吗?

不需要,默认生成的就可以,而且也不需要我们去实现。虽然迭代器中封装了节点指针,但节点是归链表管理的,链表 list 中的节点也不需要迭代器去释放,list 自己会去释放。


(4) vector 和 list 的迭代器哪个更复杂呢?有什么区别呢?

它们的大小都是一样的(32 位下),vector 的迭代器是一个原生指针,大小为 4 字节;list 的迭代器是一个类封装了一个节点指针,大小也是 4 字节,物理上没什么区别。

list 的迭代器之所以要定义成自定义类型,是因为原生指针无法直接做到像指针一样的行为,所以需要用自定义的类把原生指针封装起来,然后在类中定义一些运算符重载函数,使得这个自定义类型的对象通过调用这些函数,从而可以做到像指针一样的行为。

我们在物理上可以看到,都是存了一个指针,但在运行逻辑上完全不同。

  • vector 迭代器 ++,是一个指针直接 ++ 到下一个位置;
  • list 迭代器 ++,是一个自定义类型的对象调用了 operator++() 函数,在函数内算出下一个位置。

3、list 的底层结构

带头双向循环链表结构:

// 带头双向循环链表结构
namespace xyl
{
  template<class T> // T: 节点中数据的类型
  class list
  {
  public:
    typedef __list_node<T> Node; // 节点
 
  public:
    // 迭代器
    // iterator是内嵌类型,在list类域里面定义的类型(类中类)
    // 为啥要typedef呢?为了统一规范名称,所有容器迭代器都叫iterator,用起来更方便
        // 普通迭代器,指明模板参数为 T, T&, T*
        typedef __list_iterator<T, T&, T*> iterator;
        
        // const迭代器,指明模板参数为 T, const T&, const T*
        typedef __list_iterator<T, const T&, const T*> const_iterator;
 
    iterator begin(); // 返回指向第一个有效节点的迭代器
    iterator end();   // 返回指向头节点的迭代器
    const_iterator begin() const;
    const_iterator end() const;
 
  private:
    Node* _head; // 头节点指针
 
  public:
    // 默认成员函数
    list(); // 默认构造函数
    template<class InputIterator>
    list(InputIterator first, InputIterator last); // 带参构造函数
    list(const list<T>& lt); // 拷贝构造函数(深拷贝)
    list<T>& operator=(list<T> lt) // 赋值运算符重载函数(深拷贝)
    ~list(); // 析构函数
 
    // 容量操作
    size_t size(); // 有效元素个数
    bool empty(); // 判空
 
    // 修改操作
    iterator insert(iterator pos, const T& data); // 指定迭代器位置之前插入元素
    iterator erase(iterator pos); // 删除指定迭代器位置的元素
    void push_back(const T& data);  // 尾插
    void push_front(const T& data); // 头插
    void pop_back();  // 尾删
    void pop_front(); // 头删
    void clear(); // 清空有效元素
  };
}

注意:list 类域中的 __list_iterator 和 __list_iterator 是定义在类内部的类(嵌套类型)。

用 typedef 重命名为 iterator 和 const_iterator ,目的是为了统一规范迭代器的名称,所有容器的迭代器都叫 iterator 等,用起来更方便。否则每个容器都定义迭代器,如果每个容器迭代器的名字都不一样,那用户的使用成本就太高了。

// 普通迭代器,指明模板参数为 T, T&, T*
typedef __list_iterator<T, T&, T*> iterator;
 
// const迭代器,指明模板参数为 T, const T&, const T*
typedef __list_iterator<T, const T&, const T*> const_iterator;

其中 T 、 T& 、 T* 都是类型参数。当 list 的类型确定了,T 、 T& 、 T* 的类型自动确定,那么 iterator 的类型同时也自动确定了,这看起来就像 iterator 和 list 是共生的一样。有什么样的螺丝就需要什么样的螺丝刀,否则螺丝刀再漂亮,没有对号的螺丝也是没用的。


4、list 的一些成员函数

(1)默认成员函数
a. 构造函数
// 默认构造函数
list()
{
    // 构造头节点,自己指向自己
    _head = new Node;
    _head->_prev = _head;
    _head->_next = _head;
}
 
// 用迭代器区间初始化[first,last)
template<class InputIterator>
list(InputIterator first, InputIterator last)
    :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
{
  // 建立头节点自身的链接
  _head->_prev = _head;
  _head->_next = _head;
        
    // 用迭代器遍历元素,尾插到新容器中
  while (first != last)
  {
    push_back(*first);
    first++;
  }
}

b. 拷贝构造函数(传统写法和现代写法都可以)
//拷贝构造函数(深拷贝--传统写法)
// lt2(lt1)
list(const list<T>& lt)
    :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
{
    // 建立头节点自身的链接
    _head->_prev = _head;
    _head->_next = _head;
 
    // 把lt中所有节点拷贝插入到新容器中
    for (const auto& e : lt)
    {
      push_back(e);
    }
}
 
// 拷贝构造函数(深拷贝--现代写法)
// lt2(lt1)
list(const list<T>& lt)
    :_head(new Node) // 当前对象是一个正在构造的对象,成员变量是随机值,需要进行初始化
{
    // 建立头节点自身的链接
    _head->_prev = _head;
    _head->_next = _head;
 
  list<T> tmp(lt.begin(), lt.end());
  std::swap(_head, tmp._head); // 交换两个容器的内容
}

c. 赋值运算符重载函数(深拷贝——传统写法)
list<T>& operator=(const list<T>& lt)
{
    if (this != &lt) // 防止自己给自己赋值
    {
        // 清除当前容器所有有效元素
        clear();
        // 把lt中所有节点拷贝插入到当前容器中
        for (const auto& e : lt)
        {
            push_back(e);
        }
    }
    return *this; // 返回当前容器
}

d. 赋值运算符重载函数(深拷贝——现代写法)
list<T>& operator=(list<T> lt) // 传值传参,拷贝构造出临时对象
{
    std::swap(_head, lt._head); // 交换两个容器的内容
    return *this; // 返回当前容器
}

e. 析构函数
~list()
{
  /*
    Node* cur = _head->_next; // 从第一个有效节点开始删除
    while (cur != _head) 
    {
        Node* next = cur->_next; // 先记录下一个节点
        delete cur; // 释放当前节点
        cur = next; // 遍历下一个节点
    }
    delete _head; // 释放头节点
    _head = nullptr;
  */
 
    clear();
    delete _head;
    _head = nullptr;
}

(2)容量操作
a. size
// O(N)
size_t size()
{
    size_t n = 0; // 遍历整个链表,统计有效元素个数
    iterator it = begin();
    while (it != end())
    {
        n++;
        it++;
    }
    return n;
}

b. empty
bool empty()
{
    return begin() == end();
}

(3)修改操作
a.  insert

iterator insert(iterator pos, const T& data)
{
    Node* cur = pos._node;   // 当前节点(pos中封装了节点的指针)
    Node* prev = cur->_prev; // 前驱节点
    Node* newnode = new Node(data); // 新节点
 
    // 建立前驱节点和新节点的链接
    prev->_next = newnode;
    newnode->_prev = prev;
 
    // 建立新节点与当前节点的链接
    newnode->_next = cur;
    cur->_prev = newnode;
 
    // 返回指向第一个新插入元素的迭代器
    return iterator(newnode); // 注意:此处调用iterator的构造函数构造出一个匿名对象
    // return newnode; // 也可以这样写,因为单参数的构造函数支持隐式类型的转换
}

b. erase
iterator erase(iterator pos)
{
    assert(pos != end()); // 不能删除头节点
    Node* cur = pos._node;   // 当前节点
    Node* prev = cur->_prev; // 前驱节点
    Node* next = cur->_next; // 后继节点
 
    // 建立前驱节点与后继节点的链接
    prev->_next = next;
    next->_prev = prev;
 
    // 删除当前节点
    delete cur;
 
    // 返回指向被删除节点的下一个节点的迭代器
    return iterator(next); // 注意:此处调用iterator的构造函数构造出一个匿名对象
}

c. push_back 和 push_front
void push_back(const T& data) // 尾插
{
  /*
    Node* newNode = new Node(data); // 构造新节点
    Node* tail = _head->_prev; // 尾节点
    // 建立尾节点和新节点的链接
    tail->_next = newNode;
    newNode->_prev = tail;
    // 建立新节点和头节点的链接
    newNode->_next = _head;
    _head->_prev = newNode;
  */
    
    insert(end(), data); // 在头节点的前面插入,相当于是尾插
}
 
void push_front(const T& data) // 头插
{
    insert(begin(), data); // 在第一个有效节点前面插入,相当于是头插
}

d. pop_back 和 pop_front
void pop_back() // 尾删
{
    erase(--end()); 
}
void pop_front() // 头删
{
    erase(begin()); 
}

e. clear
void clear()
{
  /*
    iterator it = begin();
    while (it != end())
    {
        Node* cur = it._node; // 记录当前节点
        it++;                 // 遍历下一个节点
        delete cur;           // 删除当前节点
    }
    // 建立头节点自身的链接
    _head->_next = _head;
    _head->_prev = _head;
  */
 
    iterator it = begin();
    while (it != end())
    {
        it = erase(it); // 删除当前节点,返回指向下一个节点的迭代器
    }
}

【补充】 类模板成员函数的实例化

编译器进行类模板推演,实例化出一个 list 类,然后再实例化出这个 lt 对象。

  • 类模板中的成员函数都是函数模板。
  • 类模板成员函数的实例化,是按需实例化,只有当程序用到它时才进行实例化。
  • 一个模板,如果没有实例化,编译器是不会去检查它内部的语法的,也不会报错。
void test()
{
    list<int> lt;
    lt.push_back(1); 
    lt.push_back(2);
    lt.push_back(3);
    lt.clear();
}

四、listvector的对比

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


相关文章
|
12天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
28 7
|
21天前
|
存储 编译器 C++
C++ initializer_list&&类型推导
在 C++ 中,`initializer_list` 提供了一种方便的方式来初始化容器和传递参数,而右值引用则是实现高效资源管理和移动语义的关键特性。尽管在实际应用中 `initializer_list&&` 并不常见,但理解其类型推导和使用方式有助于深入掌握现代 C++ 的高级特性。
16 4
|
2月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
67 2
|
2月前
|
存储 算法 C++
【C++打怪之路Lv10】-- list
【C++打怪之路Lv10】-- list
23 1
|
2月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
73 5
|
2月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
84 2
|
2月前
|
C++
【C++】C++ STL 探索:List使用与背后底层逻辑(三)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
2月前
|
C++
【C++】C++ STL 探索:List使用与背后底层逻辑(二)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
2月前
|
存储 编译器 C++
【C++】C++ STL 探索:List使用与背后底层逻辑(一)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
2月前
|
存储 缓存 C++
C++番外篇——list与vector的比较
C++番外篇——list与vector的比较
28 0