【C++】STL之list模拟实现:关于链表容器的双向迭代器你知道多少?

【C++】STL之list模拟实现:关于链表容器的双向迭代器你知道多少?

前言:

前面的博客中我已经介绍了STL核心容器之一的list相关接口的使用,今天我们就从底层出发,来模拟实现一下list的那些核心接口函数。同时,也来感受一下list的双向迭代器到底与string和vector的随机迭代器有哪些区别?

list容器功能接口介绍:https://blog.ZEEKLOG.net/Miun123/article/details/151685386?spm=1001.2014.3001.5502

废话不多说,我们直接进入今天的正题👇️👇️👇️



list容器深度剖析及模拟实现

我们想要模拟实现list容器,那就要理解list容器的底层结构。前面的博客已经提到,其本质就是一个双向链表,所以,成员变量就应该包含一个记录头节点的指针,以及记录有效节点个数的变量。同时,为了list容器可以满足不同类型的数据,我们将所有的类实现为类模板。

1、定义节点结构

struct创建的类默认所有的成员但是公开的,而节点结构就需要公开被list访问。
template<class T> struct list_node { // 成员变量 T _data; list_node<T>* _next; list_node<T>* _prev; // 默认构造 list_node(const T& val = T()) :_data(val) , _next(nullptr) , _prev(nullptr) {} };

2、双向迭代器

我们先看一段 slt_list.h头文件 中实现list迭代器的源码:(注释是本人自己加的)

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; // const迭代器 typedef __list_iterator<T, Ref, Ptr> self; typedef bidirectional_iterator_tag iterator_category; typedef T value_type; // 数据类型 typedef Ptr pointer; // 指针类型 typedef Ref reference; // 引用类型 typedef __list_node<T>* link_type; // 节点类型 typedef size_t size_type; typedef ptrdiff_t difference_type; link_type node; // 成员变量 __list_iterator(link_type x) : node(x) {} // 拷贝构造 __list_iterator() {} // 默认构造 __list_iterator(const iterator& x) : node(x.node) {} // 拷贝构造 bool operator==(const self& x) const { return node == x.node; } // 重载== bool operator!=(const self& x) const { return node != x.node; } // 重载!= reference operator*() const { return (*node).data; } // 重载————解引用* #ifndef __SGI_STL_NO_ARROW_OPERATOR pointer operator->() const { return &(operator*()); } // 重载-> #endif /* __SGI_STL_NO_ARROW_OPERATOR */ self& operator++() { //重载前置++ node = (link_type)((*node).next); return *this; } self operator++(int) { // 重载后置++ self tmp = *this; ++*this; return tmp; } self& operator--() { // 重载前置-- node = (link_type)((*node).prev); return *this; } self operator--(int) { // 重载后置-- self tmp = *this; --*this; return tmp; } };

和前面string和vector的迭代器不同,list的迭代器不仅自己封装成了一个单独的类模板,而且这个类模板的模板参数有三个。这是为什么呢❓️

我们知道,对于const对象和非const对象,如果将模板参数只有一个<class T>,那么我们势必就要实现两个迭代器的类模板。而这两个类模板中,只有解引符号重载函数和箭头 ->重载函数由于返回值与其对应的类型有关(因为对于const对象,我们无法通过解引用和->改变const对象的值)而实现方式稍有不同;对于其他函数,在两个类模板中就重复了。所以,我们为了避免这种情况,就在类模板中引入了另外两个参数:Ref(T& / const T&——解引用操作符函数的返回值类型),     Ptr(T* / const T*—— ->重载函数的返回值类型)。

可以看到,在源码中typedef用得非常多,接下来我们就自己实现一个迭代器的类模板出来:其中++,--,==,!=,*,->这些操作符都是对节点的操作,所以,我们迭代器的类模板中应该有一个记录节点的成员变量_node。

template<class T, class Ref,class Ptr> struct list_iterator { typedef list_node<T> Node; typedef list_iterator<T, Ref, Ptr> Self; // Ref--T& / const T& ; Ptr--T* / const T* Node* _node; // 成员变量 list_iterator(Node* node) // 拷贝构造 :_node(node) {} // ... };
2.1、解引用
Ref& operator*()const { return _node->_data; }

2.2、->

Ptr operator->()const { return &(_node->_data); }
2.3、前置--  后置--
// 前置-- Self& operator--() { _node = _node->_prev; return *this; } // 后置-- Self operator--(int) { list_iterator<T> tmp(*this); _node = _node->_prev; return tmp; }
2.4、后置++  前置++
// 后置++ Self operator++(int) { Self tmp(*this); _node = _node->_next; return tmp; } // 前置++ Self& operator++() { _node = _node->_next; return *this; }
2.5、== 和 !=
bool operator==(const Self& s)const { return _node == s._node; } bool operator!=(const Self& s)const { return _node != s._node; }

3、list容器功能接口

template<class T> class list { typedef list_node<T> Node; public: typedef list_iterator<T, T&, T*> iterator; // 普通迭代器 typedef list_iterator<T, const T&, const T*> const_iterator;// const迭代器 // ... private: Node* _head; // 头节点 size_t _size;// 有效节点个数 };
3.1、链表初始化
3.1.1、构造空链表

即创建头节点(哨兵位),并初始化

void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; _size = 0; }
3.1.2、默认构造
list(){ empty_init(); }
3.1.3、拷贝构造
list(const list<T>& lt) { // 由于拷贝无法完成头节点初始化,所以先初始化头节点 empty_init(); for (auto e : lt) push_back(e); }
3.1.4、初始化列表
list(initializer_list<int> il) { empty_init(); for (auto& e : il) push_back(e); }
3.1.5、赋值重载
void swap(list<T>& lt) { std::swap(_head, lt._head); std::swap(_size, lt._size); } list<T>& operator=(list<T> lt) { swap(lt); return *this; }
3.2、析构
~list() { clear(); // 清理所有节点 delete _head; // 释放头节点 _head = nullptr; }
3.3、迭代器

迭代器即可以理解为指针,我们用迭代器记录第一个有效节点和尾节点的位置。

iterator begin() { return _head->_next; } iterator end() { return _head; } const_iterator begin()const { return _head->_next; } const_iterator end()const { return _head; }
3.4、插入

先找到指定位置之后的节点:pos->next;然后。连接pos节点,新节点newnode与指定位置之后的节点pos->next。注意:连接顺序,防止改变pos->next的指向。

3.4.1、指定位置插入
iterator insert(iterator pos, const T& x) { Node* newnode = new Node(x); // 构造新节点 Node* pcur = pos._node; Node* prev = pos._node->_prev; // 连接:prev newnode pcur prev->_next = newnode; newnode->_prev = prev; newnode->_next = pcur; pcur->_prev = newnode; ++_size; // 有效节点个数加1 return pos; // 返回pos节点 }
3.4.2、头插
void push_front(const T& x) { insert(begin(), x); }
3.4.3、尾插
void push_back(const T& x) { insert(end(), x); }
3.5、删除节点

现在的指定位置节点之前的节点:pos->prev;指定位置之后的节点:pos->next。然后,连接pos->prev与pos->next。最后,释放指定位置节点。

3.5.1、删除指定位置节点
iterator erase(iterator pos) { assert(pos != end()); Node* prev = pos._node->_prev; Node* next = pos._node->_next; // 连接 prev next prev->_next = next; next->_prev = prev; // 释放 delete pos._node; pos._node = nullptr; --_size; return next; // 返回pos节点的下一个节点 }
3.5.2、头删
void pop_front() { erase(begin()); }
3.5.3、尾删
void pop_back() { erase(--end()); }
3.5.4、链表的清理
void clear() { auto it = begin(); while (it != end()) { it = erase(it); } }
3.6、获取节点个数
size_t size() const { return _size; }
3.8、判空
bool empty() const { return _size == 0; }

4、总结

那么,本期的分享就到此结束,如果大家觉得写的还不错的话,点个小爱心❤️支持一下吧😘😘😘,我们下期再见🤗🤗🤗

Read more

Python 属性描述符:从原理到 ORM 实践详解

Python 属性描述符:从原理到 ORM 实践详解

Python 属性描述符:从原理到 ORM 实践详解 * 一、为什么需要属性描述符?从property的局限性说起 * 二、属性描述符的定义与基础使用 * 2.1 什么是属性描述符? * 2.2 基础实现:整数类型校验描述符 * 2.3 在模型类中使用描述符 * 2.4 关键注意点:避免赋值死循环 * 三、属性描述符的分类:数据描述符与非数据描述符 * 3.1 数据描述符(Data Descriptor) * 3.2 非数据描述符(Non-data Descriptor) * 四、Python完整的属性查找过程:描述符的核心作用 * 4.1 核心查找顺序 * 4.2 关键验证:数据描述符覆盖实例属性 * 4.3 关键验证:

By Ne0inhk

Bug 算法路径规划实战:从数学建模到 Python 实现

1. 从“撞墙”到“绕行”:Bug算法的直觉理解 想象一下,你被蒙上眼睛,站在一个空旷的房间里,有人告诉你:“向前走十步,就能拿到桌子上的苹果。”你开始径直向前走。走了五步,你的膝盖“砰”地一声撞到了什么东西——是一把椅子。这时候你会怎么做?你肯定不会继续硬着头皮往前撞,而是会伸出手,摸着这把椅子的边缘,小心翼翼地绕着它走,直到你感觉前方没有阻碍了,再重新判断苹果的方向,继续前进。 这个“撞到就绕”的朴素策略,就是Bug算法最核心的思想。在机器人路径规划领域,Bug算法就是这样一种简单、直接、不需要“上帝视角”地图的局部规划方法。它不关心整个房间的布局,只关心“我”现在在哪里,“目标”在哪里,以及“我”眼前有没有障碍物。这种特性让它特别适合用在未知环境探索、实时避障以及计算资源有限的场景里,比如你家里的扫地机器人,或者在一个陌生仓库里穿梭的物流小车。 我刚开始接触路径规划时,

By Ne0inhk

C++常用设计模式

前言:C++代码设计模式总共有23种,分为创建型模式、结构型模式和行为模式。本文针对常用的单例模式、简单工厂模式、工厂方法模式和抽象工厂模式进行介绍。 目录 一、为什么需要设计模式 二、单例模式 三、简单工厂模式 四、工厂方法模式 五、抽象工厂模式 一、为什么需要设计模式 C++ 需要设计模式,核心是解决其面向对象编程(OOP)的痛点、适配底层开发场景,同时弥补语言原生特性的不足。 设计模式为 C++ 提供了 “标准化的解决方案”,让代码从 “随意写” 变成 “按规则写”,提升了C++代码的工程化能力。 二、单例模式 1)定义:保证一个类仅有一个实例,并对外提供一个全局访问点来访问这个实例 2)目的:解决 “对象重复创建” 问题,尤其适用于管理唯一的硬件资源(

By Ne0inhk
【C++】stack与queue的使用与模拟实现

【C++】stack与queue的使用与模拟实现

【C++】stack与queue的使用与模拟实现 ✨前言:在C++ STL中,stack和queue是两个重要的数据结构组件,它们虽然简单却非常实用。与vector、list等容器不同,它们属于容器适配器,通过封装现有容器并提供特定接口来实现栈和队列的功能。接下来,让我们一起学习一下吧!!! 📖专栏:【C++成长之旅】 目录 * 【C++】stack与queue的使用与模拟实现 * 一、stack与queue使用 * 二、stack模拟实现 * 三、queue模拟实现 说在前面 对于stack与queue的学习更为简单,但是它们与前面的string、vector……还是有本质区别,string是容器(container ),stack与queue是容器适配器(container adaptor): 是因为它在现有容器的基础上,通过限制功能、改变接口,来“适配”出栈这种特定的数据结构。 也就是说,它的底层,可以是已有的容器,比如:list、vector…… 关于容器适配器我会在下篇博客中来详细阐述,敬请期待,现在让我们来学习stack与queue

By Ne0inhk