从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

目录
相关文章
|
3天前
|
存储 算法 C++
C++一分钟之-容器概览:vector, list, deque
【6月更文挑战第21天】STL中的`vector`是动态数组,适合随机访问,但插入删除非末尾元素较慢;`list`是双向链表,插入删除快但随机访问效率低;`deque`结合两者优点,支持快速双端操作。选择容器要考虑操作频率、内存占用和性能需求。注意预分配容量以减少`vector`的内存重分配,使用迭代器而非索引操作`list`,并利用`deque`的两端优势。理解容器内部机制和应用场景是优化C++程序的关键。
18 5
|
1天前
|
网络协议 C语言
C语言的函数指针数组的声明及应用场景
C语言的函数指针数组的声明及应用场景
|
2天前
|
C语言
C语言----深入理解指针(5)(一)
C语言----深入理解指针(5)
|
2天前
|
C语言
C语言---深入指针(4)(二)
C语言---深入指针(4)
11 2
|
2天前
|
C语言
C语言---深入指针(4)(一)
C语言---深入指针(4)
|
5天前
|
存储 算法 安全
C++一分钟之-数组与指针基础
【6月更文挑战第19天】在C++中,数组和指针是核心概念,数组是连续内存存储相同类型的数据,而指针是存储内存地址的变量。数组名等同于指向其首元素的常量指针。常见问题包括数组越界、尝试改变固定大小数组、不正确的指针算术以及忘记释放动态内存。使用动态分配和智能指针可避免这些问题。示例代码展示了安全访问和管理内存的方法,强调了实践的重要性。
23 3
|
2天前
|
C语言
C语言----深入理解指针(5)(二)
C语言----深入理解指针(5)
|
2天前
|
C语言
C语言----关于二维数组传参的本质相关的知识点(数组指针、指针数组)
C语言----关于二维数组传参的本质相关的知识点(数组指针、指针数组)
|
2天前
|
C语言
C语言--深入指针(1)二刷
C语言--深入指针(1)二刷
|
3天前
|
C语言
C语言----深入理解指针(3)(二)
C语言----深入理解指针(3)