【C++】list类的模拟实现

🏖️作者:@malloc不出对象
⛺专栏:C++的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
在这里插入图片描述

目录

    • 前言
    • 一、list类的模拟实现
      • 1.1 list的主体框架
      • 1.2 无参构造函数
      • 1.3 push_back
      • 1.4 正向迭代器
      • 1.5 反向迭代器
      • 1.6 insert
      • 1.7 erase
      • 1.8 clear
      • 1.9 析构函数
      • 1.10 构造函数
      • 1.11 赋值运算符重载
      • 1.12 empty
      • 1.13 front && back
      • 1.14 完整代码
    • 二、vector与list的对比


前言

本篇文章我们要来模拟实现的是list类,它的底层是用带头结点的双向循环链表实现的。

一、list类的模拟实现

1.1 list的主体框架

既然我们是用双向循环链表实现的,那么每个结点肯定都存储着next、prev与data信息,那么接下来我们就来定义一个类对它的结点进行初始化操作。

template<class T>  // 模板参数T
struct list_node
{list_node<T>* _next; 	//list_node<T>* 是类型list_node<T>* _prev;T _data;list_node(const T& val = T())  // 匿名对象初始化: _next(nullptr), _prev(nullptr), _data(val){}
};

我们把节点定义好之后,我们就来定义list类了,list类的成员变量只需要一个哨兵位的头结点就可以了。

template <class T>
class list
{typedef list_node<T> node;	
private:node* _head;	// 哨兵位头节点
};

1.2 无参构造函数

list()
{_head = new node;		// 申请一个节点_head->_next = _head;   // _head->_next指向自己_head->_prev = _head;   // _head->_prev也指向自己
}

1.3 push_back

双向链表的插入和删除都是非常好实现的,因为每个结点都有上一个节点和下一个节点的信息。这里我们要想实现尾插,我们要找到尾结点再改变它的指向就行了,非常的简单这里我就不做过多的赘述了。另外后续在我们实现insert和erase之后全都可以进行复用,这里只是先给大家打个样。

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;
}

1.4 正向迭代器

有了尾插之后我们可以往链表里面插入数据,下面我们想遍历一下链表,我们知道list不支持[]下标访问,原因是因为它是不连续的空间,所以我们必须使用迭代器对它进行遍历。

我们知道在实现vector类(SGI版本)时,我们的迭代器是作为一个原生指针来使用的,而在vector类(P.J.版本)中我们的迭代器是自定义类型对原生指针的封装,但本质上它们都是在模拟指针的行为!!!那么在list类中迭代器到底充当什么角色呢?我们知道迭代器支持++ - -操作这是为了找到后一个数据和前一个数据的位置,对于list而言它是双向链表它的空间是不连续的,假设迭代器是一个原生指针的话,指针++ - -一步取决于指针所指向的类型,对于不连续的空间来说++ - -能否刚好指向下一个位置或者上一个位置一切都是未知数,因此我们的迭代器在list中是对自定义类型原生指针的封装!!!

我们先来看看SGI版本下对正向迭代器的封装源码:

在这里插入图片描述

好了,也许我们有些地方可能有些不太懂,而且标准库的源码采用了非常多的命名替换,这是命名规范的问题,接下来我们模拟实现的时候不采用标准库这种方式,我们尽量的实现简洁易懂些。

最原始的代码

template<class T>
struct __list_iterator
{typedef list_node<T> node;typedef __list_iterator<T> self;node* _node;__list_iterator(node* x)  // 初始化结点: _node(x){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){__list_iterator tmp = *this;_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){__list_iterator tmp = *this;_node = _node->_prev;return tmp;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};

这是我们最原始的代码,那么大家知道为什么源码为什么会多出两个模板参数吗?这里我们实现的是正向迭代器,那么我们要实现const正向迭代器版本呢?难道要再去写一个__list_const_iterator类吗?

显然这样出现了大量的代码重复,我们是极其不支持这种实现方式的,所以我们必须想办法让他们之间可以进行复用,我们只需要改变一下返回值类型、参数类型就能实现iterator和const_iterator版本,这里模板参数的作用就体现出来了,我们可以添加一个模板参数,到时候我们可以实例化一份iterator和const_iterator。至于第三个模板参数是为了重载->运算符函数的,它同样的有T*版本和const T*版本。

为什么要重载->运算符?

struct AA
{int _a1;int _a2;AA(int a1, int a2): _a1(a1), _a2(a2){}
};void test()
{list<AA> lt;lt.push_back(AA(1, 1));lt.push_back(AA(2, 2));lt.push_back(AA(3, 3));list<AA>::iterator it = lt.begin();while (it != lt.end()){cout << (*it)._a1 << " " << (*it)._a2 << endl;  ++it;}cout << endl;
}

我们可以看到上诉代码对于一个自定义类型要想访问它的成员变量就必须得写成(*it)._a1、(*it)._a2,先*it得到AA对象,再访问它的成员变量,这种写法是不是未免有些麻烦了?我们平常可以直接使用->去访问它的成员变量,就像这段代码我们可以写成it->_a1、it->_a2,但是我们此时未重载->运算符,所以为了方便使用这里我们还需要重载一下->运算符。

对于__list_iterator类我们可以重载->写出下面的代码:

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

但你有没有发现一些奇怪之处??

在这里插入图片描述

好了,关于为什么要重载->运算符这里我们已经讲清楚了,那么为什么这跟添加第三个模板参数有什么关系呢?原因很简单,一个T*版,一个const T*版,添加第三个模板参数Ptr也是为了复用T*版本。

所以最终我们的__list_iterator可以写成这种版本:

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* x)  // 初始化结点: _node(x){}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){return _node == s._node;}bool operator!=(const self& s){return _node != s._node;}};

我们在list类中就可以实例化iterator和const_iterator这两种版本的迭代器,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);}const_iterator begin() const{return const_iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator end() const{return const_iterator(_head);}

我们来进行测试一下:

在这里插入图片描述

我们可以看到对应的过程,当list<int>显式声明模板类时,此时我们的类模板就根据类型实例化出一个具体的类。

在这里插入图片描述

1.5 反向迭代器

我们知道C++追求极致的性能,既然能复用绝不会写出两份差不多的代码,,所以我们实现反向迭代器并不会像正向迭代器那样倒着来,而是去复用正向迭代器!!!

反向迭代器其实也是一种适配器,它可以适配出各种容器的反向迭代器,其中最重要的就是将正向迭代器作为底层结构来封装反向迭代器,反向迭代器 ++ 就复用正向迭代器的 - -,反向迭代器 - - 就复用正向迭代器的 ++。

在这里插入图片描述

我们的反向迭代器既然是作为适配器去使用,那么我们就把它封装到单独的一个类中对它进行模拟实现,并且正向迭代作为它的模板参数进行复用它的功能!!

反向迭代器的模拟实现

// iterator.h
namespace curry
{template<class Iterator, class Ref, class Ptr>struct ReverseIterator{typedef ReverseIterator<Iterator, Ref, Ptr> Self;Iterator _cur;  // _cur就是一个正向迭代器ReverseIterator(Iterator it)  : _cur(it){}Ref operator*(){Iterator tmp = _cur;--tmp;return *tmp;}Self& operator++(){--_cur;return *this;}Self operator++(int){Self tmp = *this;--_cur;return tmp;}Self& operator--(){++_cur;return *this;}Self operator--(int){Self tmp = *this;++_cur;return tmp;}// 返回当前对象的地址Ptr operator->(){return &(operator*());}bool operator!=(const Self& s){return _cur != s._cur;}bool operator==(const Self& s){return _cur == s._cur;}};
}

只要知道了反向迭代器与正向迭代器的特性,我们就能够很容易的通过复用正向迭代器的成员函数来实现反向迭代器的成员函数!!同时反向迭代器其实解决了所有的双向迭代器的问题,因为只要将对应容器的正向迭代器作为反向迭代器的模板参数我们就能够对反向迭代器进行复用,所以我们之前的vector类的反向迭代器也能够直接使用它的正向迭代器复用实现!!这是一种非常巧妙的思想!!

1.6 insert

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;
}

实现了insert接口函数,那么我们的push_back与push_front都是可以复用的。

push_back(int x)

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

push_front(int x)

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

list类与vector类的insert不同之处在于list类insert不会导致迭代器失效,因为它的空间的不连续的,并且没有挪动数据造成迭代器失效,所以我们也可以看到它的返回值为void,并不需要放回插入位置的迭代器。

1.7 erase

iterator erase(iterator pos)
{assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;return iterator(next);
}

迭代器失效即迭代器所指向的节点的无效,即该节点被删除了,所以对于list类的erase会导致指向删除节点的迭代器失效,其他迭代器不会受到影响而vector类进行erase会导致当前位置或者后续迭代器失效,所以正确的解决办法是给迭代器重新赋值!!

实现了erase函数接口,pop_back()以及pop_front()就可以进行复用了。

pop_back()

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

pop_front()

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

1.8 clear

void clear()
{iterator it = begin();while (it != end()){it = erase(it);	// erase返回下一个位置的迭代器}
}

clear释放链表中的结点,_head哨兵位头结点除外。

1.9 析构函数

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

析构函数的作用是释放所有结点,我们可以先调用clear依次释放链表中的结点,最后再释放头结点。

1.10 构造函数

传统写法

void empty_init()
{// 创建并初始化哨兵位头节点_head = new node;_head->_prev = _head;_head->_next = _head;
}// 拷贝构造传统写法 lt2(lt1)
list(const list<T>& lt)
{empty_init();for (auto& e : lt)	// 加引用避免自定义类型的拷贝构造{push_back(e);}
}

现代写法

template <class Iterator>  // 双向迭代器类型构造
list(Iterator first, Iterator last)
{empty_init();while (first != last){push_back(*first);++first;}
}void swap(list<T>& tmp)
{std::swap(_head, tmp._head);	// 交换哨兵位的头节点
}// 拷贝构造现代写法 lt2(lt1)
list(const list<T>& lt)
{empty_init();	list<T> tmp(lt.begin(), lt.end());	// 迭代器区间初始化swap(tmp);	
}

1.11 赋值运算符重载

传统写法

list<T>& operator=(const list<T>& lt)
{if (this != &lt)	// 防止自己给自己赋值{clear();	// 清理数据for (auto& e : lt){push_back(e);}}return *this;
}

现代写法

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

一些常用的函数接口就讲到这里了,还有一些简单的函数接口读者下来也可以自己去尝试实现一下。

1.12 empty

bool empty()
{return _head->_next == _head &&_head->_prev == _head;
}

1.13 front && back

T& front()
{assert(!empty());return *begin();
}const T& front() const
{assert(!empty());return *begin();
}T& back()
{assert(!empty());return *(--end());
}const T& back() const
{assert(!empty());return *(--end());
}

1.14 完整代码

// list.h#include "iterator.h"namespace curry
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;list_node(const T& val = T()): _next(nullptr), _prev(nullptr), _data(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* x)  // 初始化结点: _node(x){}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){return _node == s._node;}bool operator!=(const self& s){return _node != s._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;typedef ReverseIterator<iterator, T&, T*> reverse_iterator;typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;iterator begin(){return iterator(_head->_next);}reverse_iterator rbegin(){return reverse_iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_reverse_iterator rbegin() const{return const_reverse_iterator(_head);}iterator end(){return iterator(_head);}reverse_iterator rend(){return reverse_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}const_reverse_iterator rend() const{return const_reverse_iterator(_head->_next);}list(){empty_init();}// 现代写法list(const list<T>& lt){empty_init();list<T> tmp(lt.begin(), lt.end());swap(tmp);}template<class Iterator>list(Iterator first, Iterator last){empty_init();while (first != last){push_back(*first);++first;}}list<T>& operator=(list<T> lt){swap(lt);return *this;}// 释放所有结点~list(){clear();delete _head;_head = nullptr;}void swap(list<T>& tmp){std::swap(_head, tmp._head);}// 释放结点,但是_head头结点不处理void clear(){iterator it = begin();while (it != end()){it = erase(it);}}void empty_init(){_head = new node;_head->_next = _head;_head->_prev = _head;}void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}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;}void pop_back(){erase(--end());}void pop_front(){erase(begin());}iterator erase(iterator pos){assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;return iterator(next);}T& front(){assert(!empty());return *begin();}const T& front() const{assert(!empty());return *begin();}T& back(){assert(!empty());return *(--end());}const T& back() const{assert(!empty());return *(--end());}bool empty(){return _head->_next == _head &&_head->_prev == _head;}private:node* _head;};
}

二、vector与list的对比

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素效率O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

以上就是本文的所有内容了,如有错处或者疑问欢迎大家在评论区相互交流orz~🙈🙈

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

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

相关文章

2023-08-28 LeetCode每日一题(插入区间)

2023-08-28每日一题 一、题目编号 57. 插入区间二、题目链接 点击跳转到题目位置 三、题目描述 给你一个 无重叠的 &#xff0c;按照区间起始端点排序的区间列表。 在列表中插入一个新的区间&#xff0c;你需要确保列表中的区间仍然有序且不重叠&#xff08;如果有必要的…

nacos服务器启动报错集合

报错1 Error creating bean with name ‘user‘: Unsatisfied dependency expressed through field ‘jwtTokenManage 开启鉴权之后&#xff0c;你可以自定义用于生成JWT令牌的密钥&#xff0c;application.properties中的配置信息为&#xff1a; ### Since 1.4.1, worked when…

Django(3)-创建第一个数据模型-ORM映射

数据库配置 根目录下settings.py 。这是个包含了 Django 项目设置的 Python 模块。 通常&#xff0c;这个配置文件使用 SQLite 作为默认数据库。如果你不熟悉数据库&#xff0c;或者只是想尝试下 Django&#xff0c;这是最简单的选择。Python 内置 SQLite&#xff0c;所以你无…

【科研论文配图绘制】task6直方图绘制

【科研论文配图绘制】task6直方图绘制 task6 主要掌握直方图的绘制技巧&#xff0c;了解直方图含义&#xff0c;清楚统计指标的添加方式 1.直方图 直方图是一种用于表示数据分布和离散情况的统计图形&#xff0c;它的外观和柱形图相近&#xff0c;但它所 表达的含义和柱形图…

《golang设计模式》第二部分·结构型模式-03-组合模式(Composite)

文章目录 1. 概述1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.3 类图 1. 概述 将叶子节点和复合节点组合起来&#xff0c;定义一个抽象接口遍历他们 1.1 角色 Component&#xff08;抽象构件&#xff09;&#xff1a;为叶子构件和复合构件声明接口&#xff0c;定义了结构…

使用element-plus组件,默认显示英文 转换为中文

最近在边写项目边学习vue3 所以这几天没有更新 找机会把vue3的知识也统计一下吧 先说今天遇到的问题 最近做项目的时候使用element-plus分页组件时发现&#xff0c;显示的不是中文的了&#xff0c;是英文的 解决方法 在app.vue里面配置 <template><el-config-provi…

ROS2 CLI工具

目录 节点topic参数launch录包 节点 查看所有node&#xff1a;ros2 node list查看某个node的信息&#xff1a;ros2 node info node_name topic 查看topic输出&#xff1a; ros2 topic echo <topic_name> 查看topic频率&#xff1a;ros2 topic hz <topic_name> …

美创科技“签”手柠檬文才学堂,共推高校数据安全建设

近日&#xff0c;由柠檬文才学堂联合中国教育在线、东北财经大学网络教育学院共同主办的“三教统筹下高校继续教育数字化转型研讨”顺利召开。 国内高等院校&#xff08;高职院校&#xff09;继续教育分管领导&#xff0c;继续教育学院领导及继续教育信息化、教学教务管理、课程…

C++之ifstream成员函数get、tellg、eof实例(一百八十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

范式 事务 多表查询

范式 概念&#xff1a;设计数据库时&#xff0c;需要遵循的一些规范。要遵循后边的范式要求&#xff0c;必须遵循前边的所有范式要求 第一范式&#xff1a; 数据库表的每一列都是不可分割的基本数据项 这样子就不满足第一范式 这样子就满足第一范式 存在问题&#xff1a; 数…

无涯教程-PHP - 返回类型声明

在PHP 7中&#xff0c;引入了一个新函数返回类型声明&#xff0c;返回类型声明指定函数应返回的值的类型&#xff0c;可以声明返回类型的以下类型。 intfloatbooleanstringinterfacesarraycallable 有效返回类型 <?phpdeclare(strict_types1);function returnIntValue(i…

Unity打包Windows程序,概率性出现无法全屏或分辨率不匹配

排除代码和Resolution and Presentation面板设置问题 如果程序还是不能按照预期的分辨率运行&#xff0c;应该是系统注册表记录了对应的设置。 解决方案&#xff1a; 打开注册表&#xff0c;使用快捷键“Win” "R"组合快捷键。在打开后面键入命令&#xff1a;Rege…

一键快速还原修复人脸,CodeFormer 助力人脸图像修复

今天在查资料的时候无意间看到了一个很有意思的工具&#xff0c;就是CodeFormer &#xff0c;作者给出来的说明是用于人脸修复任务的&#xff0c;觉得很有意思就拿来实践了一下&#xff0c;这里记录分享一下。 首先对人脸修复任务进行简单的回顾总结&#xff1a; 人脸修复是指…

分发饼干【贪心算法】

分发饼干 假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。但是&#xff0c;每个孩子最多只能给一块饼干。 对每个孩子 i&#xff0c;都有一个胃口值 g[i]&#xff0c;这是能让孩子们满足胃口的饼干的最小尺寸&#xff1b;并且每块饼干 j&#xff0c;都有一个…

MATLAB中符号变量的使用方法解析

简介 MATLAB中常常使用符号变量&#xff0c;这里定义符号变量的函数是syms 使用方法如下 syms x y z 其中&#xff0c;x、y、z 是符号变量&#xff0c;可以是任意字母、数字或下划线组合而成的字符串。 举例1&#xff1a; 代码 以下是一个简单的例子&#xff0c;演示如何…

[论文阅读笔记25]A Comprehensive Survey on Graph Neural Networks

这是一篇GNN的综述, 发表于2021年的TNNLS. 这篇博客旨在对GNN的基本概念做一些记录. 论文地址: 论文 1. 引言, 背景与定义 对于图像数据来说, CNN具有平移不变性和局部连接性, 因此可以在欧氏空间上良好地学习. 然而, 对于具有图结构的数据(例如社交网络 化学分子等)就需要用…

Qt5升级到Qt6分步迁移教程

Qt框架的一个新的长期支持版本6.5最近发布。它为以前的版本引入了许多修复、改进和新功能。有些可能对您的应用程序有用&#xff08;如果不是现在&#xff0c;可能会在将来&#xff09;&#xff0c;因此最好将应用程序迁移到最新版本的框架。 仍然有许多应用程序仍在使用Qt 5&…

SpringDataRedis 使用

1. SpringDataRedis 特点2. 使用 SpringDataRedis 步骤3. 自定义 RedisTemplate 序列化4. SpringDataRedis 操作对象 1. SpringDataRedis 特点 提供了对不同 Redis 客户端的整合&#xff08;Lettuce 和 Jedis&#xff09;提供了 RedisTemplate 统一 API 来操作 Redis支持 Redi…

C++ 改善程序的具体做法 学习笔记

1、尽量用const enum inline替换#define 因为#define是做预处理操作&#xff0c;编译器从未看见该常量&#xff0c;编译器刚开始编译&#xff0c;它就被预处理器移走了&#xff0c;而#define的本质就是做替换&#xff0c;它可能从来未进入记号表 解决方法是用常量替换宏 语言…

Spring Cache的介绍以及怎么使用(redis)

Spring Cache 文章目录 Spring Cache1、Spring Cache介绍2、Spring Cache常用注解2.1、EnableCaching注解2.2、CachePut注解2.3、CacheEvict注解2.4、Cacheable注解 3、Spring Cache使用方式--redis 1、Spring Cache介绍 Spring Cache是一个框架&#xff0c;实现了基于注解的缓…