从C语言到C++_17(list的模拟实现)list不是原生指针的迭代器(上)

简介: 从C语言到C++_17(list的模拟实现)list不是原生指针的迭代器

上一篇说到,list 其实就是带哨兵位循环双向链表而已,这种链表虽然结构复杂,

但是实现起来反而是最简单的,我们在数据结构与算法专栏中有过详细的讲解:

数据结构与算法⑦(第二章收尾)带头双向循环链表的实现_GR C的博客-CSDN博客

当时我们是用C语言实现,这里对 list 的实现其实也是大同小异的。

当然,我们重点还是倾向于去理解它的底层实现原理,

所以我们将对其实现方式进行进一步地简化,并且按照我们自己习惯的命名风格去走。

我们之前已经模拟实现过 string 和 vector 了,这是本专栏 STL 的第三个模拟实现,

因此在讲解的时,出现重复的知识点我们就一笔带过。我们将重点去讲解迭代器的实现!

本章我们要对迭代器有一个新的认知,迭代器不一定就是一个原生指针,

也有可能是一个自定义类型。

本章我们将通过自定义类型的运算符重载去控制我们的迭代器的 "行为"。


1. list 的基本框架

我们还是参考《STL源码剖析》,既然是要实现链表,我们首先要做的应该是建构结点。

此外,为了和真正的 list 进行区分,我们这里仍然在自己的命名空间内实现。

1.1 list 的结点

C语言写的:

C++的代码:

  template<class T>
  struct list_node
  {
    T _data;
    list_node* _prev;
    list_node* _next;
  };

C++中对象里的成员如果全是共有的还是比较习惯用 struct 的

我们知道,结构体 struct 在 C++ 中升级成了类,因此它也有调用构造函数的权利。

也就是说,在创建结构体对象的时会调用构造函数。

既然如此,结点的初始化工作,可以考虑写一个构造函数去初始化:

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

设计成全缺省,给一个匿名对象 T() 。如此一来,如果没有指定初识值,

它就会按模板类型去给对应的初始值了。

1.2 list 构造函数

设计好结点后,我们现在可以开始实现 list 类了。

考虑到我们刚才实现的 "结点" ListNode 类型比较长,为了美观我们将其 typedef 成 Node

因为是带头(哨兵位)双向循环链表,我们先要带个头。

我们先要把头结点 _pHead 给设计出来,而 _prev 和 _next 是默认指向头结点的。

到这里 list.h 就是这样:

#pragma once
 
#include<iostream>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  template<class T>
  struct list_node // 定义结点
  {
    T _data;
    list_node* _prev;
    list_node* _next;
 
    list_node(const T& x = T())
      :_data(x)
      ,_prev(nullptr)
      ,_next(nullptr)
    {}
  };
 
  template<class T>
  class list // 定义list类
  {
    typedef list_node<T> Node;
  public:
    list()
    {
      _head = new Node;
      _head->_prev = _head;
      _head->_next = _head;
    }
 
  private:
    Node* _head; // 哨兵位头结点
  };
}

1.3 push_back

我们先去实现一下最经典的 push_back 尾插,好让我们的 list 先跑起来。

尾插思路和以前写过的思路一样,后面很多接口也是,不懂的回去看啊,别逼我求你,

数据结构与算法⑦(第二章收尾)带头双向循环链表的实现_GR C的博客-CSDN博客

直接放代码了:

    void push_back(const T& x)
    {
      Node* tail = _head->_prev;
      Node* NewNode = new Node(x);
      // 思路图:head        tail  NewNode
      tail->_next = NewNode;
      NewNode->_prev = tail;
      _head->_prev = NewNode;
      NewNode->_next = _head;
    }

我们想要打印的话就只能自己实现迭代器了:

2. list 迭代器的实现

list 的重点是迭代器,因为这里的迭代器的实现和我们之前讲的实现方式都不同。


我们之前讲的 string 和 vector 的迭代器都是一个原生指针,实现起来是非常简单的。


但是 list 是一个链表,你的迭代器还能这样去实现吗?在空间上不是连续的,如何往后走?


而这些所谓的 "链接" 其实都是我们想象出来的,实际上根本就不存在。


而这些链接的含义只是 "我存的就是你的地址" ,所以我可以找到你的位置。


而我要到下一个位置的重点是 —— 解引用能取到数据,++ 移动到下一位。


而自带的 解引用* 和 ++ 的功能,是没法在链表中操作的。


但是,得益于C++有运算符重载的功能,我们可以用一个类型去对结点的指针进行封装,


然后重载运算符 operator++ 和 operator* ,


是不是就可以控制其解引用并 ++ 到下一个位置了?


所以,我们首先要做的是对这两个运算符进行重载:

2.1 迭代器的构造

代码:只需要用一个结点的指针

  template<class T>
  struct __list_iterator // 定义迭代器
  {
    typedef list_node<T> Node;
        typedef __list_iterator<T> iterator; // STL规定的命名,且公有
    Node* _node;
 
    iterator(Node* node)
      :_node(node)
    {}
  };

这里命名是参考源码的,__list_iterator 前面是两个下划线。

我们想要打印的话应该是这样的:

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

所以我们来实现这几个操作

2.2 begin() 和 end()

代码:在 list 类中设计 begin 和 end

  template<class T>
  class list // 定义list类
  {
    typedef list_node<T> Node;
  public:
    typedef __list_iterator<T> iterator; // STL规定的命名,且公有
 
    iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
    {
      return iterator(_head->_next);
    }
    iterator end()// end是实际数据的下一个
    {
      return iterator(_head);
    }
 
    list()
    {
      _head = new Node;
      _head->_prev = _head;
      _head->_next = _head;
    }
 
    void push_back(const T& x)
    {
      Node* tail = _head->_prev;
      Node* NewNode = new Node(x);
      // 思路图:head        tail  NewNode
      tail->_next = NewNode;
      NewNode->_prev = tail;
      _head->_prev = NewNode;
      NewNode->_next = _head;
    }
 
  private:
    Node* _head; // 哨兵位头结点
  };

2.3  重载 != 和 * 和 ++

operator!=

如何判断是否相等呢?

如果两个迭代器结点的指针指向的是同一个结点,那就说明是相等的迭代器:

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

operator*

解引用就是取结点 _node 里的数据,并且 operator* 和指针一样,不仅仅能读数据,还能写数据。

为了使 operator* 能支持修改的操作,我们这里用引用返回 & (返回 _node 中 _data 的别名)

    T& operator*()
    {
      return _node->_data;  // 返回结点的数据
    }

operator++

加加分为前置和后置,我们这里先实现前置++:

    iterator& operator++()
    {
      _node = _node->_next;
      return *this; // 返回加加后的值
    }

因为前置是直接改变本体,我们直接 return *this 即可。

因为出了作用域还在,所以可以用引用返回, __list_iterator&

对应的,后置++ 我们可以拷贝构造出一个 tmp 存储原来的值,这样虽然改变本体了,

但是返回的还是之前的值,这就实现了后置++。此外,因为前置++后置++都是 operator++,

区分方式是后置++用占位符 (int) 占位,这些知识点在之前讲解日期类的时候都说过。

后置++的实现:(注意后置++不能用引用返回)

    iterator operator++(int)
    {
      __list_iterator<T> tmp(*this); // 拷贝构造一个tmp存储原来的值
      _node = _node->next;
      return tmp; // 返回加加前的值
    }

2.4 遍历测试:

至此,可以遍历打印我们的代码了,而且范围for也能用了:

list.h

#pragma once
 
#include<iostream>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  template<class T>
  struct list_node // 定义结点
  {
    T _data;
    list_node* _prev;
    list_node* _next;
 
    list_node(const T& x = T())
      :_data(x)
      ,_prev(nullptr)
      ,_next(nullptr)
    {}
  };
 
  template<class T>
  struct __list_iterator // 定义迭代器
  {
    typedef list_node<T> Node;
        typedef __list_iterator<T> iterator; // STL规定的命名,且公有
    Node* _node;
 
    __list_iterator(Node* node)
      :_node(node)
    {}
 
    bool operator!=(const iterator& it) 
    {
      return _node != it._node;
    }
    T& operator*()
    {
      return _node->_data;  // 返回结点的数据
    }
    iterator& operator++()
    {
      _node = _node->_next;
      return *this; // 返回加加后的值
    }
    iterator& operator++(int)
    {
      iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
      _node = _node->next;
      return tmp; // 返回加加后的值
    }
  };
 
  template<class T>
  class list // 定义list类
  {
    typedef list_node<T> Node;
  public:
    typedef __list_iterator<T> iterator; // STL规定的命名,且公有
 
    iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
    {
      return iterator(_head->_next);
    }
    iterator end()// end是实际数据的下一个
    {
      return iterator(_head);
    }
 
    list()
    {
      _head = new Node;
      _head->_prev = _head;
      _head->_next = _head;
    }
 
    void push_back(const T& x)
    {
      Node* tail = _head->_prev;
      Node* NewNode = new Node(x);
      // 思路图:head        tail  NewNode
      tail->_next = NewNode;
      NewNode->_prev = tail;
      _head->_prev = NewNode;
      NewNode->_next = _head;
    }
 
  private:
    Node* _head; // 哨兵位头结点
  };
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
 
#include "list.h"
 
namespace rtx
{
  void Test_push_back()
  {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_back(5);
 
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
      cout << *it << " ";
      ++it;
    }
    cout << endl;
 
    for (const auto& e : lt)
    {
      cout << e << " ";
    }
    cout << endl;
  }
}
 
int main()
{
  rtx::Test_push_back();
 
  return 0;
}


2.6 operator->

迭代器是像指针一样的,所以要重载两个解引用。

为什么?指针如果指向的类型是原生的普通类型,要取对象是可以用解引用,

但是如果指向而是一个结构,并且我们又要取它的每一个成员变量,

比如我们想打印坐标:

  void Test_arrow()
  {
    struct Pos
    {
      int _a1;
      int _a2;
 
      Pos(int a1 = 0, int a2 = 0)
        :_a1(a1)
        , _a2(a2)
      {}
    };
    Pos aa;
    Pos* p2 = &aa;
    p2->_a1;
    p2->_a2;
 
    list<Pos> lt;
    lt.push_back(Pos(10, 20));
    lt.push_back(Pos(10, 21));
 
    list<Pos>::iterator it = lt.begin();
    while (it != lt.end())
    {
      cout << (*it)._a1 << "," << (*it)._a2 << endl;
      //cout << it->_a1 << "," << it->_a2 << endl;
      ++it;
    }
    cout << endl;
  }

虽然能用解引用+点操作符,但用箭头还是方便的,而且你模拟实现总不能不给别人用吧,

所以我们这里可以去实现一下箭头操作符 operator->,如果不是很熟练应该是不会的。

我们直接看一下源代码是怎么实现的,抄下来用用然后思考下:

    T& operator*()
    {
      return _node->_data;  // 返回结点的数据
    }
    T* operator->()
    {
      return &(operator*());
      //即 return &(_node->_data);
    }
  void Test_arrow()
  {
    struct Pos
    {
      int _a1;
      int _a2;
 
      Pos(int a1 = 0, int a2 = 0)
        :_a1(a1)
        , _a2(a2)
      {}
    };
    Pos aa;
    Pos* p2 = &aa;
    p2->_a1;
    p2->_a2;
 
    list<Pos> lt;
    lt.push_back(Pos(10, 20));
    lt.push_back(Pos(10, 21));
 
    list<Pos>::iterator it = lt.begin();
    while (it != lt.end())
    {
      //cout << (*it)._a1 << "," << (*it)._a2 << endl;
      cout << it->_a1 << "," << it->_a2 << endl;
        //实际要写,it->->_a1,但是编译器优化了一个箭头
      ++it;
    }
    cout << endl;
  }

第一个指针是operator->,第二个指针是原生指针的箭头,但是编译器为了可读性:

所有类型重载 operator-> 时都会省略一个箭头。

从C语言到C++_17(list的模拟实现)list不是原生指针的迭代器(中):https://developer.aliyun.com/article/1521342

目录
相关文章
|
2天前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
2天前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
2天前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
1天前
|
C++ 容器
【编程技巧】 C++11智能指针
C++11引入了智能指针以自动管理内存,防止内存泄漏和悬挂指针: - `shared_ptr`:引用计数,多所有权,适用于多个对象共享资源。 - `unique_ptr`:独占所有权,更轻量级,适用于单一对象所有者。 - `weak_ptr`:弱引用,不增加引用计数,解决`shared_ptr`循环引用问题。 ## shared_ptr - 支持引用计数,所有者共同负责资源释放。 - 创建方式:空指针、new操作、拷贝构造/移动构造,以及自定义删除器。 - 提供`operator*`和`operator-&gt;`,以及`reset`、`swap`等方法。 ## unique_ptr
|
2天前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
23小时前
|
C语言
C语言中的函数指针、指针函数与函数回调
C语言中的函数指针、指针函数与函数回调
5 0
|
23小时前
|
存储 C语言
C语言中的多级指针、指针数组与数组指针
C语言中的多级指针、指针数组与数组指针
5 0
|
1天前
|
存储 C语言
C语言数组指针详解与应用
C语言数组指针详解与应用
8 0
|
23小时前
|
存储 C语言
C语言中的指针
C语言中的指针
5 0
|
2天前
|
C++ 容器
【c++】优先级队列|反向迭代器(vector|list)
【c++】优先级队列|反向迭代器(vector|list)
5 0