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

前言:

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

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

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

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

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

1、定义节点结构

struct创建的类默认所有的成员但是公开的,而节点结构就需要公开被list访问。

cpp 复制代码
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迭代器的源码:(注释是本人自己加的)

cpp 复制代码
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。

cpp 复制代码
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、解引用
cpp 复制代码
Ref& operator*()const {
	return _node->_data;
}

2.2、->

cpp 复制代码
Ptr operator->()const {
	return &(_node->_data);
}
2.3、前置-- 后置--
cpp 复制代码
// 前置--
Self& operator--() {
	_node = _node->_prev;
	return *this;
}

// 后置--
Self operator--(int) {
	list_iterator<T> tmp(*this);
	_node = _node->_prev;

	return tmp;
}
2.4、后置++ 前置++
cpp 复制代码
// 后置++
Self operator++(int) {
	Self tmp(*this);
	_node = _node->_next;

	return tmp;
}

// 前置++
Self& operator++() {
	_node = _node->_next;
	return *this;
}
2.5、== 和 !=
cpp 复制代码
bool operator==(const Self& s)const {
	return _node == s._node;
}

bool operator!=(const Self& s)const {
	return _node != s._node;
}

3、list容器功能接口

cpp 复制代码
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、构造空链表

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

cpp 复制代码
void empty_init() {
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}
3.1.2、默认构造
cpp 复制代码
list(){
	empty_init();
}
3.1.3、拷贝构造
cpp 复制代码
list(const list<T>& lt) {
	// 由于拷贝无法完成头节点初始化,所以先初始化头节点
	empty_init();

	for (auto e : lt)
		push_back(e);
}
3.1.4、初始化列表
cpp 复制代码
list(initializer_list<int> il)
{
	empty_init();

	for (auto& e : il)
		push_back(e);
}
3.1.5、赋值重载
cpp 复制代码
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、析构
cpp 复制代码
~list() {
	clear(); // 清理所有节点
	delete _head; // 释放头节点
	_head = nullptr;
}
3.3、迭代器

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

cpp 复制代码
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、指定位置插入
cpp 复制代码
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、头插
cpp 复制代码
void push_front(const T& x) 
{
	insert(begin(), x);
}
3.4.3、尾插
cpp 复制代码
void push_back(const T& x) 
{
	insert(end(), x);
}
3.5、删除节点

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

3.5.1、删除指定位置节点
cpp 复制代码
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、头删
cpp 复制代码
void pop_front() 
{
	erase(begin());
}
3.5.3、尾删
cpp 复制代码
void pop_back() 
{
	erase(--end());
}
3.5.4、链表的清理
cpp 复制代码
void clear() {
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}
3.6、获取节点个数
cpp 复制代码
size_t size() const
{
	return _size;
}
3.8、判空
cpp 复制代码
bool empty() const
{
	return _size == 0;
}

4、总结

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

相关推荐
半桔2 小时前
【STL源码剖析】从源码看 list:从迭代器到算法
java·数据结构·c++·算法·stl·list
666HZ6662 小时前
Java Stream流
java·开发语言
数模加油站2 小时前
最新R(4.4.1)及R-studio保姆级安装配置详细教程及常见问题解答
开发语言·windows·数学建模·r语言
轩源源2 小时前
双向链表,这也太简单了吧!(C语言实现)
c语言·数据结构·算法·链表·青少年编程
编程指南针2 小时前
2026新选题-基于Python的老年病医疗数据分析系统的设计与实现(数据采集+可视化分析)
开发语言·python·病历分析·医疗病历分析
青草地溪水旁2 小时前
VSCode C/C++调试配置文件 `launch.json` 全字段深度解析
c语言·c++·vscode
bkspiderx2 小时前
C++设计模式之行为型模式:命令模式(Command)
c++·设计模式·命令模式
暴力求解3 小时前
数据结构---栈和队列详解(上)
开发语言·数据结构·c++
froginwe114 小时前
Rust 基础语法
开发语言