当前位置: 首页 > news >正文

【C++】list 简介与模拟实现(详解)

文章目录

  • 上文链接
  • 一、list 类简介
  • 二、迭代器
    • 1. 迭代器的分类
    • 2. list 中的迭代器
  • 三、模拟实现
    • 1. 整体框架
    • 2. 构造函数
      • (1) 节点的构造函数
      • (2) 迭代器的构造函数
      • (3) 链表的构造函数
    • 3. 迭代器中的运算符重载
      • (1) 解引用
      • (2) ->
      • (3) 前缀++
      • (4) 后缀++
      • (5) 前缀--
      • (6) 后缀--
      • (7) ==
      • (8) !=
    • 3. const迭代器
      • (1) 对比普通迭代器
      • (2) 模板优化
    • 4. 链表中获取迭代器
      • (1) begin
      • (2) end
    • 5. 链表的修改操作
      • (1) insert
      • (2) push_back
      • (3) push_front
      • (4) erase
      • (5) swap
    • 6. 链表容量相关
      • (1) size
      • (2) clear
    • 7. 链表的拷贝构造函数
    • 8. 链表的赋值重载
    • 9. 链表的析构函数
    • 10. 完整代码

上文链接

  • 【C++】vector 的模拟实现(详解)

一、list 类简介

  • list 参考文档:list - C++ Reference

list 是 C++ STL 库中的一个容器,它是一个模板类,可以认为它是一个带头的双向循环链表

请添加图片描述


二、迭代器

1. 迭代器的分类

在前面的学习中我们了解了迭代器,他是一个类似于指针一样的东西。STL 中的容器中都有一个自己的迭代器类型,而迭代器从功能的角度可以分为三种:单向迭代器双向迭代器随机迭代器

迭代器类型典型容器支持的操作
单向迭代器(forward)forward_list (单链表) / unordered_map…++
双向迭代器(bidirectional)list / map…++ / --
随机迭代器(random access)string / vector / deque…++ / -- / + / -

一个容器的迭代器类型是什么取决于该容器的底层结构,比如之前学过的 vector 和 string 它们的物理空间是连续的,从一个位置就可以快速 + 到另外一个位置,所以它能够支持 +- 的操作,是随机迭代器。而像链表这样的结构它想要 + 到后面的位置只能一个一个位置地移动,效率较低,所以没有提供 +- 这样的操作。

在许多参数列表中,参数的类型名字就暗示了要传何种类型的迭代器。比如说算法库中的 reverse 函数:

请添加图片描述

从名字上可以看出,迭代器应该传双向迭代器。同时这里传随机迭代器也可以,因为随机迭代器支持双向迭代器的所有操作,即 ++--


2. list 中的迭代器

在之前学习 string 和 vector 的时候我们的迭代器都是用指针来模拟实现的,那么这里的 list 是否可行呢?答案是否定的。因为 string 和 vector 的结构是连续的,用指针解引用就是当前位置的数据,对指针进行 ++ 就是下一个位置的指针。但是对于一个链表的节点的指针 Node* 而言,用指针解引用就不是当前位置的数据,而是节点。同样,对指针进行 ++ 操作更不是下一个节点的地址。因此我们不能用普通的指针去实现链表的迭代器。

所以在 STL 库中,用了一个类对节点的指针进行封装,在这个类中,重载了 *++ 等运算符,使得我们可以直接对链表的迭代器进行 * 之后直接访问到链表中的数据;对迭代器进行 ++ 之后移动到下一个节点处。而这整个类就是链表的迭代器。

下面是某版本 STL 中 list.h 的部分源码:

struct __list_iterator {  // 用一个类去封装作为链表的迭代器typedef __list_iterator<T, T&, T*>             iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;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;  // link_type 是链表节点的指针__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_OPERATORpointer 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;}
};

注:由于 C++ 中 struct 升级为了类,在 struct 中也可以定义函数。当我们不用访问限定符限制一个类中的任何变量或者函数的时候我们一般可以把这个类定义在一个 struct,比如说上面所看到的链表迭代器的封装。如果需要访问限定符限制,则定义在 class 中。


三、模拟实现

1. 整体框架

// list.h
namespace mine
{// 链表的节点template<class T>struct list_node{list_node* _next;  // 指向下一个节点list_node* _prev;  // 指向前一个节点T _data;// ...};// 迭代器template<class T>struct list_iterator{typedef list_node<T> Node;  // 节点typedef list_iterator<T> Self;  // 迭代器Node* _node;  // 节点的指针// ...};// 链表template<class T>class list{// typedef 后的名称也受访问限定符的限制,这里的 Node 是私有的typedef list_node<T> Node;public:typedef list_iterator<T> iterator;// ...private:Node* _head;size_t _size;}; 
}

2. 构造函数

(1) 节点的构造函数

节点的构造函数相当于我们 new 一个节点的时候初始化数据,可以给定一个值 x 进行初始化,如果不传参数则采用默认值。

list_node(const T& x = T()):_next(nullptr),_prev(nullptr),data(x)
{}

(2) 迭代器的构造函数

生成一个节点对应的迭代器,只需要传入该节点的指针即可。

list_iterator(Node* node):_node(node)
{}

(3) 链表的构造函数

首先需要我们开辟一个节点作为头节点 (哨兵节点),由于是双向带头的循环链表,所以我们需要让 _next_prev 都指向自己。

list()
{_head = new node;_head->_next = _head;_head->_prev = _head;_size = 0;
}

为了方便起见,我们可以额外写一个 empty_init 函数来完成这里的初始化过程,这样的话之后的拷贝构造函数也可以利用此函数。

void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}list()
{empty_init();
}

3. 迭代器中的运算符重载

(1) 解引用

对迭代器解引用的目的是想要访问到链表中节点的数据而不是节点本身,因此我们返回节点中的 _data 即可。

T& operator*()
{return _node->_data;
}

(2) ->

这里直接说结论了,正确的 -> 运算符重载写法如下:

T* operator->()
{return &_node->_data;
}

但是关于这个运算符需要补充讲解一个点。

struct AA
{int _a1;int _a2;AA(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}
};void test()
mine::list<AA> lt1;
lt1.push_back({ 1, 1 });
lt1.push_back({ 2, 2 });
lt1.push_back({ 3, 3 });mine::list<AA>::iterator lit1 = lt1.begin();
while (lit1 != lt1.end())
{cout << lit1->_a1 << endl;  cout << lit1->_a2 << endl;++lit1;
}
cout << endl;
  • 运行结果
1
1
2
2
3
3

按理来说根据 -> 的重载的写法,返回的是节点数据的指针,那么正确的写法应该是 lit->->_a1 才能访问到 AA 中的数据才对。但是现在只用了一个 ->。这里就是编译器所做的优化:省略了一个 ->,目的是增加可读性

显式地写两个 -> 编译器会报错,但是我们可以显式地调用 -> 的重载函数,然后再用 ->

lit1->->_a1;  // ERROR
lit1.operator->()->_a1;  // OK 

(3) 前缀++

前缀 ++ 操作将迭代器移动至当前节点的下一个节点。

Self& operator++()
{_node = _node->_next;return *this;
}

(4) 后缀++

后缀 ++ 操作将迭代器移动至当前节点的下一个节点,但表达式的结果是当前节点。所以我们可以先用一个临时变量记录当前节点,将迭代器移动过后返回这个临时变量即可。

注意这里不能传引用返回!因为 tmp 是函数中创建的一个临时对象,出了这个函数就销毁了。用引用返回的话会导致引用变成 “野引用”

Self operator++(int)  // 不能用引用返回
{Self tmp(*this);_node = _node->_next;return tmp;
}

(5) 前缀–

Self& operator--()
{_node = _node->_prev;return *this;
}

(6) 后缀–

Self operator--(int)  // 不能用引用返回
{Self tmp(*this);_node = _node->_prev;return tmp;
}

(7) ==

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

(8) !=

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

3. const迭代器

(1) 对比普通迭代器

const迭代器的要求是迭代器所指向的内容不能修改,而不是迭代器本身不能修改。所以我们不能单纯地用 const iterator 来表示 const迭代器,因为它表示的是迭代器本身不能修改。所以我们需要单独实现一个和普通迭代器 iterator 高度相似的类作为 const迭代器。

  • 命名
// struct list_iterator
struct list_const_iterator// typedef list_iterator<T> Self;
typedef list_const_iterator<T> Self;// 在 list 类中额外为const迭代器typedef
typedef list_const_iterator<T> const_iterator;
  • 解引用运算符重载

由于迭代器所指向的内容不能修改,而解引用返回的内容正是所指向的内容,所以在 const迭代器中,解引用操作符重载函数的返回值类型改为了 const T&

// T& operator*()
// {
//     return _node->_data;
// }const T& operator*()  // 返回值多加了一个const
{return _node->_data;
}
  • -> 运算符重载

这个函数同理,在 const迭代器中我们需要对它的返回值类型进行修改,以达到迭代器指向的内容不可修改的目的。

// T* operator->()
// {
// 		return &_node->_data;
// }const T* operator->()
{return &_node->_data;
}

const迭代器:

template<class T>
struct list_const_iterator  // const版本的迭代器
{typedef list_node<T> Node;typedef list_const_iterator<T> Self;Node* _node;list_const_iterator(Node* node):_node(node){}const T& operator*() {return _node->_data;}const T* operator->(){return &_node->_data;}// 其他函数的实现与普通迭代器一致
};

(2) 模板优化

仔细观察了上面的 const迭代器之后发现,从功能的角度来说,两个迭代器只有一个不同:就是只有解引用和 -> 操作符重载的返回类型不同,其他完全一样,代码复用率很低。因此我们考虑采用模板来优化。

template<class T, class Ref, class Ptr>  // 多加了两个模板参数 Ref,Ptr 表示引用返回的不同类型
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;Ref operator*()  // 由于两个迭代器的返回类型不同,所以这里的返回类型设置为一个模板{return _node->_data;}Ptr operator->()  // 此处同理{return &_node->_data;}// ...
};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引用/指针的就是const迭代器// ...
};

4. 链表中获取迭代器

(1) begin

获取第一个有效节点(头节点的下一个节点)的迭代器

// typedef list_iterator<T> iterator;iterator begin()
{return iterator(_head->_next);
}

const迭代器版本:

const_iterator begin() const
{return const_iterator(_head->_next);
}

(2) end

获取最后一个有效节点下一个节点(头节点)的迭代器

iterator end()
{return iterator(_head);
}

const迭代器版本:

const_iterator end() const
{return const_iterator(_head);
}

5. 链表的修改操作

(1) insert

在 pos 位置之前插入一个节点

void insert(iterator pos, const T& x)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;
}

(2) push_back

尾插一个节点。可以像下面这样老老实实地写:

push_back(const T& x)
{Node* tail = _head->_prev;Node* newnode = new Node(x);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;++_size;
}

更简洁的写法是直接复用 insert 函数:

void push_back(const T& x)
{insert(end(), x);
}

(3) push_front

头插一个节点。直接复用 insert 即可。

void push_front(const T& x)
{insert(begin(), x);
}
  • 测试
void test_list_1()
{mine::list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);mine::list<int>::iterator it = l1.begin();while (it != l1.end()){*it += 10;cout << *it << " ";++it;}cout << endl;l1.push_front(1);l1.push_front(2);l1.push_front(3);l1.push_front(4);for (auto e : l1){cout << e << " ";}cout << endl;
}int main()
{test_list_1();return 0;
}
  • 输出
11 12 13 14
4 3 2 1 11 12 13 14

(4) erase

删除 pos 位置的节点

void erase(iterator pos)
{assert(pos != end());  // 注意不能把哨兵位的节点删除了,所以断言一下Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;
}

上面的写法已经把基本的 erase 逻辑实现了,但是还存在一个问题就是迭代器失效,我们来看下面这样的例子。

void test_list_2()
{mine::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto e : lt) cout << e << " ";cout << endl;mine::list<int>::iterator it = lt.begin();while (it != lt.end()){if (*it % 2 == 0){lt.erase(it);}else{++it;}}for (auto e : lt) cout << e << " ";cout << endl;
}int main()
{test_list_2();return 0;
}
  • 运行结果

请添加图片描述
为什么会导致访问错误?就是因为我们删除了该节点之后,迭代器变成了一个“野指针”。再进行 ++ 操作编译器就会报错。所以为了解决这个问题,当我们 erase 之后需要对迭代器进行重新赋值,赋值为被删除节点的下一个节点的迭代器

iterator erase(iterator pos)
{assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return iterator(next);  // 返回下一个节点的迭代器// return next  // 也可以这样写,这样写就是隐式类型转换
}

因此 while 循环中的逻辑就变成了 lt = lt.erase(it)

while (it != lt.end())
{if (*it % 2 == 0){// lt.erase(it);it = lt.erase(it);}else{++it;}
}
  • 修改之后运行结果
1 2 3 4
1 3

(5) swap

交换两个链表。只需交换两个链表的头节点的指针 _head_size 即可。交换这两个内置类型用库中的 swap 函数即可。

void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}

6. 链表容量相关

(1) size

返回链表节点的个数(头节点除外)

size_t size()
{return _size;
}

(2) clear

清空链表中的节点(头节点除外)

void clear()
{auto it = begin();while (it != end())  // 遍历链表通过 erase 删除节点{it = erase(it);}
}

7. 链表的拷贝构造函数

list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}

8. 链表的赋值重载

list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}

9. 链表的析构函数

我们只需要自己实现链表的析构函数,而不需要自己显式地实现链表节点和迭代器的析构函数。因为编译器默认生成的析构函数就能够做到析构节点和迭代器并且不会造成内存泄漏。但是链表不行,因为链表中存储了节点的指针,编译器默认生成的析构函数不能释放指针所指向的空间,会造成内存泄漏的问题。所以我们需要手动地将链表节点释放再置空指针,避免出现内存泄漏的问题。

~list()
{clear();delete _head;_head = nullptr;
}

10. 完整代码

#pragma once
#include<iostream>
#include<cassert>using namespace std;namespace mine
{template<class T>struct list_node{list_node* _next;list_node* _prev;T _data;list_node(const T& x = T()):_next(nullptr),_prev(nullptr) ,_data(x){}};template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator==(const Self& s) const{return _node == s._node;}bool operator!=(const Self& s) const{return _node != s._node;}};//template<class T>//struct list_const_iterator//{//	typedef list_node<T> Node;//	typedef list_const_iterator<T> Self;//	Node* _node;//	list_const_iterator(Node* node)//		:_node(node)//	{}//	const T& operator*()//	{//		return _node->_data;//	}//	Self& operator++()//	{//		_node = _node->_next;//		return *this;//	}//	Self operator++(int)//	{//		Self tmp(*this);//		_node = _node->_next;//		return tmp;//	}//	Self& operator--()//	{//		_node = _node->_prev;//		return *this;//	}//	Self operator--(int)//	{//		Self tmp(*this);//		_node = _node->_prev;//		return tmp;//	}//	bool operator==(const Self& s) const//	{//		return _node == s._node;//	}//	bool operator!=(const Self& s) const//	{//		return _node != s._node;//	}//};template<class T>class list{typedef list_node<T> Node;public:typedef list_iterator<T, T&, T*> iterator;//typedef list_const_iterator<T> const_iterator;typedef list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){auto it = begin();while (it != end()){it = erase(it);}}size_t size(){return _size;}//void push_back(const T& x)//{//	Node* tail = _head->_prev;//	Node* newnode = new Node(x);//	tail->_next = newnode;//	newnode->_prev = tail;//	newnode->_next = _head;//	_head->_prev = newnode;//}void push_front(const T& x){insert(begin(), x);}void push_back(const T& x){insert(end(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}void insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;}iterator erase(iterator pos){assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;return iterator(next);}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}private:Node* _head;size_t _size;}; 
}
http://www.dtcms.com/a/266444.html

相关文章:

  • TM56M152A 十速工业级32位闪存MCU控制器芯片 外设接口+硬件加密引擎
  • 跨平台ZeroMQ:在Rust中使用zmq库的完整指南
  • JDK8升级为JDK21
  • error MSB8041: 此项目需要 MFC 库。从 Visual Studio 安装程序(单个组件选项卡)为正在使用的任何工具集和体系结构安装它们。
  • CSS之布局详解指南
  • 深度学习洪水推演:Python融合多源卫星数据可视化南方暴雨灾情
  • bpftrace统计mmap调用时延
  • 应急响应靶场——web3 ——知攻善防实验室
  • 怎么限制某些IP访问服务器?
  • 版本控制器SVN
  • React Native屏幕适配的艺术:px2dp从像素完美到跨平台优雅布局之详细篇
  • Flink TiDB CDC 环境配置与验证
  • RESTful API 安装使用教程
  • 用Python解锁图像处理之力:从基础到智能应用的深度探索
  • 项目前置知识技术点功能用例:C++11中的bind
  • 浏览器(Chrome /Edge)高效使用 - 内部命令/快捷键/启动参数
  • Excel 如何进行多条件查找或求和?
  • BLDC电机-运动控制---stm32时钟树定时器SYSTICKRTC的学习
  • HTTP 压缩
  • JavaScript与HTML:Web开发的双翼
  • 使用 C# 发送电子邮件(支持普通文本、HTML 和附件)
  • 基于SpringBoot+Vue的高校心理健康服务平台(AI心理分析、websocket即时通讯)
  • 【第三章:神经网络原理详解与Pytorch入门】01.神经网络算法理论详解与实践-(4)神经网络中的重要组件
  • 自动驾驶感知模块的多模态数据融合:时序同步与空间对齐的框架解析
  • [开源]微软 PowerToys 获 0.92 版本更新:新增系统托盘图标开 / 关功能、改进 Command Palette
  • RabbitMQ 4.1.1初体验
  • NeighborGeo:基于邻居的IP地理定位(四)
  • 攻防世界-Reverse-insanity
  • 通用业务编号生成工具类(MyBatis-Plus + Spring Boot)详解 + 3种调用方式
  • 基于 ETL 工具实现人大金仓数据库的数据迁移与整合实操指南