【C++初阶】模拟实现list

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 一、简单剖析list源码
  • 二、准备工作
  • 三、模拟实现list常见操作
      • 3.1 默认构造函数
      • 3.2 push_back - 尾插
      • 3.3 迭代器(重点)
      • 3.4 const的迭代器(重点)
      • 3.5 insert - 插入
      • 3.6 erase - 删除
      • 3.7 头插 - push_front
      • 3.8 尾删 - pop_back
      • 3.9 头删 - pop_front
      • 3.10 个数 - size
      • 3.11 析构
      • 3.12 清空 - clear
      • 3.13 拷贝构造
      • 3.14 交换
      • 3.15 赋值运算符重载
  • 四、源码

一、简单剖析list源码

在模拟vector容量讲过,要想快速了解STL源码,首先要看成员变量

在这里插入图片描述

node从名字上猜测是一个节点,其类型是list_node。然后我发现list_node也是重命名出来的:

在这里插入图片描述

__list_node<T>又是什么东西呢?如下所示:

在这里插入图片描述

显然这是一个双向链表,并且__list_node是用来定义结点的

在这里插入图片描述

接下来就应该分析构造函数

在这里插入图片描述

get_node从名字上是得到结点,那么应该是开辟空间的。我们可以简单看看:

在这里插入图片描述

空间配置器讲起来有点麻烦,直接使用newdelete也是够用的

然后nodenextprev都指向自己。因此list的底层是一个带头(哨兵位)双向循环链表,因此list的成员变量应该是哨兵位结点。

大致结构我们已经知道了,不妨再来看看插入操作:

在这里插入图片描述

这和以往学习过的双向循环链表很相似,无非就是创造新的结点,然后再把它们链接起来。

大致内容已经了解了,直接开始实现吧~

二、准备工作

为了方便管理代码,分两个文件来写:

  • Test.cpp - 测试代码逻辑
  • list.h - 模拟实现list
    在这里插入图片描述

三、模拟实现list常见操作

3.1 默认构造函数

namespace wj
{template<class T>struct list_node // 定义结点{list_node<T>* _next; list_node<T>* _prev;T _val;};template<class T>class list{public:list(){// 为哨兵位头结点开空间_head = new list_node<T>;// 自己指向自己_head->_prev = _head;_head->_next = _head;}private:list_node<T> _head; // 哨兵位(不存储有效数据)};
}

定义结点的成员变量最好是公有的,方便类外可以随时访问。注意:此处的struct可不是C语言的结构体,在C++中已经升级成了类,并且默认成员都是公有的。当然使用class也是没问题的,只是要加上public

以上代码还能简化,我们知道类模板和普通类是不同的,普通类的类名即是类型,而类模板的类名是类名<T>。而有许多人会很容易忘记加上<T>,因此我们可以对list_node<T>进行重命名typedef

namespace wj
{template<class T>struct list_node // 定义结点{list_node<T>* _next; list_node<T>* _prev;T _val;};template<class T>class list{typedef list_node<T> Node;public:list(){// 为哨兵位头结点开空间_head = new Node;// 自己指向自己_head->_prev = _head;_head->_next = _head;}private:list_node<T> _head; // 哨兵位(不存储有效数据)};
}
  • 为了防止与库的list冲突,要重新写一个命名空间域wj
  • typedef在类中是有讲究的。如果typedef放在public段中,则可以在类外部使用;而如果放在private段中,则只能在类内使用。注意:上述代码是只能在类中使用!

3.2 push_back - 尾插

void push_back(const T& val)
{//1. 找尾(哨兵位的prev)Node* tail = _head->_prev;// 2. 开辟一个新节点Node* newnode = new Node(val); // 3. 链接 _head tail newnodetail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;
}

尾插就容易多了,下面有图帮助大家理解:

在这里插入图片描述

注意:new对于自定义类型除了开空间,还会调用构造函数。初始化_val

struct list_node // 结点的定义
{list_node<T>* _next;list_node<T>* _prev;T _val; list_node(const T& val = T()):_next(nullptr), _prev(nullptr), _val(val){}
};

缺省值给T()相信看过模拟实现vector都不陌生。不能直接给0,这样就写死能,只能int类型适用,对于string就不行了。因此可以给个匿名对象,它会调用T类型的默认构造。内置类型也是有默认构造的:

在这里插入图片描述

3.3 迭代器(重点)

能否定义类似像vector的迭代器?如下所示:

typedef Node* iterator;

答案当然不行!list不能像vector一样以原生指针(普通指针)作为迭代器。

vector类似于数组,数据在内存中是连续存储的。对迭代器(指针)++,就可以跳过一个对象的大小,并且解引用也能得到对应的数据;然而,list的节点不能保证一定在内存空间中连续存在,导致++/--不一定能找到下一个节点,并且对其解引用得到的是结点而不是有效数据。

那问题来了,如何定义list的迭代器呢?

我们可以封装一个类,然后用重载运算符去改变指针的行为。为什么可以这样呢?原因是:内置类型的++是行为规定的,但是自定义类型的++是自己说的算。可以联想以往实现的日期类->点击跳转

auto it = l.begin();
while (it != l.end())
{cout << *it << ' ';++it;
}

我们可以对照以上代码一步一步实现迭代器

  • begin() + end()

在这个类中,只需要一个结点类的指针成员变量,用于指向list某一个结点, 在一开始定义迭代器时,需要一个构造函数,用于迭代器的初始化。注意:beginend需要定义在list类中,因为它们本身就是list内置的接口函数

// 封装一个类实现迭代器
template<class T>
struct __list_iterator 
{typedef list_node<T> Node;Node* _node; //指向某个节点的指针// 迭代器的初始化__list_iterator(Node* node) :_node(node){}
};template<class T>
class list
{typedef list_node<T> Node;
public:typedef __list_iterator<T> iterator; iterator begin(){return _head->_next;// return iterator(_head->_next);}iterator end(){return _head;//return iterator(_head);}
private:Node* _head;
};

这里还有一个知识点,beginend返回类型为迭代器,怎么能返回结点的指针呢?— 这是因为单参数的构造函数支持隐式类型转换。

  • !=== *++--

封装一个类,然后用重载运算符去改变指针的行为

// 封装一个类实现迭代器
template<class T>
struct __list_iterator 
{typedef list_node<T> Node;typedef __list_iterator<T> self;Node* _node; //指向某个节点的指针__list_iterator(Node* node) // 迭代器的初始化:_node(node){}
/// 用结点的指针比bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;}
/T& operator*(){// 出了作用域,结点还在,引用返回return _node->_val;}
/// 迭代器++返回的还是迭代器self& operator++() //前置{_node = _node->_next;return *this;}self& operator--() // 前置{_node = _node->_prev;return *this;}self operator--(int) // 后置{self tmp(*this);_node = _node->_prev;return tmp;}self operator++(int) // 后置{self tmp(*this);_node = _node->_next;return tmp;}
};

前置++后置++会发生一个问题:函数名会相同。因此,C++规定:后置(++/--)重载时多增加一个int类型的参数,但调用函数时该参数不用传递。

3.4 const的迭代器(重点)

现在又有一个问题,const的迭代器也能否像类似于vector一样设计?如下所示:

在这里插入图片描述

答案当然是不可以的!这是因为 const迭代器要求的是迭代器指向的内容不可以被修改,而对一个类加上一个const,这是让这个类对象无法被修改啊。也就是类的成员变量都不可以被修改,这样一来,这个迭代器里面的指针无法移动了。(const的迭代器指针是可以移动的,但是指向的内容不可被修改)

那么const的迭代器该如何设计呢?我们知道,list迭代器输出数据是依靠解引用的,因此可以在返回值加上const

template<class T>
struct __list_iterator
{typedef list_node<T> Node;typedef __list_iterator<T> selfNode* _node; //指向某个节点的指针__list_iterator(Node* node) // 迭代器的初始化:_node(node){}// 用结点的指针比bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;}T& operator*(){// 出了作用域,结点还在,引用返回return _node->_val;}// 迭代器++返回的还是迭代器self& operator++() //前置{_node = _node->_next;return *this;}self& operator--() // 前置{_node = _node->_prev;return *this;}self operator--(int) // 后置{self tmp(*this);_node = _node->_prev;return tmp;}self operator++(int) // 后置{self tmp(*this);_node = _node->_next;return tmp;}
};template<class T>
struct __list_iterator
{typedef list_node<T> Node;typedef __list_iterator<T> self;Node* _node; //指向某个节点的指针__list_iterator(Node* node) // 迭代器的初始化:_node(node){}// 用结点的指针比bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;}const T& operator*(){// 出了作用域,结点还在,引用返回return _node->_val;}// 迭代器++返回的还是迭代器self& operator++() //前置{_node = _node->_next;return *this;}self& operator--() // 前置{_node = _node->_prev;return *this;}self operator--(int) // 后置{self tmp(*this);_node = _node->_prev;return tmp;}self operator++(int) // 后置{self tmp(*this);_node = _node->_next;return tmp;}
};

但以上代码显得有点冗余,只有两个函数的返回值不一样,其它都是一样的。那还有什么别的设计方法呢?

注意:上面两个函数只要返回值的类型不一样,因此可以通过一个类型来控制返回值 -> 即增加一个模板参数(库里也是这么实现的~)

// 封装一个类实现迭代器
template<class T, class Ref> // 增加一个模板参数
struct __list_iterator
{typedef list_node<T> Node;typedef __list_iterator<T, Ref> self;Node* _node; //指向某个节点的指针__list_iterator(Node* node) // 迭代器的初始化:_node(node){}Ref operator*(){return _node->_val;}
}template<class T>
class list
{typedef list_node<T> Node;
public:typedef __list_iterator<T, T&> iterator;typedef __list_iterator<T, const T&> const_iterator;iterator begin(){return _head->_next;}const_iterator end() const{return _head;}const_iterator begin() const{return _head->_next;}iterator end(){return _head;}
private:list_node<T> _head; // 哨兵位(不存储有效数据)
};

补充:除了重载*运算符,当然也要重载->操作符

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

那什么时候会用到->操作符呢?下面有个例子:

#include <iostream>
#include "list.h"
using namespace std;struct A
{A(int a = 0):_a(a){}int _a;
};
int main()
{wj::list<A> lt;lt.push_back(A(1));lt.push_back(A(2));lt.push_back(A(3));lt.push_back(A(4));lt.push_back(A(5));wj::list<A>::iterator it = lt.begin();while (it != lt.end()){cout << it->_a << " ";it++;}cout << endl;
}

【输出结果】

在这里插入图片描述

有没有发现operator->非常怪,首先我们这个运算符重载返回的是什么呢?是T*,也就是A*,也就是说它还需要一次->才能打印_a。严格来说,it->->_a,才是符合语法的。那么这里为什么还能编译通过呢?因为运算符重载要求可读性,那么编译器特殊处理,省略了一个->

但是以上代码还是不够完善,由于->只针对普通对象,如果是const对象,其返回值应该是const T*,这个问题就和运算符重载*类似了,再增加一个模板参数,因此完整代码如下:

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->_val;// 出了作用域,结点还在,要加&}ptr operator->() {return &_node->_val;}
}template<class T> // 为list提供
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; iterator begin(){// return iterator(_head->_next);return _head->_next;}iterator end(){// return iterator(_head);return _head;}
private:Node* _head; // 哨兵位(不存储有效数据)
};

3.5 insert - 插入

iterator insert(iterator pos, const T& x)
{// pos 不需要检查  // 假设在node前插入// head newnode node tail// 步骤如下// 1. 开辟新的结点Node* newnode = new Node(x);// 2. 找到要删除的结点nodeNode* cur = pos._node;// 3. 以及node的前一个节点Node* prev = cur->_prev;// 4. 链接prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;return newnode;// 返回新插入元素的位置
}

在这里插入图片描述

3.6 erase - 删除

iterator erase(iterator pos)
{// 检查pos的有效性assert(pos != end());// 1.分别找到pos的前一个节点和后一个节点Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;// 2, 链接prev->_next = next;next->_prev = prev;// 3. 删除delete cur;// 注意:list的erase会有迭代器失效问题// 返回删除元素的下一个位置return next;
}

在这里插入图片描述

3.7 头插 - push_front

复用insert

void push_front(const T& val)
{insert(begin(), val);
}

3.8 尾删 - pop_back

复用erase

void pop_back()
{erase(--end());
}

3.9 头删 - pop_front

void pop_front()
{erase(begin());
}

3.10 个数 - size

遍历即可

size_t size()
{size_t count = 0;iterator it = begin();while (it != end()){++count;++it;}return count;
}

或者还可以在成员变量中定义size_t _size,每次插入数据++,以及删除数据--即可

3.11 析构

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

3.12 清空 - clear

void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}

3.13 拷贝构造

list(const list<T>& it)
{_head = new Node;_head->_prev = _head;_head->_next = _head;for (auto& e : it){push_back(e);}
}

3.14 交换

void swap(list<T> it)
{std::swap(_head, it._head);std::swap(this->size(), it._size());
}

3.15 赋值运算符重载

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

四、源码

#pragma once
#include <assert.h>namespace wj
{template<class T> struct list_node {list_node<T>* _next;list_node<T>* _prev;T _val; list_node(const T& val = T()):_next(nullptr), _prev(nullptr), _val(val){}};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->_val;}ptr operator->() {return &_node->_val;}self& operator++(){_node = _node->_next;return *this;}self& operator--(){_node = _node->_prev;return *this;}self& operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;}};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; iterator begin(){// return iterator(_head->_next);return _head->_next;}iterator end(){// return iterator(_head);return _head;}const_iterator begin() const{//return _head->_next;return const_iterator(_head->_next);}const_iterator end() const{return _head;//return const_iterator(_head);}list(){_head = new Node;_head->_prev = _head;_head->_next = _head;_size = 0;}list(const list<T>& it){_head = new Node;_head->_prev = _head;_head->_next = _head;_size = 0;for (auto& x : it){push_back(x);}}void push_back(const T& val){Node* tail = _head->_prev;Node* newnode = new Node(val);tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;_size++;}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;_size++;return newnode;}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 next;}void push_front(const T& val){insert(begin(), val);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}size_t size(){/*size_t count = 0;iterator it = begin();while (it != end()){++count;++it;}return count;*/return _size;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}_size = 0;}void swap(list<T> it){std::swap(_head, it._head);std::swap(_size, it._size);}list<T>& operator=(const list<T> it){swap(it);return *this;}private:Node* _head; size_t _size;};
}

测试代码

#include <iostream>
using namespace std;
#include "list.h"int main()
{// 默认构造wj::list<int> ll;// 尾插测试ll.push_back(1);ll.push_back(2);ll.push_back(3);ll.push_back(4);// 迭代器测试wj::list<int>::iterator it = ll.begin();while (it != ll.end()){cout << *it << ' ';it++;}cout << endl;// 范围for(底层迭代器)for (auto& x : ll){cout << x << ' ';}cout << endl;// insert测试// 在3的前面插入30it = ll.begin();for (int i = 0; i < 2; i++){it++;}ll.insert(it, 30);for (auto& x : ll){cout << x << ' ';}cout << endl;//  erase测试it = ll.begin();// 删除30for (int i = 0; i < 2; i++){it++;}ll.erase(it);for (auto x : ll){cout << x << ' ';}cout << endl;// 头插测试// 头插100ll.push_front(100);for (auto x : ll){cout << x << ' ';}cout << endl;// 尾删测试ll.pop_back(); // 100 1 2 3for (auto x : ll){cout << x << ' ';}cout << endl;// 头删测试ll.pop_front(); // 1 2 3for (auto x : ll){cout << x << ' ';}cout << endl;// size测试cout << "个数为:" << ll.size() << endl; // 3// 清空ll.clear();for (auto x : ll){cout << x << ' '; // 无输出}cout << endl;// 拷贝构造ll.push_back(1);ll.push_back(2);ll.push_back(3);ll.push_back(4);ll.push_back(5);wj::list<int> lll(ll);for (auto x : lll){cout << x << ' '; // 1 2 3 4 5}cout << endl;// 赋值运算符重载wj::list<char> a;a.push_back('a');wj::list<char> b;b.push_back('b');b.push_back('b');b.push_back('b');a = b;for (auto x : a){cout << x << ' ';}cout << endl;// 交换wj::list<char> c;a.push_back('c');wj::list<char> d;b.push_back('d');b.push_back('d');b.push_back('d');d.swap(c);for (auto x : c){cout << x << ' ';}cout << endl;for (auto x : d){cout << x << ' ';}cout << endl;return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/90039.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java基础二十一(异常捕获和处理)

1. 异常 1.1 概述 (1) 认识异常 异常是指程序在运行过程中出现非正常情况。 (2) Java 异常体系结构 所有异常类都是 Throwable 类的子类&#xff0c;它派生出两个子类&#xff0c;Error 类和 Exception 类。 &#xff08;1&#xff09;Error 类 : 表示程序无法恢复的严重错误…

D-Link DCS 密码泄露漏洞

0x01 前言 本次测试仅供学习使用&#xff0c;如若非法他用&#xff0c;与本文作者无关&#xff0c;需自行负责&#xff01;&#xff01;&#xff01; 0x02 漏洞描述 D-link DCS是一款成像色彩为彩色 是一款网络摄像机。D-link DCS系统存在密码泄露漏洞&#xff0c;攻击者通过…

Qt应用开发(基础篇)——字体选择器 QFontDialog

一、前言 QFontDialog类继承于QDialog&#xff0c;是一个设计用来选择字体的对话框部件。 对话框窗口QDialog QFontDialog字体选择对话框&#xff0c;设计用来让用户选择某一种字体&#xff0c;一般用于文本编辑窗口、标签显示和一些需要文本输入的场景。你可以直接使用静态函数…

Android 13 - Media框架(8)- MediaExtractor

上一篇我们了解了 GenericSource 需要依赖 IMediaExtractor 完成 demux 工作&#xff0c;这一篇我们就来学习 android media 框架中的第二个服务 media.extractor&#xff0c;看看 IMediaExtractor 是如何创建与工作的。 1、MediaExtractorService media.extractor 和 media.p…

Day46|leetcode 139.单词拆分

leetcode 139.单词拆分 题目链接&#xff1a;139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; 视频链接&#xff1a;动态规划之完全背包&#xff0c;你的背包如何装满&#xff1f;| LeetCode&#xff1a;139.单词拆分_哔哩哔哩_bilibili 题目概述 给你一个字符串 s 和一…

Stable Diffusion 提示词入门指南

前言 本文主要讲解 Stable Diffusion &#xff08;下文简称 SD&#xff09;提示词的用法&#xff0c;帮助大家生成更高质量的图片 本章节主要讲解文生图&#xff0c;其他类型读者可以自行探索。同时本文主要是以 Stable Diffusion Discard 的形式生成图片 如果各位对于图片隐…

Vue2向Vue3过度核心技术自定义指令

目录 1 自定义指令1.指令介绍2.自定义指令3.自定义指令语法4.指令中的配置项介绍5.代码示例6.总结 2 自定义指令-指令的值1.需求2.语法3.代码示例 3 自定义指令-v-loading指令的封装1.场景2.需求3.分析4.实现5.准备代码 1 自定义指令 1.指令介绍 内置指令&#xff1a;v-html、v…

大数据时代的软件开发实践:利用云计算和AI赋能创新

文章目录 云计算的赋能弹性资源管理远程协作与分布式开发持续集成和持续交付成本效益 人工智能的赋能数据驱动的决策自动化智能预测和优化自适应系统 创新的实践方法数据驱动的创新智能化产品开放式创新迭代和反馈 &#x1f388;个人主页&#xff1a;程序员 小侯 &#x1f390;…

11. 盛最多水的容器(c++题解)

11. 盛最多水的容器&#xff08;c题解&#xff09; 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大…

Ansible学习笔记2

Ansible是Python开发的自动化运维工具&#xff0c;集合了众多运维工具&#xff08;Puppet、cfengine、chef、func、fabric&#xff09;的优点&#xff0c;实现了批量系统配置&#xff0c;批量程序部署、批量运行命令等功能。 特点&#xff1a; 1&#xff09;部署简单&#xff…

根据源码,模拟实现 RabbitMQ - 网络通讯设计,自定义应用层协议,实现 BrokerServer (8)

目录 一、网络通讯协议设计 1.1、交互模型 1.2、自定义应用层协议 1.2.1、请求和响应格式约定 ​编辑 1.2.2、参数说明 1.2.3、具体例子 1.2.4、特殊栗子 1.3、实现 BrokerServer 1.3.1、属性和构造 1.3.2、启动 BrokerServer 1.3.3、停止 BrokerServer 1.3.4、处…

基于java swing和mysql实现的汽车租赁管理系统(源码+数据库+文档+运行指导视频)

一、项目简介 本项目是一套基于java swing和mysql实现的汽车租赁管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经…