c++编程(13)——vector的模拟实现

欢迎来到博主的专栏——c++编程
博主ID:代码小豪


文章目录

    • 前言
    • vector的模拟实现
      • vector的成员对象
      • 插入、删除、扩容
      • 访问vector元素
      • 构造函数
    • 填坑:为什么拷贝vector类元素的时候不能用浅拷贝
    • 末尾源代码:

前言

博主目前的水平还不能很明确的描述allocator(空间配置器)的作用和行为,因此在模拟实现vector模板的时候不会模拟实现allocator。但是会尽可能的模拟vector常用的行为以及迭代器原理。

本博客模拟实现的vector参考了SGI版本的STL库,如果大家想要STL的源码,可以私信博主。

文章目录

    • 前言
    • vector的模拟实现
      • vector的成员对象
      • 插入、删除、扩容
      • 访问vector元素
      • 构造函数
    • 填坑:为什么拷贝vector类元素的时候不能用浅拷贝
    • 末尾源代码:

vector的模拟实现

vector是一个顺序表的数据结构,因此STL为vector设计插入,删除等顺序表常用的操作,由于篇幅原因,博主不会详细的介绍这些操作使用的算法原理,毕竟重点在于vector模板的模拟实现,目的在于让大家对vector的使用更加得心应手,而且对泛型编程可以有更多的理解。

vector的成员对象

如果你对顺序表的数据结构非常了解,那么你大概会认为vector当中的成员至少有以下三种:
(1)管理资源的指针
(2)表示元素个数的size
(3)表示容量的capacity

	template<class T>
	class vector
	{
		T* vals;//管理资源的指针
		size_t size;//表示元素个数的size
		size_t capacity;///表示容器容量的capacity
	};

实际上用这三个成员来模拟实现一个vector模板也是可行的,但是SGI版本的STL源码使用了更为精妙的方式。我们不再需要使用抽象的下标size和capacity来观察顺序表的使用状况,而是使用了三个vector模板生成迭代器分别指向顺序表的三个位置:表头,表尾,存储空间的末尾。

class vector
	{
		template<class T>
		typedef T value_type;
		typedef value_type* iterator;
		typedef const value_type* const_iterator;

	private:
		iterator _start=nullptr;//表头
		iterator _finish=nullptr;//表尾
		iterator _end_of_storage=nullptr;//存储空间的末尾
	};

在这里插入图片描述

但如果你打开c++在线文档查查,可以发现c++标准当中是设计了size()和capacity()这两个接口来返回vector对象的元素个数个容量大小的。但是我们的vector的成员是三个迭代器,如果让迭代器体现出vector的size和capacity呢?

我们观察上图,可以发现一个规律,如果让_finish - _start,得到的值就是size,如果让_end_of_storage -_start,得到的值就是capacity。那么迭代器能与迭代器进行加法计算吗?当然可以了,因为vector当中的迭代器实际上就是一个T*的指针(查看上面代码的定义)。那么到此为止,我们已经可以设计c++标准当中的四个接口。分别是begin(),end(),size(),和capacity()。

		iterator begin()
		{
			return _start;
		}

		const_iterator begin()const//begin的重载函数
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end()const//end的重载函数
		{
			return _finish;
		}

		size_t  size()const
		{
			return _finish - _start;
		}

		size_t capacity()const
		{
			return _end_of_storage - _start;
		}

插入、删除、扩容

既然vector的数据结构是一个顺序表,那么它是需要进行数据的插入、删除这两个操作的。插入分为头插、尾插、任意位置的插入这三种方式、而删除也可以分为头删、尾删、和任意位置的删除这三种方式、但是头删和头插由于效率比较低,因此STL当中并没有规定要vector要实现头删和头插这两种操作。但是我们可以通过insert和erase这两个成员函数来进行头插、头删(当然,也可以进行尾插和尾删,之所以保留这两个操作的原因在于:这两个插入方式的效率很高,使用次数也很频繁)。

但是既然要插入数据,那么肯定会遇到容量不够的问题。此时就需要进行扩容了。我们先来实现扩容这个操作。
在这里插入图片描述
在c++标准当中,vector的扩容接口的函数名是reserve。我们也按照c++的标准将模拟实现的vector扩容接口命名为reserve。不过要注意的是,STL当中的vector是用空间配置器(allocator)来进行空间分配的。而博主模拟实现的vector则是用new和delete来管理空间。

	void reserve(size_t n)
		{
			if (n > capacity)//reserve的作用是扩容,而不能用来缩容
			{
				size_t olesize = size();
				iterator tmp = new value_type[n];

				if (_start != nullptr)
				{
					memcpy(tmp, _start, sizeof(value_type) * size());//将原来的数据拷贝到新的地址
					delete[] _start;//释放旧空间
				}

				_start = tmp;//接手新空间
				_finish = _start + oldsize;
				_end_of_storage = _start + n;
			}
		}

博主在扩容这方面使用的是异地扩容(原地扩容需要用到allocator),但是这个扩容函数在逻辑上看起来没问题,实际上存在一个bug,这个bug等待文章的后面再说明吧。我们先用这个reserve来为vector容器进行扩容。

那么接下来我们来实现一下头插和尾插。头插和尾插的原理非常简单,就是在表尾_finish的位置插入或删除一个元素,然后修改_finish,除此之外就是要注意扩容的问题。

void push_back(const value_type& val)
{
	if (_finish == _end_of_storage)//扩容
	{
			size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
			reserve(newcapacity);
	}

		*_finish = val;//尾插元素
		_finish++;
	}

void pop_back()
{
	assert(_finish > _start);
	_finish--;//只需要让_finish往前一格就好了
	//后面插入元素时会将原数据覆盖掉
}

任意位置的插入和删除的函数名字为insert和erase。我们要注意这插入算法和尾插算法实际上是存在一些区别的,主要的区别在于需要让数据发生挪动(这也是尾插和尾删的效率高的原因)。

我们先来看看c++标准中定义的insert原型

single element (1)	
iterator insert (iterator position, const value_type& val);

可以发现c++标准中的insert函数的参数其实是用迭代器来作为position的原型。也就是说我们模拟实现的insert函数需要在position这个位置插入val。注意扩容

在这里插入图片描述

我们仔细观察c++标准中制定的insert函数原型,可以看到insert函数的返回值是一个迭代器,这个迭代器实际上就是position。为什么这个操作需要返回position呢?一个函数竟然要返回用户传入的参数,这可真是奇了怪了。

实际上此position非彼position,我们想想扩容函数reserve干了什么?没错,如果vector容器发生了扩容,这有可能会导致异地扩容。如果异地扩容一旦发生,就会导致迭代器失效的问题(迭代器指向了不再使用的空间,这不就是失效了?),所以c++标准规定insert函数需要返回position的真实位置(即无论发不发生扩容,这个position都是有效的迭代器)。

	void pop_back()
		{
			_finish--;//只需要让_finish往前一个就好了
		}

iterator insert(iterator position, const value_type& val)
{
	assert(position <= _finish);//检测position合法性
	assert(position >= _start);
	size_t oldpos = position - _start;
	if (_start == _finish)//扩容
	{
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newcapacity);
	}

	position = _start + oldpos;//更新position
	iterator end = _finish - 1;
	while (end >= position)//挪动数据
	{
		*(end + 1) = *end;
		end--;
	}

	*position = val;//在position位置插入val
	_finish++;
	return position;
}

erase为了防止迭代器失效也有类似的设计。

iterator erase (iterator position);

在这里插入图片描述

iterator erase(iterator position)
{
	assert(position >= _start);//检测position的合法性
	assert(position < _finish);
	iterator begin = position+1;

	while (begin <= _finish)//挪动数据
	{
		*(begin - 1) = *begin;
		begin++;
	}
	_finish--;

	if (position >= _finish)
	{
		return _finish - 1;
	}
	return position;
}

访问vector元素

我们已经简单的写好了vector的部分模块,那么我们现在最好先测试一下这些模块能否正常工作。请注意一个问题,当我们编写与模板相关的程序时,请多测试一下这些写好的模块,因为模板出现bug是很麻烦的。(博主就是在vector模拟实现的时候写错了一个标识符,找bug找了半小时qwq)。

既然要测试vector能不能好好使用,我们肯定是要遍历一下vector来查看这些函数能不能正常使用。那么写一个用来访问vector元素的函数就至关重要了。

博主这里将operator[] 模拟实现一下,因为这个函数是vector当中最常用的mumber access函数。

value_type& operator[](size_t pos)
{
	return _start[pos];
}

const value_type& operator[](size_t pos)const
{
	return _start[pos];
}

测试案例我就不放出来了,大家也请放心这些源码都是通过测试案例的(我的测试案例哈哈哈,不知道博主有没有天赋去测试岗位--!)。大家如果感兴趣,可以去我的代码仓库看看,博主最近计划将这个专栏写的源代码放进代码仓库里。

构造函数

构造函数一般不是最先写的吗?是的,但是由于博主在模板中对成员对象使用了缺省值,因此编译器的默认构造会将成员对象初始化成缺省值写的那样。这就是在前期我们可以不写构造函数,但是编译器不报错的原因。

为什么将构造函数放在最后面讲解。这是因为博主在设计这部分的时候遇到很有意思的事情,所以像放在后面讲解。实际上,这部分的内容应该本博客最核心的部分。

我们先来看看c++标准当中的constructor是怎样的吧。

default (1)	
explicit vector (const allocator_type& alloc = allocator_type());
fill (2)	
explicit vector (size_type n, const value_type& val = value_type(),                 const allocator_type& alloc = allocator_type());
range (3)	
template <class InputIterator>         
vector (InputIterator first, InputIterator last,                 const allocator_type& alloc = allocator_type());
copy (4)	
vector (const vector& x);

我们忽略掉哪些allocator。

default构造很简单,甚至不需要写任何东西。因为我们在设计成员对象的时候就为其设计了缺省值。

vector() = default;//这个default的意思是,函数体内什么都不写,但是这个类是有默认构造的

填充(fill)构造则是为vector填充n个val,而缺省值value_type()则是一个匿名对象。(关于匿名对象,博主将其放在c++杂谈当中说明)


vector(size_t n, const value_type& val = value_type())//fill
{
	while (n--)
	{
		push_back(val);
	}
}

范围(range)构造,就是将这个迭代区间内的所有元素按照顺序放置进vector容器中。

template<class inputiterator>
vector(inputiterator first,inputiterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

这部分的内容也非常好理解,我们将送入构造函数的迭代器的数据插入至vector当中,整个代码的逻辑是没有问题的。但是编译器偏偏就报错。

error C2100 非法的间接寻址 myvector D:\code\test-c123\myvector\vector.h 30

博主这是一顿冥思苦想啊,这个指针不是空指针,也不是野指针,怎么就非法寻址了呢?最后上网搜了一下导致的原因,恍然大悟了。

非法的间接寻址这个报错其实挺常见的。当初在C语言写scanf的时候就会看到这么一种情况

char ch;
scanf("%c",ch);//error,非法的间接寻址

这是什么原因。ch漏了一个取地址符号。一个char类型的变量是不能当做指针来用的。

但是这是一个构造函数啊,inputiterator其实就是一个迭代器,绝对是一个指针啊。但是别忘了,这是模板,是泛型编程,编译器可不管这个inputiterator到底是不是指针,只要有类型对的上,编译器就把模板套用了,还记不记得我们写的fill构造。

vector(size_t n, const value_type& val = value_type())//fill
{
	while (n--)
	{
		push_back(val);
	}
}

这个构造函数也没有什么问题啊,逻辑和类型都是正确的,但还是那句话,这是一个类模板,是泛型编程,在这个领域,我们要时刻主要调用函数时传入的类型。比如我们使用fill构造时。如果这么调用fill构造

vector<int> v3(4, 5);//fill构造

这样调用没问题啊。真的吗?我们再来看看range构造的模板

template<class inputiterator>
vector(inputiterator first,inputiterator last)

哦,原来是编译器把
在这里插入图片描述
解释成调用range构造了。因为类型很正确嘛。把inputiterator解释成int。很符合模板。但如此一来,就造成非法的间接寻址了。

那么解决方法是什么?
(1)将函数参数的类型强制转换成(size_t,int),这样编译器就不会解释为(int,int)。
在这里插入图片描述
但是记住一个原则,那就是不要让使用者去考虑太多。如果STL库需要使用者顾忌这个顾忌那个的,那么STL也不会被这么多人使用。

于是就考虑解决方法(2)了,那就是暴力的重载多个fill构造,把可能导致错误解释的情况考虑进去

vector(size_t n, const value_type& val = value_type())//fill
{
	while (n--)
	{
		push_back(val);
	}
}

vector(int n, const value_type& val = value_type())//重载版本
{
	while (n--)
	{
		push_back(val);
	}
}

这样子编译器会调用重载版本的构造函数(因为编译器会优先选择类型匹配的,虽然inputiterator解释成int也很匹配。但是他毕竟要先解释了再用,那么不需要解释就匹配的函数编译器会更加青睐)。

OK,问题(1)就这么解决了。

最后让我们解决copy赋值函数

vector(const vector& v)
{
	for (auto& e: v)
	{
		push_back(e);
	}
	_end_of_storage = _start + v.capacity();

}

拷贝构造的原理很简单,遍历待拷贝的对象v,将对象v的所有元素尾插到vector当中。完成拷贝构造。

有人也许会感到疑惑,因为在拷贝构造当中我们是可以是memcpy将v中的元素进行拷贝的。但是为什么我不这样做呢?不知道大家还记不记得博主在reserve函数提到过这个实现存在一个bug。造成bug的原因就在于这个memcpy函数上。

填坑:为什么拷贝vector类元素的时候不能用浅拷贝

这里简单的说说什么是浅拷贝。

浅拷贝:也称为值拷贝,指的是将某个内存空间中的所有位(bit),拷贝到另外一个空间上去,显然memcpy就是一个典型的浅拷贝,而浅拷贝会带来数据隐患。

我们先来分析一下为什么vector的元素不能浅拷贝。实际上更准确的描述是,vector的部分类型的元素是不能浅拷贝的。比如string类。我们可以尝试一下让vector<string>发生扩容。

vector<string>v1;
v1.push_back("1111111111111111111111111");
v1.push_back("1111111111111111111111111");
v1.push_back("1111111111111111111111111");
v1.push_back("1111111111111111111111111");
cout << "第一次遍历元素的结果:" << endl;
for (auto& e : v1)
{
	cout << e << ' ';
}
cout << endl;
v1.push_back("1111111111111111111111111");//这次尾插扩容

cout << "第二次遍历元素的结果:" << endl;
for (auto& e : v1)
{
	cout << e << ' ';
}

我们多次尾插string元素,让v1发生扩容,然后运行这段程序,我们会看到一个非常奇怪的结果。
在这里插入图片描述
可以发现,vector<string>的部分元素变成了乱码。我们打开监视窗口看一看。
在这里插入图片描述
v1的内存似乎失效了。这其实就是因为浅拷贝vector元素导致的。我们可以画图分析一下reserve函数的核心代码(即执行扩容操作的那部分)。
在这里插入图片描述

一个string对象可以认为具有以下成员
(1)管理资源的指针(char*)
(2)元素个数size
(3)容量capacity
在这里插入图片描述

那么v1在扩容前的内存映像应该是这样:
在这里插入图片描述
现在开始扩容了,我们执行第一条语句,让tmp开辟一个更大的空间
在这里插入图片描述

在这里插入图片描述
然后使用memcpy进行浅拷贝
在这里插入图片描述

在这里插入图片描述
最后释放旧的空间,让_start接手新的空间·。
在这里插入图片描述
在这里插入图片描述
这就导致了很尴尬的情况,扩容的时候竟然将元素的内容丢失了,这肯定不是符合逻辑的使用效果,既然浅拷贝不适合vector的元素,那么就考虑用深拷贝。

深拷贝的原理是这样:深拷贝不是单纯的拷贝元素的值(bit),而是使用元素的副本。达到拷贝的效果
在这里插入图片描述
这样即使释放掉原来的空间,也不会导致扩容后的元素内容发生丢失。

所以最好的解决方法就是深拷贝vector的元素。

void reserve(size_t n)
{
	if (n > capacity())//reserve的作用是扩容,而不能用来缩容
	{
		size_t oldsize = size();
		iterator tmp = new value_type[n];

		if (_start != nullptr)
		{
			//memcpy(tmp, _start, sizeof(value_type) * size());
			//不再使用memcpy的浅拷贝,转而使用下面这种深拷贝
			int i = 0;
			for (auto& e : *this)
			{
				tmp[i++] = e;
			}
			delete[] _start;//释放旧空间
		}

		_start = tmp;//接手新空间
		_finish = _start + oldsize;
		_end_of_storage = _start + n;
	}
}

实际上不止reserve函数不能用到浅拷贝,而是整个vector容器当中都不能使用浅拷贝。这也是博主在拷贝构造、或者其他与拷贝有关的函数时,博主宁愿使用push_back都不适用memcpy。

其他容器的模拟实现也会出现这种场景,但是博主不会再详细的解释不能浅拷贝的原因了。

末尾源代码:

博主将源代码和测试案例放到代码仓库当中了,博主会创建一个单独的仓库来放置这些博客当中的源码。
代码小豪的仓库

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CV之Nougat:Nougat(一种基于神经网络实现OCR功能的视觉转换器模型)的简介、安装和使用方法、案例应用之详细攻略

CV之Nougat&#xff1a;Nougat(一种基于神经网络实现OCR功能的视觉转换器模型)的简介、安装和使用方法、案例应用之详细攻略 目录 相关论文 《Nougat: Neural Optical Understanding for Academic Documents》的翻译与解读 Nougat的简介 Nougat的安装和使用方法 1、安装 …

短视频拍摄方式有哪些:四川鑫悦里文化传媒有限公司

​短视频拍摄方式有哪些 在数字化时代&#xff0c;短视频以其短小精悍、传播迅速的特点&#xff0c;成为了人们表达自我、分享生活的重要工具。然而&#xff0c;想要制作出引人入胜的短视频&#xff0c;除了创意和构思&#xff0c;拍摄方式的选择也至关重要。四川鑫悦里文化传…

JavaEE:Servlet创建和使用及生命周期介绍

目录 ▐ Servlet概述 ▐ Servlet的创建和使用 ▐ Servlet中方法介绍 ▐ Servlet的生命周期 ▐ Servlet概述 • Servlet是Server Applet的简称&#xff0c;意思是 用Java编写的服务器端的程序&#xff0c;Servlet被部署在服务器中&#xff0c;而服务器负责管理并调用Servle…

香橙派KunpengPro测评之使用C语言操控40pin引脚

香橙派KunpengPro测评之使用C语言操控40pin引脚 香橙派KunpengPro介绍香橙派实物图香橙派登录界面香橙派KunpengPro的登录界面香橙派KunpengPro的原始桌面香橙派KunpengPro内安装了VScode等软件香橙派KunpengPro的终端 香橙派硬件参数核心性能图形与显示接口丰富性扩展与兼容性…

2024年中国金融行业网络安全研究报告

网络安全一直是国家安全的核心组成部分&#xff0c;特别是在金融行业&#xff0c;金融机构拥有大量的敏感数据&#xff0c;包括个人信息、交易记录、财务报告等&#xff0c;这些数据的安全直接关系到消费者的利益和金融市场的稳定&#xff0c;因此金融行业在网络安全建设领域一…

短道速滑短视频:四川京之华锦信息技术公司

短道速滑短视频&#xff1a;冰雪激情的视觉盛宴 随着冬奥会的热度不断攀升&#xff0c;短道速滑作为其中一项紧张刺激、充满观赏性的运动&#xff0c;受到了越来越多人的关注。而在社交媒体和短视频平台的助力下&#xff0c;短道速滑短视频成为了人们了解、欣赏这项运动的新窗…

MPLS原理与配置

1.MPLS概述 &#xff08;1&#xff09;传统IP路由转发 &#xff08;2&#xff09;MPLS基本概念 ⦁ MPLS起源于IPv4&#xff08;Internet Protocol version 4&#xff09;&#xff0c;其核心技术可扩展到多种网络协议&#xff0c;包括IPv6&#xff08;Internet Protocol ver…

LFSR线性反馈移位寄存器及Verilog实现

一、LFSR LFSR线性反馈移位寄存器&#xff0c;通常由移位寄存器和异或门组成&#xff0c;主要用于产生伪随机序列等。 线性反馈的含义是各个寄存器的输出通过一个反馈函数连接到第一级触发器的输入&#xff1b;LFSR中的寄存器的个数被称为LFSR的级数。 LFSR分为两类&#xff…

【机器学习】K-近邻算法(KNN)全面解析

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 K-近邻算法&#xff08;KNN&#xff09;全面解析概述1. 基本概念与原理1.1 KNN算…

【Vue学习记录】如何通过Vue-cli创建并启动一个项目}}}}}}

1.安装node.js 略 2.安装npm.js 略 3.安装Vue 略 4.安装Vue.cli 略 步骤1-4 参考&#xff1a; 最详细的vue安装教程 5.在Vue.cli里创建一个新项目 略 6.启动这个新项目 略 步骤5-6 参考&#xff1a; 五分钟教你使用vue-cli3创建项目&#xff08;三种创建方式&#…

卢文岩博士受邀参与中国科学院大学校友论坛 解码DPU核心价值

近日&#xff0c;第五届中国科学院大学校友创新论坛正式举行&#xff0c;本次论坛聚焦科技前沿领域&#xff0c;旨在搭建高端对话平台&#xff0c;促进产学研深度融合。在大算力时代——AI技术前沿沙龙上&#xff0c;中科驭数高级副总裁、CTO卢文岩博士受邀分享《DPU——连接算…

【马蹄集】— 百度之星 2023

BD202301公园 可以先 b f s bfs bfs统计一下 t , f t,f t,f到达每个点的距离&#xff0c;还有 n n n到达其他点的距离即其他点到达 n n n的距离 然后枚举每个点 直接计算得到最小值即可 注意特判有人无法到达 n n n的情况 code: #include<bits/stdc.h> #define endl \n…

MaxKB,基于 LLM 大语言模型的知识库问答系统,开箱即用,支持快速嵌入到第三方业务系统。

MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB Max Knowledge Base&#xff0c;旨在成为企业的最强大脑。开箱即用&#xff0c;支持快速嵌入到第三方业务系统。 项目链接 github.com/1Panel-dev/MaxKB 项目介绍 MaxKB 是一款基于 LLM 大语言模型的知识库问答系统…

C++代码使用ClangCL编译注意事项

遇到cmake指定模板类工程使用msvc的clang编译器编译代码&#xff0c;代码变量出现与预期不符的问题&#xff1b; 如下&#xff1a; clangcl将实现放到头文件里则不会出现这样的情况&#xff1b; 最后按照pcl的模板类写法则解决这个问题&#xff1b;

WPF之TextBlock文本标签

TextBlock: 用于显示文本内容 常用属性 Text设置展示的文本fontsize设置字体大小FontWeight设置字体粗细FontFamily设置字体样式 实例 <Grid><TextBlock Text"显示文本"FontSize"10"FontWeight"Bold"Foreground"red">&l…

【博客20】缤果Matlab串口调试助手V1.0(中级篇)

超级好用的Matlab串口调试助手 开发工具: MATLAB 2024a中文版 (编程语言matlab) 目录 前言 一、软件概要&#xff1a; 二、软件界面&#xff1a; 1.App演示 ​ ​---- ◇♣♡♠ ---- 2.其他扩展App展示 ​编辑 三、获取 >> 源码以及Git记录&#xff1a; 总结 前…

[STM32+HAL]LD-1501MG舵机二次开发指南

一、准备材料 核心板&#xff1a;STM32F407ZGT6 舵机&#xff1a;LD-1501MG数字舵机 控制器&#xff1a;24路PWM舵机控制器 二、HAL库配置 开启串口与控制器通信 三、Keil填写代码 1、Servo.c #include "Servo.h" #include "usart.h"uint8_t Message…

计算机毕业设计 | springboot养老院管理系统 老人社区管理(附源码)

1&#xff0c;绪论 1.1 背景调研 养老院是集医疗、护理、康复、膳食、社工等服务服务于一体的综合行养老院&#xff0c;经过我们前期的调查&#xff0c;院方大部分工作采用手工操作方式,会带来工作效率过低&#xff0c;运营成本过大的问题。 院方可用合理的较少投入取得更好…

微前端探秘:初识微前端

微前端探秘&#xff1a;初识微前端 什么是微前端微前端的好处现有的微前端方案 什么是微前端 微前端是指存在于浏览器中的服务&#xff0c;其借鉴了微服务的架构理念&#xff0c;将微服务的概念扩展到了前端。 如果对微服务的概念比较陌生的话&#xff0c;可以简单的理解为微前…

媒体发布会怎么邀请媒体,到场采访报道?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体发布会的邀请和组织是一个需要精心策划的过程&#xff0c;以下是一些基本步骤和建议&#xff0c;以确保媒体发布会能够吸引媒体到场并进行有效的采访报道&#xff1a; 明确宣传目的和…