【C++】list 的模拟实现

news2024/11/6 3:02:34

​🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉前言👈
    • 👉节点的创建👈
    • 👉list 的构建👈
      • list 的框架
      • 无参的构造函数
      • push_back 和 push_front
      • 正向迭代器
      • insert 和 erase
      • pop_back 和 pop_front
      • clear 和 析构函数
      • 拷贝构造
      • 赋值运算符重载
      • 用 n 个 val 来构造对象
      • size 和 empty
      • 类名和类型
      • front 和 back
      • 完整代码
    • 👉vector 和 list 的对比👈
    • 👉总结👈

👉前言👈

上一篇博客介绍了 list 的基本使用,那么本篇博客就带着大家来模拟实现 list。模拟实现 list 之前需要注意几个问题:第一,为了避免和库函数产生命名冲突,我们需要将我们的代码封装在命名空间里。第二,我们模拟实现的 list 是带哨兵位头节点的双向循环链表。

👉节点的创建👈

链表是一个节点连接着一个节点的,所以我们首先要将节点创建出来。

namespace Joy
{
	template <class T>
	struct list_node
	{
		list_node* _prev;
		list_node* _next;
		T _data;
	
		list_node(const T& val = T())
			: _prev(nullptr)
			, _next(nullptr)
			, _data(val)
		{}
	};
}

注:因为节点存储的数据可以是内置类型,也可以是自定义类型,所以我们要讲节点定义成模板类。还有就是以下的代码都是封装在命名空间里。

👉list 的构建👈

list 的框架

我们已经将节点定义好了,那么我们现在就来搭建 list 的基本框架。因为我们实现的是带哨兵位头节点的双向循环链表,所以 list 的成员变量只需要哨兵位的头节点 _head就行了。

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

注:为了方便使用list_node<T>,我们可以将其重命名为node。以下的函数接口均是 public 修饰。

无参的构造函数

无参的构造函数主要是申请哨兵位的头节点,然后该哨兵位头节点的_prev_next都指向自己。

// ...
list()
{
	_head = new node;	// 申请一个哨兵位头节点
	_head->_next = _head;
	_head->_prev = _head;
}
// ...

push_back 和 push_front

因为_head->_prev就是尾结点,所以根据双向循环链表的特性,我们就很容易将尾插函数写出来。

在这里插入图片描述

// ...
void push_back(const T& val)
{
	node* tail = _head->_prev;
	node* newnode = new node(x);
	// _head   tail   newnode
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->next = _head;
}
// ...
void push_front(const T& x)
{
	node* head = _head->_next;
	node* newnode = new node(x);

	// _head  newnode   head
	_head->_next = newnode;
	newnode->_prev = _head;
	newnode->_next = head;
	head->_prev = newnode;
}

正向迭代器

迭代器是类的内嵌类型,所以我们使用迭代器时需要指定类域。迭代器用起来像是指针,支持解引用,++ 和 - -
其底层的实现不一定是原生指针,而 vector 的正向迭代器的底层就是原生指针。因为 vector 的空间是连续的,++ 和 - - 就能够找到后一个数据和前一个数据 。而链表的空间是不连续的,那么 ++ 和 - - 就不能找到后一个数据和前一个数据了。但是,我们又想支持这样的使用方法,那怎么办呢?我们可以将迭代器封装成一个类,然后利用运算符重载来支持 ++ 和 - - 的用法。

以上的做法也是 stl 源码中的实现方式。

在这里插入图片描述

在这里插入图片描述

// 像指针一样的对象
template <class T, class Ref, class Ptr>
struct __list_iterator
{
	typedef list_node<T> node;
	typedef __list_iterator<T, Ref, Ptr> Self;	// Ref是T&,Ptr是T*,Self是迭代器
	node* _pnode;	// 正向迭代器的成员变量,_pnode是指向节点的指针

	__list_iterator(node* pnode = nullptr)
		: _pnode(pnode)
	{}

	// 因为T有可能是自定义类型,所以返回值设置为引用Ref,可以减少拷贝构造
	Ref operator*() const
	{
		return _pnode->_data;	// 返回节点的数据
	}

	// 比较节点的指针是否不相等即可
	bool operator!=(const Self& it) const
	{
		return _pnode != it._pnode;
	}

	// 比较节点的指针是否相等即可
	bool operator==(const Self& it) const
	{
		return _pnode == it._pnode;
	}

	// 返回节点数据的地址
	Ptr operator->()
	{
		return &(operator*());
	}

	// 因为链表是通过指针连接起来的,那么_pnode = _pnode->_next就相当于迭代器++
	// ++it,前置++的返回值为++之后的值
	Self& operator++()
	{
		_pnode = _pnode->_next;
		return *this;
	}

	// it++,后置++的返回值为++之前的值
	Self operator++(int)
	{
		Self tmp(*this);
		_pnode = _pnode->_next;
		return *this;
	}

	// --it
	Self& operator--()
	{
		_pnode = _pnode->_prev;
		return *this;
	}

	// it--
	Self operator--(int)
	{
		Self tmp(*this);
		_pnode = _pnode->_prev;
		return tmp;
	}
};

因为链表中的节点是通过指针来建立联系的,所以正向迭代器的成员变量就可以是节点指针_pnode了。那么_pnode = _pnode->_next就相当于迭代器 ++。

为什么正向迭代器有三个模板参数?

正向迭代器有三个模板参数主要是为了避免代码冗余。因为除了实现没有const修饰的正向迭代器,我们还需要实现有const修饰的正向迭代器,所以我们只需要修改模板参数的类型就能同时实现两个迭代器了,从而避免代码的冗余。

在这里插入图片描述

const 迭代器的易错点

const T* p1T* const p2,const 迭代器类似 p1 的行为,保护指向的对象不被修改,迭代器本身可以修改。

为什么要有operator->函数重载?

struct Pos
{
	int _row;
	int _col;

	Pos(int row = 0, int col = 0)
		: _row(row)
		, _col(col)
	{}
};

void listTest5()
{
	list<Pos> lt;
	Pos p1(1, 1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(Pos(2, 2));
	lt.push_back(Pos(3, 3));

	list<Pos>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << (*it)._row << ':' << (*it)._col << endl;
		++it;
	}
	cout << endl;
}

有时候,链表的数据类型有可能是自定义类型。而我们想数据自定义类型的数据,这时候就可以通过流插入。如果不是使用流插入的话,可能会出现像上面(*it)._row(*it)._col的写法。这样的写法并不好。那么为了解决这个问题,就需要借助operator->重载函数了。这个函数返回的是节点数据的地址。

在这里插入图片描述

在这里插入图片描述
迭代器是否需要析构函数?

默认生成的析构函数对于自定义类型,会调用该自定义类型的析构函数;而对于内置类型,编译器不做处理。知道了这个,那么迭代器需不需析构函数就显而易见了。迭代器不需要析构函数,如果有析构函数的话,就会把链表的节点给释放掉。这是我们不希望看到的。那如果我们不写析构函数,默认生成的析构函数会不会做出处理呢?也不会,因为迭代器的成员变量是指针(内置类型),它不会轻易地释放这个资源。

迭代器是否需要深拷贝

之前我们说过,需要自己写析构函数的自定义类型,都需要自己写拷贝构造(深拷贝)。那么,很明显迭代器不需要深拷贝。因为迭代器不需要写析构函数,浅拷贝也能够完成任务。

typedef list_node<T> node;
typedef __list_iterator<T, T&, T*> iterator;	// 正向迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator;	// const正向迭代器

const_iterator begin() const
{
	// const_iterator it(head->_next);
	// return it
	
	// 匿名对象
	return const_iterator(_head->_next);
}

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

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

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

注:迭代器的begin就是_head->_next,而迭代器的end就是最后一个数据的下一个位置,也就是哨兵位头节点_head

在这里插入图片描述

现在正向迭代器就实现完了,那么我们通过下面的测试用例来测试一下迭代器写得对不对。

void listTest1()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	list<int>::iterator it = lt.begin();	// 拷贝够,迭代器浅拷贝就可以了
	while (it != lt.end())
	{
		cout << *it << " ";	// 调用operator*()函数
		++it;	// 调用operator++()函数
	}
	cout << endl;

	it = lt.begin();
	while (it != lt.end())
	{
		*it *= 2;
		++it;
	}

	// 傻瓜式替换成迭代器,如果名字对不上就会报错
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述
注:比较迭代器最好使用 != 或者 ==,而只有string 和1 vector 能够使用 > 和 < 来比较迭代器,因为这两个容器的空间是连续的。vector 的正向迭代器也不一定是原生指针,sgi 版(g++)的 vector 迭代器是原生指针,而 pj 版(VS)的 vector 迭代器不是原生指针。

在这里插入图片描述

迭代器的价值

  • 封装底层实现,不暴露底层的实现细节
  • 提供统一的访问方式,降低使用成本

insert 和 erase

iterator insert(iterator pos, const T& x)
{
	node* cur = pos._pnode;	// 迭代器中节点的指针
	node* prev = cur->_prev;
	node* newnode = new node(x);

	// prev   newnode   cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	return iterator(newnode);	// 返回值为新插入节点的迭代器
}
iterator erase(iterator pos)
{
	assert(pos != end());	// pos不能等于end()

	node* cur = pos._pnode;
	node* prev = cur->_prev;
	node* next = cur->_next;

	// prev   cur   next
	prev->_next = next;
	next->_prev = prev;
	delete cur;

	return iterator(next);	// 返回值为删除节点的下一个节点的迭代器
}

有了 insert 和 erase 函数,那么 push_back 和 push_front 函数就可以改成下面的样子了。

void push_back(const T& x)
{
	insert(end(), x);	// end()是哨兵位头节点
}

void push_front(const T& x)
{
	insert(begin(), x);	// begin()是第一个数据的迭代器
}

pop_back 和 pop_front

void pop_back()
{
	/*node* tail = _head->_prev;
	node* prev = tail->_prev;

	// _head   prev   tail
	_head->_prev = prev;
	prev->_next = _head;
	delete tail;*/

	erase(--end());	// --end()为尾结点
}

void pop_front()
{
	/*node* head = _head->_next;
	node* next = head->_next;

	// _head   head   next
	_head->_next = next;
	next->_prev = _head;
	delete head;*/

	erase(begin());
}

测试样例

void listTest2()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";	// 1 2 3 4 5
		++it;
	}
	cout << endl;

	it = lt.begin();
	while (it != lt.end())
	{
		*it *= 2;
		++it;
	}

	for (auto e : lt)
	{
		cout << e << " ";	// 2 4 6 8 10
	}
	cout << endl;

	lt.push_front(10);
	lt.push_front(20);
	lt.push_front(30);
	lt.push_front(40);

	lt.pop_back();
	lt.pop_back();

	for (auto e : lt)
	{
		cout << e << " ";	// 40 30 20 10 2 4 6
	}
	cout << endl;
}

clear 和 析构函数

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

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

clear 函数依次释放链表中的节点(除了哨兵位头节点),析构函数则需要将所有节点释放点。所有析构函数可以先复用 clear 函数,再释放哨兵位头节点,再将其置为nullptr

拷贝构造

传统写法

void empty_init()
{
	// 创建并初始化哨兵位头节点
	_head = new node;
	_head->_prev = _head;
	_head->_next = _head;
}

// 拷贝构造传统写法 lt2(lt1)
list(const list<T>& lt)
{
	empty_init();

	for (const auto& e : lt)	// 加引用避免自定义类型的拷贝构造
	{
		push_back(e);
	}
}

现代写法

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

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

void swap(list<T>& x)
{
	std::swap(_head, x._head);	// 交换哨兵位的头节点
}

// 拷贝构造现代写法 lt2(lt1)
list(const list<T>& lt)
{
	// 注意:_head不能为nullptr,因为链表至少要有哨兵位头节点
	empty_init();	// 先初始化哨兵位的头节点,防止报错
	list<T> tmp(lt.begin(), lt.end());	// 迭代器区间初始化
	swap(tmp);	// 交换哨兵位头节点
}

赋值运算符重载

传统写法

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

现代写法

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

用 n 个 val 来构造对象

list(int n, const T& val = T())
{
	empty_init();
	for (int i = 0; i < n; ++i)
	{
		push_back(val);
	}
}

size 和 empty

为了避免频繁调用 size 函数,降低效率,所以我们可以多加一个成员变量_size。那么所以跟_size有关的函数接口都需要修改。不过也可以采用不增加成员变量的方式,自己喜欢吧。

增加成员变量的写法

size_t size() const
{
	return _size;
}

bool empty() const
{
	return _size == 0;
}

不增加成员变量的写法

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

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

注:因为有了 size 和 empty 函数接口,所以之前的 erase、pop_back 等函数接口,都需要进行判空检查。

类名和类型

对于普通类而言,类名就等价于类型;对于类模板而言,类名不等于类型。如:list 模板,类名 list,类型list<T>。而在类模板里面可以用类名代表类型,但是建议不要那么用。但是在类外,类名不等同于类型。

在这里插入图片描述

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

完整代码

namespace Joy
{
	template <class T>
	struct list_node
	{
		list_node* _prev;
		list_node* _next;
		T _data;

		list_node(const T& val = T())
			: _prev(nullptr)
			, _next(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;	// Ref是T&,Ptr是T*,Self是迭代器
		node* _pnode;	// 正向迭代器的成员变量,_pnode是指向节点的指针

		__list_iterator(node* pnode = nullptr)
			: _pnode(pnode)
		{}

		Ref operator*()
		{
			return _pnode->_data;
		}

		bool operator!=(const Self& it) const
		{
			return _pnode != it._pnode;
		}

		bool operator==(const Self& it) const
		{
			return _pnode == it._pnode;
		}

		Ptr operator->()
		{
			return &(operator*());
		}

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

		// it++
		Self operator++(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_next;
			return *this;
		}

		// --it
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		// it--
		Self operator--(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}
	};

	template <class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T, T&, T*> iterator;	// 正向迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator;	// const正向迭代器

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

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

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

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


		list()
		{
			/*_head = new node;
			_head->_prev = _head;
			_head->_next = _head;*/

			empty_init();
		}

		void empty_init()
		{
			// 创建并初始化哨兵位头节点
			_head = new node;
			_head->_prev = _head;
			_head->_next = _head;

			_size = 0;
		}

		// 拷贝构造传统写法 lt2(lt1)
		/*list(const list<T>& lt)
		{
			empty_init();

			for (const auto& e : lt)
			{
				push_back(e);
			}
		}*/

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

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

		void swap(list<T>& x)
		{
			std::swap(_head, x._head);	// 交换哨兵位的头节点
			std::swap(_size, x._size);
		}

		// 拷贝构造现代写法 lt2(lt1)
		//list(const list& lt)
		list(const list<T>& lt)
		{
			empty_init();	// 先初始化哨兵位的头节点,防止报错
			list<T> tmp(lt.begin(), lt.end());	// 迭代器区间初始化
			swap(tmp);	// 交换哨兵位头节点
		}

		// 赋值运算符重载传统写法
		/*list<T>& operator=(const list<T>& lt)
		{
			if (this != &lt)	// 防止自己给自己赋值
			{
				clear();	// 清理数据
				for (const auto& e : lt)
				{
					push_back(e);
				}
			}

			return *this;
		}*/

		// 赋值运算符现代写法
		//list& operator=(list lt)
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		list(int n, const T& val = T())
		{
			empty_init();
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

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

		// 析构函数
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void push_back(const T& x)
		{
			/*node* tail = _head->_prev;
			node* newnode = new node(x);

			// _head   _tail   newnode
			tail->_next = newnode;
			newnode->_prev = tail;
			_head->_prev = newnode;
			newnode->_next = _head;
			++_size;*/

			insert(end(), x);
		}

		void push_front(const T& x)
		{
			/*node* head = _head->_next;
			node* newnode = new node(x);

			// _head  newnode   head
			_head->_next = newnode;
			newnode->_prev = _head;
			newnode->_next = head;
			head->_prev = newnode;
			++_size;*/

			insert(begin(), x);
		}


		iterator insert(iterator pos, const T& x)
		{
			node* cur = pos._pnode;	// 迭代器中节点的指针
			node* prev = cur->_prev;
			node* newnode = new node(x);

			// prev   newnode   cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			++_size;
			return iterator(newnode);	// 返回值为新插入节点的迭代器
		}

		iterator erase(iterator pos)
		{
			assert(!empty());
			assert(pos != end());	// pos不能等于end()

			node* cur = pos._pnode;
			node* prev = cur->_prev;
			node* next = cur->_next;

			// prev   cur   next
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			
			--_size;
			return iterator(next);	// 返回值为删除节点的下一个节点的迭代器
		}

		void pop_back()
		{
			/*assert(!empty());
			node* tail = _head->_prev;
			node* prev = tail->_prev;

			// _head   prev   tail
			_head->_prev = prev;
			prev->_next = _head;
			delete tail;*/

			erase(--end());
		}

		void pop_front()
		{
			/*assert(!empty());
			node* head = _head->_next;
			node* next = head->_next;

			// _head   head   next
			_head->_next = next;
			next->_prev = _head;
			delete head;
			--_size;*/

			erase(begin());
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}

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

	private:
		node* _head;	// 哨兵位头节点
		size_t _size;
	};
}

👉vector 和 list 的对比👈

在这里插入图片描述
注:vector 和 list 的对比是面试中非常喜欢考的知识点,比如:vector 的扩容问题、迭代器失效问题等等。

vector 的扩容问题

为什么 vector 的扩容倍数是二倍?因为二倍比较合适,扩容过多存在空间浪费问题;扩容过少会导致频繁扩容,影响效率。

vector 的 CPU 高速缓存命中率高

CPU 不会直接访问内存拿取内存中的数据,而是先将内存的数据加载到缓存中,然后 CPU 再去缓存获取想要的数据。而将内存的数据加载到缓存中并不是只加载一个数据,而是加载该数据及其后面一段的数据。为什么呢?根据局部性原理,你访问该数据,就有可能访问其周围的数据,所以就把其周围的数据也加载到缓存中,提高效率。因为 vector 的空间是连续的,所以其高速缓存命中率高。而 list 的空间是不连续的,高速缓存命中率不高,还会带来缓存污染的问题。

迭代器失效问题总结

对于 vector,insert 和 erase 函数接口的不正确使用都会带来迭代器失效问题。vector 的 insert 因为扩容问题而带来迭代器失效问题,而 erase 是因为迭代器的意义变了,即相对位置变了。如果再用该迭代器去访问数据就会导致无法意料的结果。而 list 只有 erase 函数接口会失效,因为其节点都被释放掉了。那么 string 会不会有迭代器失效问题呢?其实 string 也会有迭代器失效问题,insert 和 erase 也会导致迭代器失效,原因和 vector 的迭代器失效原因相似。但是因为不经常使用迭代器向 string 对象里插入数据或者删除数据,通常使用下标来插入或删除数据,所以我们不太关心 string 的迭代器失效问题。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

👉总结👈

本篇博客主要介绍了 list 的模拟实现,重点的内容是正向迭代器的实现、vector 和 list 的对比和迭代器失效问题总结。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

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

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

相关文章

Halcon条码和二维码质量评级

现在各行各业的人们都使用条码/二维码从生产阶段到销售点全程追踪他们 的产品。那么怎么验证生产出来的具有可读性&#xff0c;码的质量等级如何呢&#xff1f; 其实ISO行业标准已经给出了如何评估码的质量等级的标准&#xff0c;以下三种主要验证标准用于确定一维条码、二维码…

毕业设计 - 基于Java EE平台项目管理系统的设计与实现【源码+论文】

文章目录前言一、项目设计1. 模块设计2. 实现效果二、部分源码项目工程前言 今天学长向大家分享一个 java web项目: 基于Java EE平台项目管理系统的设计与实现 一、项目设计 1. 模块设计 从管理员角度看: 用户登入系统后&#xff0c;可以修改管理员的密码。同时具有以下功能…

最全的SpringMVC教程,终于让我找到了

1. 为啥要学 SpringMVC&#xff1f; 1.1 SpringMVC 简介 在学习 SpringMVC 之前我们先看看在使用 Servlet 的时候我们是如何处理用户请求的&#xff1a; 配置web.xml <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmln…

[附源码]Python计算机毕业设计国际美容会所管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Jetpack Compose中的动画

Jetpack Compose中没有沿用Android原有的View动画和属性动画&#xff0c;而是新创建了一套全新的动画系统API&#xff0c;这是理所当然的&#xff0c;因为旧的动画系统主要是基于View体系的&#xff0c;而Compose中需要针对的是Composable可组合函数进行处理&#xff0c;那么势…

他文献查到凌晨两点,我用Python十分钟搞定!

大家好&#xff0c;我是爱学习的王饱饱。 对于应届毕业生来说&#xff0c;今年一定是难熬的一年。本来找工作、写论文就已经是两座大山了&#xff0c;还要面临论文无指导的额外压力。 这让我想到了去年毕业的表弟&#xff0c;当时他为了完成论文&#xff0c;摔烂了三个鼠标。…

Jsp服装商城包安装调试

(https://img-blog.csdnimg.cn/78351365dac24f6185cb69ee3a804ba1.png)jsp mysql新季服装商城 功能&#xff1a;前台会员中心后台 前台&#xff1a; 1.服装展示 图文列表 新闻列表 详情 2.注册登录 管理登陆 3.加入购物车 会员中心&#xff1a; 1.个人账户 查看 修改个人…

一个新的React项目我们该如何配置

最近组长让我负责一个新的项目&#xff0c;项目的技术栈是React typescript redux and design&#xff0c;一个工龄1年的小白菜只能先去github找开源项目看看他们做了哪些配置&#xff0c;然后去百度这些配置改如何安装。下面就是我记录一个新的React项目配置的过程。 安装…

知识图谱有哪些应用领域?

知识图谱通常应用于自然语言处理和人工智能领域&#xff0c;常用于提高机器学习模型的准确性和效率。它还可以用于数据挖掘、信息检索、问答系统和语义搜索等领域。近年来知识图谱在电子商务、金融、公安、医疗等行业逐步开始落地&#xff0c;在这些行业的渗透、深入中&#xf…

部门还有谁在? 已经没几个人了~

正文大家好&#xff0c;我是bug菌&#xff5e;终于熬过了阳性的第三天&#xff0c;症状相对没之前那么痛苦了&#xff0c;打算要家里面的兄弟帮忙处理点事情&#xff0c;一个电话打过去&#xff0c;没想到整个部门都没几个人了&#xff0c;病毒的毒性是减弱了&#xff0c;这传染…

Linux进程概念(一)

Linux进程概念冯诺依曼体系结构操作系统操作系统是什么操作系统与硬件的关系操作系统如何管理硬件数据操作系统与软件的关系操作系统的安全操作系统的服务系统调用和库函数概念进程的基本概念什么是进程如何查看进程进程常见的调用冯诺依曼体系结构 常见的计算机&#xff08;台…

牛客题霸sql入门篇之条件查询(三)

牛客题霸sql入门篇之条件查询(三) 3 高级操作符 3.1 高级操作符练习(1) 3.1.1 题目内容 3.1.2 示例代码 SELECT device_id,gender,age,university,gpa FROM user_profile WHERE gendermale AND gpa>3.53.1.3 运行结果 3.1.4 考察知识点 AND关键字表示会筛选出符合左右两…

java DDD领域分层架构设计思想

1为什么要分层 高内聚&#xff1a;分层的设计可以简化系统设计&#xff0c;让不同的层专注做某一模块的事低耦合&#xff1a;层与层之间通过接口或API来交互&#xff0c;依赖方不用知道被依赖方的细节复用&#xff1a;分层之后可以做到很高的复用扩展性&#xff1a;分层架构可…

正则表达式总结

React15前的渲染方式 React15以前&#xff0c;React是用全量渲染的方式进行页面渲染&#xff0c;每次更新都需要整个页面重新渲染。全量当数据量大的时候&#xff0c;页面就会掉帧卡顿。 为什么需要React Fiber 为了解决上述React15带来的卡顿问题&#xff0c;我们不能让一个…

PG::PyExp

nmap -Pn -p- -T4 --min-rate1000 192.168.115.118 nmap -Pn -p 1337,3306 -sCV 192.168.115.118 1337端口是ssh服务&#xff0c;3306是数据库&#xff0c;没有web服务&#xff0c;只能尝试进行爆破。 由于不知道ssh用户名&#xff0c;所以尝试root用户爆破3306端口。 hydra …

昆仑万维或将引领国内 AIGC 技术发展

AIGC 发展历程 如果说 2021 年是元宇宙元年&#xff0c;那么 2022 年绝对可以称作 AIGC 元年。自从 Accomplice 于 2021 年 10 月推出 Disco Diffusion 以来&#xff0c;AIGC 受到了前所未有的关注&#xff0c;相关产品和技术更是以井喷之势快速更新迭代。 AIGC&#xff08;A…

C++ Reference: Standard C++ Library reference: Containers: map: map: rend

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/rend/ 公有成员函数 <map> std::map::rend C98 reverse_iterator rend(); const_reverse_iterator rend() const; C11 reverse_iterator rend() noexcept; const_reverse_iterator rend() const noe…

Unity - 搬砖日志 - MatierlaPropertyDrawer 中的参数如何匹配 - 自定义 Attribute 的参数提取

环境 Unity : 2020.3.37f1 搬一下砖&#xff0c;并记录&#xff0c;免得后续重新搬砖 完成的测试shader Shader "Unlit/TestMyEnuMatAttributeShader" {Properties{_MainTex ("Texture", 2D) "white" {}[MyEnumMatAttribute] _TestProp (&quo…

前端CSS实现苹果官网文字渐入效果

效果 分析 文字是从左到有慢慢呈现出来&#xff0c;不是整体消失和出现&#xff0c;那么肯定不能使用透明度。 我们可以想到渐变文字&#xff0c;然后通过改变背景的位置来控制文字的显示与隐藏。 渐变文字 渐变文字该如何实现呢&#xff1f;这是实现这个效果的关键步骤。 其…

计算机毕设Python+Vue校园闲置物品管理系统的实现(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…