从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

目录
相关文章
|
25天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
45 0
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
31 4
|
24天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
24天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
15 2
|
25天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
25天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
1月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
30天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
1月前
|
C语言
C语言指针(3)
C语言指针(3)
11 1