【C++】list的使用和模拟实现

news2024/10/7 16:26:09

目录

  • 1.什么是list
  • 2.list的一些接口
  • 3.list的模拟实现
    • 3.1 迭代器
    • 3.2 list主体
      • 3.2.1 构造函数
      • 3.2.2 拷贝构造、赋值重载
      • 3.2.3 主体内引入迭代器
      • 3.2.4 insert和erase
      • 3.2.5 clear和析构函数
    • 3.3 const迭代器的实现
    • 3.4 实现迭代器的operator->
  • 4.总结list迭代器的实现

1.什么是list

list的底层是一个双向带头循环链表,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素,其遍历只能通过迭代器来实现,范围for的底层也是迭代器。
迭代器是所有容器都可以使用的迭代方式。
与list类似的还有forward_list,底层是单链表,只能朝前迭代,以让其更简单高效。
与vector相比,list在任意位置的插入或删除效率更高,不需要去移动数据。但是其最大的缺陷是不支持随机访问,想要访问list某一个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,这一块的时间开销较大。
在这里插入图片描述

2.list的一些接口

在这里插入图片描述
这里的接口看起来都很熟悉,使用起来和前面的string,vector的接口也没有什么差别。
assign:在这里插入图片描述
list的assign支持两种使用方式,第一种是利用迭代器来进行切片,第二中则是将链表修改为n个val值。

void test1
{
	list<int> l1;
	list<int> l2;
	l1.assign(5, 10);
	l2.assign(l1.begin(), l1.end());
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : l2)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述
clear:将除了头节点以外的所有节点全部释放。与析构函数不同,析构会释放包括头节点在内的全部节点。

sort:链表单独提供了一个排序的函数,为什么不直接使用算法库中的sort呢?
在这里插入图片描述
这是算法库中的sort,我们可以看到其中的参数类型是RandomAccessIterator,意思是支持随机访问的迭代器,这里就要谈到迭代器的分类,迭代器可分为单向迭代器(单链表,哈希表)、双向迭代器(双向链表),随机访问迭代器(顺序表)。至于为什么算法库中的sort只能支持随机访问的迭代器,这就要从其底层实现说起了。
algorithm中的sort是利用快排实现的,而快排中需要三数取中,由于链表中的迭代器并不连续,所以不支持这种运算。list中的sort采用归并排序来解决了这个问题。

remove:删除list中第一个值为val的元素在这里插入图片描述

void test2()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.remove(3);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述
splice:将一个链表中的一段区间转移到另一个链表的指定位置。
在这里插入图片描述

void test3()
{
	list<int> l1;
	list<int> l2;
	l1.push_back(1);
	l1.push_back(2);
	l1.push_back(3);
	l1.push_back(4);
	l2.push_back(5);
	l2.push_back(6);
	l2.push_back(7);
	list<int>::iterator it = ++l1.begin();
	l1.splice(it, l2);
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
}

unique:用于链表的去重,要求去重前链表必须有序,否则会发生错误。

merge:合并两个链表。

void test4()
{
	list<int> l1;
	list<int> l2;
	l1.push_back(1);
	l1.push_back(2);
	l1.push_back(3);
	l1.push_back(4);
	l2.push_back(5);
	l2.push_back(6);
	l2.push_back(7);
	l1.merge(l2);
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述
list中的迭代器失效:
对于list,insert并不会使迭代器失效,因为list中的节点都有单独的空间,节点并不连续,在进行扩容操作的时候只需要申请新空间,不需要进行异地扩容释放原有的空间。而erase仍然会导致迭代器失效,,erase之后会释放该节点的空间,必然会导致pos指向已经释放的空间,想要继续使用可以用erase的返回值来更新pos。

3.list的模拟实现

3.1 迭代器

在vector中,我们说把迭代器当作一个指针,模拟实现的时候也是利用typedef将指针重命名实现迭代器,但在list中,我们发现迭代器没法这样实现,比如迭代器的+±-,想要通过++找到下一个位置需要连续的空间,而我们又知道list中每一个节点不是连续的,这样++不就没法找到下一个位置了吗?但通过前面类和对象的学习,这点问题难不倒我们,可以用运算符重载将++的行为改变,不是指向当前地址+1,而是指向下一个节点。这样我们就需要用类或结构体将这个指针封装起来,通过重载来改变这个指针的++,–,解引用等行为。
现在来模拟实现一下:

首先我们要先构建节点的结构体:

template<class T>
struct list_node
{
	list_node<T>* _next;	// 下一个节点的地址
	list_node<T>* _prev;	// 前一个节点的地址
	T _val;					// 值

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

迭代器的实现:

template<class T>
struct __list_iterator
{
	typedef list_node<T> node;
	typedef __list_iterator<T> self;
	node* _node;	// 唯一的成员变量:节点的指针

	__list_iterator(node* n)
		:_node(n)
	{ }

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

	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迭代器的基本构建,接下来就来写一下list的各种函数。

3.2 list主体

3.2.1 构造函数

这里设定list的成员变量为头节点的指针。
无参构造:new一个头节点,然后将该节点的prev和next都指向自己。

template<class T>
class list
{
	typedef list_node<T> node;
public:
	list()
	{
		_head = new node;
		_head->_next = _head;
		_head->_prev = _head;
	}
private:
	node* _head;
};

以迭代器区间作为参数的构造:使用模板实现

template <class Iterator>
list(Iterator first, Iterator last)
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;

	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

这里发现有些代码重复,不妨将其写在另一个函数里,提高复用率。

void empty_init()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

list()
{
	empty_init();
}

template <class Iterator>
list(Iterator first, Iterator last)
{
	empty_init();

	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

3.2.2 拷贝构造、赋值重载

这里就直接写现代写法了,可读性高,先实现交换函数,再通过临时变量与自身交换。

// 拷贝构造
void swap(list<T>& tmp)
{
	std::swap(_head, tmp._head);
}

list(const list<T>& lt)
{
	empty_init();
	list<T> tmp(lt.begin(), lt.end());
	swap(tmp);
}

//赋值重载
list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

3.2.3 主体内引入迭代器

依旧通过typedef引入

typedef __list_iterator<T> iterator;

iterator begin()
{
	return iterator(_head->_next);
}

iterator end()
{
	return iterator(_head);		// 最后一个节点的下一个节点,就是头节点
}

3.2.4 insert和erase

insert和erase的操作应该都非常熟悉了,在前面数据结构学习的时候练习了不少。

// 因为insert不会引发迭代器失效,这里就没有给它设返回值
void insert(iterator pos, const T& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* new_node = new node(x);
	
	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

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

push_back,pop_back等只需复用insert和erase即可,这里不做赘述。

3.2.5 clear和析构函数

clear:将除了头节点以外的节点全部释放,通过erase和迭代器来一个一个释放。

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

析构函数实现时,可不敢直接delete[] _head,因为节点不是连续的,这样只删掉了头节点,其他节点没有删掉,造成内存泄漏。
只要先用clear将其他节点先释放,在释放头节点即可。

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

3.3 const迭代器的实现

首先,可不敢直接用const修饰iterator,这样会导致iterator无法++和–,因为我们需要的const迭代器是指iterator指向的内容不可变,而不是iterator不可变。
实现const迭代器和普通迭代器不同的地方就只有operator*这个函数上了,const迭代器需要的是不可改变的返回值。于是就写出了下面一段代码

template<class T>
struct __list_const_iterator
{
	typedef list_node<T> node;
	typedef __list_const_iterator<T> self;
	node* _node;

	__list_const_iterator(node* n)
		:_node(n)
	{ }

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

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

上述代码和普通迭代器相比只增加了几个const,却又要重新写这样一大堆代码,显得很冗余,怎么才能使它变得简洁一些呢?
可以通过增加模板参数,通过手动填入参数来选择你是普通迭代器还是const迭代器。
增加了一个Ref模板参数,这样当Ref=T&时就是普通迭代器,Ref=const T&时就是const迭代器。

template<class T, class Ref>
struct __list_iterator
{
	typedef list_node<T> node;
	typedef __list_iterator<T, Ref> self;
	node* _node;

	__list_iterator(node* n)
		:_node(n)
	{ }

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

	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中加入这样一行代码,就可以尽情使用const迭代器了。

typedef __list_iterator<T, const T&> const_iterator;

3.4 实现迭代器的operator->

当list的中元素的类型为自定义类型时,打印*it会报错,因为该自定义类型没有重载流插入,但是要重载的话就必须每写一个自定义类型都重载一个流插入,很麻烦。还可以用(*it).x的方式来获取,不过这种方式不太符合平常的使用习惯,平常面对这种情况更喜欢去用->,所以我们需要在迭代器中重载一个operator->()。

struct Point
{
	int x = 0;
	int y = 0;
};

void list_test6()
{
	list<Point> lt;
	lt.push_back(Point());
	list<Point>::iterator it = lt.begin();
	cout << *it << endl;		// 会报错,Point没有重载流插入
}

operator->()的实现:

T* operator->()
{
	return &_node->_val;		//node->_val就是那个结构体,返回结构体的指针
}

根据这样的实现方式,在使用时应该是像“it->->x”这样使用,但实际上只需要一个->,是因为编译器为了可读性省略掉了一个->。
再考虑到const迭代器,因为->也是要返回内容,所以要保障返回值不可修改,所以再像前面一样添加一个模板参数Ptr实例化时根据T和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* n)
		:_node(n)
	{ }

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

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

	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代码:

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

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

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

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

	void empty_init()
	{
		_head = new node;
		_head->_next = _head;
		_head->_prev = _head;
	}

	list()
	{
		empty_init();
	}

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

	list(const list<T>& lt)
	{
		empty_init();
		list<T> tmp(lt.begin(), lt.end());
		swap(tmp);
	}

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

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

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

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

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

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

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

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

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

private:
	node* _head;
};

4.总结list迭代器的实现

通过list迭代器的实现,我们知道不能把迭代器简单理解为一个指针,而是通过运算符重载等把它包装成一个行为和指针极为相似的东西,让不论底层数据结构是怎样的容器都能够通过迭代器来实现读写数据,由此可以感受到类封装的强大。

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

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

相关文章

领域驱动应用架构实践

一个合适的应用架构不仅能促使项目朝着好的方向发展&#xff0c;易于维护&#xff0c;也能指导团队成员有效协作。 DDD是站在领域的角度来驱动应用架构的落地&#xff0c;接下来将介绍一种落地方案。 架构分层 首先在架构层次方面&#xff0c;在遵循DDD的分层架构模式的同时&…

STM32单片机(六)TIM定时器 -> 第五节:TIM输入捕获

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

Nucleo-F411RE (STM32F411)LL库体验 4 -Letter Shell移植与调试

Nucleo-F411RE &#xff08;STM32F411&#xff09;LL库体验 4 -Letter Shell移植与使用 1、串口的初始化 Nucleo-F411RE自带st-link&#xff0c;并支持虚拟串口的功能&#xff0c;根据原理图&#xff0c;st-link的rx tx接到了Nucleo-F411RE的PA2 PA3&#xff0c;所以我们要初…

以太网MII、RMII、GMII、RGMII(三)

目录 一、MII 二、RMII 三、GMII 四、RGMII 以太网硬件主要包括OSI的最下面两层&#xff0c;物理层和数据链路层&#xff0c;前者的芯片为PHY&#xff0c;后者的芯片为MAC控制器。而MAC与PHY之间的常用的数据传输接口有MII、RMII、GMII、RGMII。 模式 时钟 位宽 速率 M…

pytorch笔记:transformer 源码

来自B站视频&#xff0c;API查阅&#xff0c;TORCH.NN seq2seq 可以是 CNN&#xff0c;RNN&#xff0c;transformer nn.Transformer 关键源码&#xff1a; encoder_layer TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout,activation, layer_norm_eps, ba…

5.vue3医疗在线问诊项目 - _极速问诊-前置准备 ==> 需求分析、枚举类型、pinia仓库的初始化

5.vue3医疗在线问诊项目 - _极速问诊-前置准备 > 需求分析、枚举类型、pinia仓库的初始化 极速问诊-需求分析{#consult-product} 极速问诊阶段流程分析 线下看病流程&#xff1a; 选择医院&#xff08;三甲、普通&#xff09;》挂号》选择科室 》选择医生&#xff08;专家…

牛客网专项练习——C语言错题集(5)

文章目录 指针的值指针与数组、函数的组合空结构体* 和 的优先级 指针的值 指针的值是一个地址&#xff0c;题目中的字符串 “girl” 应该是 *p 的值&#xff0c;即指针 p 所指地址存储的数据的值。 指针与数组、函数的组合 int *p[n] 等价于 int (*)p[n]&#xff0c;是一个…

xinput1_3.dll丢失怎么办?xinput1_3.dll丢失的修复方法

xinput1_3.dll是电脑文件中的dll文件&#xff08;动态链接库文件&#xff09;。如果计算机中丢失了某个dll文件&#xff0c;可能会导致某些软件和游戏等程序无法正常启动运行&#xff0c;并且导致电脑系统弹窗报错。 在我们打开软件或者游戏的时候&#xff0c;电脑提示xinput1_…

STM32 Proteus仿真自动刹车系统超声波测距电机控制-0042

STM32 Proteus仿真自动刹车系统超声波测距电机控制-0042 Proteus仿真小实验&#xff1a; STM32 Proteus仿真自动刹车系统超声波测距电机控制-0042 功能&#xff1a; 硬件组成&#xff1a;STM32F103C6单片机 LCD1602显示器HCSR04超声波传感器按键(加 减)电机蜂鸣器 1.单片机…

学习Angular的编程之旅

目录 1、简介 2、特点 2.1 横跨多种平台 2.2 速度与性能 2.3 美妙的工具 3、Angular 应用&#xff1a;知识要点 3.1 组件 3.2 模板 3.3 依赖注入 4、与其他框架的对比 1、简介 Angular 是一个应用设计框架与开发平台&#xff0c;旨在创建高效而精致的单页面应用。 A…

Java(二):Spring Boot 项目-文件的增删改查下载

Spring Boot 项目-文件的增删改查下载 准备docker运行mysql设置MySQL时区查看当前MySQL使用的时区MySQL建库建表 定义两个实体类数据表实体类查询条件实体类 工具类com/example/user/utils/FileUtil.java 用到的SQL语句mapper user/src/main/resources/mapper/FileTableDao.xml…

基于SSM框架的Java的医院管理系统的设计与实现+文档

博主介绍&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 项目名称 基于SSM框架的Java的医院管理系统的设计与实现文档 视频演示 视频去哪了呢&#xff1f;_哔哩哔哩_bilibili 系统介绍 摘 要 随着互联网技术的快速进…

ARM-驱动

字符设备驱动的内部实现&#xff1a; 文件存在文件系统中&#xff0c;会有一个标识inode号&#xff0c;基于这个标识找到了struct_inode结构体&#xff08;保存当前文件信息&#xff09;&#xff0c;struct_inode结构体中有一个struct cdev *i_cdev类型的字符设备指针&#x…

糖基化修饰1240252-34-9,Fmoc-Thr((Ac4Galβ1-3)Ac3GlcNAcβ1-6AcGalNAcα)-OH,反应特点及性质研究

文章关键词&#xff1a;糖化学试剂&#xff0c;多肽合成&#xff0c;Fmoc-保护氨基酸&#xff0c;糖基化修饰 Fmoc-Thr((Ac4Galβ1-3)Ac3GlcNAcβ1-6AcGalNAcα)-OH &#xff08;文章编辑来源于&#xff1a;西安凯新生物科技有限公司小编WMJ&#xff09;​ 一、Product stru…

Golang每日一练(leetDay0098) 生命、Nim、猜数字游戏

目录 289. 生命游戏 Game Of Life 292. Nim 游戏 Nim Game 299. 猜数字游戏 Bulls and Cows &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 289. 生命游戏 Game Of L…

牛客小白月赛56

今天无聊vp了一下 A.省略 B.最优肯定是全部都是1 C.直接统计每个余数下可以填多少个数&#xff0c;然后排序从小到大的排序输出即可 #include <iostream> #include <cstring> #include <algorithm> #include <vector> #include <queue> #inc…

Linux yum常用命令

Linux服务器安装成功后&#xff0c;我们会经常使用yum安装rpm包以满足使用的需要。使用yum源安装rpm包有两种方式&#xff1a; 方式一&#xff1a; 搭建本地yum源环境&#xff0c;进行rpm包的安装&#xff0c;具体搭建方式&#xff0c;参考(chapter-5)&#xff1a; Linux常规…

Android12之如何查看hidl服务(一百五十五)

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

关闭网络钓鱼攻击 - 类型、方法、检测、预防清单

在当今互联互通的世界中&#xff0c;数字通信和交易占主导地位&#xff0c;网络钓鱼攻击已成为一种无处不在的威胁。 通过伪装成可信赖的实体&#xff0c;网络钓鱼攻击欺骗用户和组织泄露敏感信息&#xff0c;例如密码、财务数据和个人详细信息。 网络钓鱼攻击是网络罪犯使用…

【SpringCloud入门】-- 认识SpringCloudAlibabaNacos服务注册和配置中心

目录 1.Nacos是什么&#xff1f; 2.Nacos能干什么&#xff1f; 3. 各种服务注册中心比较 4.Nacos安装与运行 5.介绍一下Nacos图形化界面 6.NameSpace&#xff0c;Group&#xff0c;Data ID三者的关系&#xff1f;为什么这样设计&#xff1f; 7.Nacos集群和持久化配置 前…