用哈希表封装unordered_map(以及set)【C++】

目录

一,前言

二,封装层框架(哈希底层以哈希桶为例)

三,迭代器

1. operator++

2. operator[]

3. 仿函数优化

3. 解决unordered_set中Key可以修改的Bug

代码区

Hash_map_set.h

HashTable.h

下节预告:哈希应用!!

结语


一,前言

在学习封装unordered_map与unordered_set前,建议先学习如何封装map & set该篇文章用红黑树封装map&set【C++】-CSDN博客

这样更能理解其中的封装思想(两种封装方式,主题思路大体相似)

同时用哈希表封装 unordered_map和undordered_set,在封装之前,我们首先是要学会哈希表基本的精华,哈希实现建议大家先学习从底层认识哈希表【C++】-CSDN博客

二,封装层框架(哈希底层以哈希桶为例)

由于我们前面已经学习过map与set的封装,在学习unordered_map(set) 封装这里我们直接给出基本的框架代码啦! 

本质上:还是对哈希表进行封装,在unordered_map层面调用哈希底层。

用哈希表封装 unordered_map &  unordered_set,思想核心:就是让unordered_set如何适应unordered_map,看到这里的都是已经学习过红黑树封装map与set的童鞋了吧

namespace hash_map_set
{
	template <class K>
	class unordered_set
	{
	public:
		struct SetkeyofT   // 从T类型中提取Key
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		bool insert(const K& data)
		{
			return table.insert(data);
		}

	private:
		hash_barrel::HashTable<K, K, SetkeyofT>  table;
	};

	template <class K, class V> 
	class unordered_map
	{
	public:

		struct MapkeyofT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};

		bool insert(const pair<const K, V>& kv)
		{
			return table.insert(kv);
		}
	private:
		hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT>  table;
	};
}
// 哈希表底层部分变化,为了避免冗余,就只展现部分代码

template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

template <class K, class T, class KeyofT ,class Hashstr = Hashstr<K>>
	class HashTable
	{
	public:
		typedef Node_Data<T> Node_Data;

.......

三,迭代器

首先我们先完成 迭代器基本框架 和 基本“ * ”, “ & ”,“ ->”, " != "实现,代码其实比较简单,重要的是逻辑如何行云流水的形成,多写多看多思考吧!!

    template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

	template <class type>
	struct Hashstr
	{
		int operator()(const type& sd)
		{
			return sd;
		}
	};

	template<>
	struct Hashstr<string>
	{
		size_t operator()(const string& str)
		{
			size_t sum = 0;
			for (auto e : str)
			{
				sum += e;
				sum *= 31;   // 别问,问就是实验的结果
			}
			return sum;
		}
	};

	// 由于迭代器需要使用HashTable作为成员变量,因此需要前置声明
	template <class K, class T, class KeyofT, class Hashstr = Hashstr<K>>
	class HashTable;

	template <class K, class T, class KeyofT, class Ref, class Ptr, class Hash>
	struct HT_iterator
	{
		typedef Node_Data<T> Node_Data;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;
		typedef HT_iterator<K, T, KeyofT, Ref, Ptr, Hash> HT_iter; // 正常迭代器

		Node_Data* _node;  // 当前位置
		HashTable* _ht;    // 当前哈希表
		size_t _bucket = -1;     // 当前桶

		HT_iterator(Node_Data* node, HashTable* ht)
			:_node(node)
			, _ht(ht)
		{
			Hash hashstr;
			KeyofT keyofT;
			if (node) // node为空的时候,我们会对nullptr进行访问,所以规避一下
			_bucket = hashstr(keyofT(_node->_data)) % _ht->Get_tables().size();
		}

		// HT_iterator(const HT_iterator)

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

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

		bool operator!=(const HT_iter& it)
		{
			return _node != it._node;
		}
	};

1. operator++

思路:从第一个桶开始访问数据,访问完一个数据后,接着下一个桶,直至结束

思路比较简单,但迭代器设计,逻辑比较难处理。


        // 哈希迭代器只往前单向
		HT_iter operator++()
		{
			// 三种情况:
			// 1. 下一个为空,下一个桶存在
			// 2. 下一个为空,下一个不存在
			// 3. 下一个存在
			if (_node->_downstars)
			{ // 3.
				_node = _node->_downstars;
				return *this;
			}
			else
			{   // 1 , 2
				 寻找下一个不为空的桶
				++_bucket;
				while (_bucket < _ht->Get_tables().size())
				{
					_node = (_ht->Get_tables())[_bucket];
					if (_node)  // 找到后中断寻找
						break;
					++_bucket;
				}
				return *this;
			}
		}

2. operator[]

在实现operator[]时,我们需要先将insert,find函数进行优化:

让insert返回值从bool ——> 返回pair<iterator,  bool>;

find()从返回数据指针——>  返回迭代器;

// Hashtable——哈希类中
        T& operator[](const T& pa)
		{
			pair<HT_iterator, bool> it = insert(pa);
			return *(it.first);
		}

// 封装层 unordered_map
        V& operator[](const K& pa)
		{
			pair<HT_iterator, bool> ret = insert(make_pair(pa, V()));
			return ret.first->second;
		}
        
// unordered_set
        K& operator[](const K& pa)
		{
			return table[pa];
		}

3. 仿函数优化

在哈希实现过程中,我们实现了该仿函数,目的就是为了解决当 K不是int的时候(比如string类)无法转化为size_t类型,来寻找哈希地址。 

那么问题来了!!  我如果想将Day类型(处理年,月,日的类)作为K呢?? 请问这怎么将这个日期类转化为size_t???

为了方便分析,我将这个将其他数据转化为size_t的类叫做转化类 

我们会发现,封装在底层的中转化类(int与string类)在目前看来,应该将其放在unordered_map与unordered_set的封装层;同时在两个封装层添加一个新的参数,作为转化类的行参

这样做的好处是,在我们不知道K类型时,用户可以导入其自己设定的转化类,来实现各种类(各种类型)转化为size_t类型。

3. 解决unordered_set中Key可以修改的Bug

在前面的文章中(用红黑树封装map&set【C++】-CSDN博客),我们已经修复过一次了,这里就只展现修改位置了。

代码区

Hash_map_set.h

namespace hash_map_set
{

	template <class type>
	struct Hashstr
	{
		int operator()(const type& sd)
		{
			return sd;
		}
	};

	template<>
	struct Hashstr<string>
	{
		size_t operator()(const string& str)
		{
			size_t sum = 0;
			for (auto e : str)
			{
				sum += e;
				sum *= 31;   // sum为啥都要乘31这个数??1.减少数据聚集在一个桶中。2.31这个数是大量实验的最佳数
			}
			return sum;
		}
	};

	template <class K, class Hash = Hashstr<K>>
	class unordered_set
	{
	public:
		struct SetkeyofT   // 从T类型中提取Key
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		// 关于为什么要这样操作?原因:底层(HT_iterator)实例化后,再取得模板,而不是自己设定
		typedef typename hash_barrel::HashTable<K, K, SetkeyofT, Hash>::const_HT_iterator  HT_iterator;
		typedef typename hash_barrel::HashTable<K, K, SetkeyofT, Hash>::const_HT_iterator const_HT_iterator;

		const_HT_iterator begin() const
		{
			return table.begin();
		}

		const_HT_iterator end() const 
		{
			return table.end();
		}

		HT_iterator begin()
		{
			return table.begin();
		}

		HT_iterator end()
		{
			return table.end();
		}

		pair<HT_iterator, bool> insert(const K& data)
		{
			return table.insert(data);
		}

		K& operator[](const K& pa)
		{
			return table[pa];
		}

		HT_iterator find(const K& pa)
		{
			return table.find(pa);
		}

		bool erase(const K& pa)
		{
			return table.erase(pa);
		}

	private:
		hash_barrel::HashTable<K, K, SetkeyofT, Hash>  table;
	};

	template <class K, class V, class Hash = Hashstr<K>> 
	class unordered_map
	{
	public:

		struct MapkeyofT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};

		typedef typename hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>::HT_iterator  HT_iterator;
		typedef typename hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>::const_HT_iterator  const_HT_iterator;

		const_HT_iterator begin() const 
		{
			return table.begin();
		}

		const_HT_iterator end() const 
		{
			return table.end();
		}

		HT_iterator begin()
		{
			return table.begin();
		}

		HT_iterator end()
		{
			return table.end();
		}

		pair<HT_iterator, bool> insert(const pair<const K, V>& kv)
		{
			return table.insert(kv);
		}

		V& operator[](const K& pa)
		{
			pair<HT_iterator, bool> ret = insert(make_pair(pa, V()));
			return ret.first->second;
		}

		HT_iterator find(const K& pa)
		{
			return table.find(pa);
		}

		bool erase(const K& pa)
		{
			return table.erase(pa);
		}

	private:
		hash_barrel::HashTable<K, pair<const K, V>, MapkeyofT, Hash>  table;
	};
}

HashTable.h

namespace hash_barrel
{
	template <class T>
	struct Node_Data
	{
		typedef Node_Data<T> Node_data;
		T _data;
		Node_data* _downstars = nullptr;

		Node_Data(const T& pa = T())
			:_data(pa)
		{}
	};

	// 由于迭代器需要使用HashTable作为成员变量,因此需要前置声明
	template <class K, class T, class KeyofT, class Hashstr>
	class HashTable;

	template <class K, class T, class KeyofT, class Ref, class Ptr, class Hash>
	struct HT_iterator
	{
		typedef Node_Data<T> Node_Data;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;
		typedef HT_iterator<K, T, KeyofT, Ref, Ptr, Hash> HT_iter; // 正常迭代器
		typedef HT_iterator<K, T, KeyofT, T&, T*, Hash> iterator; 

		Node_Data* _node;  // 当前位置
		const HashTable* _ht;    // 当前哈希表,添加const原因:1.迭代器不会修改哈希表 2.const迭代器传入的是const的哈希表,所以需要适配
		size_t _bucket = -1;     // 当前桶

		HT_iterator(Node_Data* node, const HashTable* ht)
			:_node(node)
			, _ht(ht)
		{
			Hash hashstr;
			KeyofT keyofT;
			if (node) // node为空的时候,我们会对nullptr进行访问,所以规避一下
				_bucket = hashstr(keyofT(_node->_data)) % _ht->Get_tables().size();
		}

		// 1. 当对象是普通迭代器时,这是拷贝构造
		// 2. 当对象是const迭代器时,是利用普通迭代器构造const迭代器的构造函数
		HT_iterator(const iterator& it)
			:_node(it._node)
			,_ht(it._ht)
			,_bucket(it._bucket)
		{}

		// HT_iterator(const HT_iterator)

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

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

		bool operator!=(const HT_iter& it)
		{
			return _node != it._node;
		}

		// 哈希迭代器只往前单向
		HT_iter operator++()
		{
			// 三种情况:
			// 1. 下一个为空,下一个桶存在
			// 2. 下一个为空,下一个不存在
			// 3. 下一个存在
			if (_node->_downstars)
			{ // 3.
				_node = _node->_downstars;
				return *this;
			}
			else
			{   // 1 , 2
				 寻找下一个不为空的桶
				++_bucket;
				while (_bucket < _ht->Get_tables().size())
				{
					_node = (_ht->Get_tables())[_bucket];
					if (_node)
						break;
					++_bucket;
				}
				return *this;
			}
		}
	};



	template <class K, class T, class KeyofT ,class Hash>
	class HashTable
	{
	public:
		typedef Node_Data<T> Node_Data;
		typedef HT_iterator<K, T, KeyofT, const T&, const T*, Hash>  const_HT_iterator;  // const迭代器
		typedef HT_iterator<K, T, KeyofT, T&, T*, Hash> HT_iterator;    // 普通迭代器
		
		~HashTable()
		{
			for (auto bucket : _tables)
			{
				Node_Data* cur = bucket;
				while (cur)
				{
					Node_Data* next = cur->_downstars;
					delete (cur);
					cur = next;
				}
			}
		}

		const_HT_iterator begin() const 
		{
			size_t i = 0;
			Node_Data* cur = nullptr;
			for (; i < _tables.size(); i++)
			{
				cur = _tables[i];

				if (cur)
					break;
			}

			return const_HT_iterator(cur, this);  // this就是哈希表对象
		}

		const_HT_iterator end() const 
		{
			return const_HT_iterator(nullptr, this);
		}

		HT_iterator begin()
		{
			size_t i = 0;
			Node_Data* cur = nullptr;
			for (; i < _tables.size(); i++)
			{
				cur = _tables[i];

				if (cur)
					break;
			}
			return HT_iterator(cur, this);  // this就是哈希表对象
		}

		HT_iterator end()
		{
			return HT_iterator(nullptr, this);
		}

		// 在实现 operator[]时,底层用到insert(),没找到,插入新值返回迭代器;找到直接返回迭代器
		pair<HT_iterator,bool> insert(const T& pa)
		{
			KeyofT keyofT;
			HT_iterator ret = _find(keyofT(pa));
			if (ret != end())
				return make_pair(ret, true);  

			// 考虑扩容:负载因子为1,2,3都可以
			Hash hashstr;
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 10)
			{
				size_t  new_size = _tables.size() == 0 ? 10 : _tables.size() * 2;
			     // 开始扩容	
				vector<Node_Data*> new_tables;
				new_tables.resize(new_size);
				for (auto& data : _tables)
				{
					// 处理桶内的数据,重新插入新节点
					Node_Data* cur = data;
					while (cur)
					{
						Node_Data* room = cur->_downstars;
						size_t new_hashi = hashstr(keyofT(cur->_data)) % new_tables.size();
						// 处理结点关系
						Node_Data* tmp = new_tables[new_hashi];
						new_tables[new_hashi] = cur;
						cur->_downstars = tmp;
						cur = room;
					}
				}
				_tables.swap(new_tables);
			}

			size_t hashi = hashstr(keyofT(pa)) % _tables.size();
			Node_Data* new_node = new Node_Data(pa);
			new_node->_downstars = _tables[hashi];
			_tables[hashi] = new_node;
			_n++;
			return make_pair(HT_iterator(new_node, this), true);
		}

		HT_iterator find(const K& order)
		{
			return _find(order);
		}

		bool erase(const K& order)
		{
			Hash hashstr;
			HT_iterator cur = find(order);
			if (!(cur._node))
			{
				cout << "擦除失败: 不存在" << endl;
				return false;
			}
			size_t index = hashstr(order) % _tables.size();
			Node_Data* tmp = _tables[index];

			while (tmp)
			{
				if (cur._node == tmp)
				{
					break;
				}
				else if (cur._node == tmp->_downstars)
				{
					break;
				}
				tmp = tmp->_downstars;
			}
			// 开始处理节点
			if (tmp == _tables[index])  // 如果擦除的是头,那要置空的包括指针数组
			{
				_tables[index] = cur._node->_downstars;
			}
			else
			{
				tmp->_downstars = cur._node->_downstars;
			}
			
			delete (cur._node);
			cout << "擦除成功" << endl;
			return true;
		}

		T& operator[](const T& pa)
		{
			pair<HT_iterator, bool> it = insert(pa);
			return *(it.first);
		}

		size_t Get_n()
		{return _n; }

		 vector<Node_Data*>& Get_tables()
		{
			return _tables;
		}

		 // 需要为const 对象进行重载
		 const vector<Node_Data*>& Get_tables() const 
		 {
			 return _tables;
		 }

	private:
		// Node_Data* _find(const K& order)
		HT_iterator _find(const K& order)
		{
			if (!_tables.size())
				return end();

			Hash hashstr;
			KeyofT keyofT;
			size_t hashi = hashstr(order) % _tables.size();
			auto cur = _tables[hashi];
			while (cur)
			{
				if (keyofT(cur->_data) == order)
					return HT_iterator(cur, this);
				cur = cur->_downstars;
			}
			return end();
		}

		vector<Node_Data*> _tables;  // 存放各个哈希地址的第一个结点地址的指针数组
		size_t _n = 0;                  // 哈希桶中,数据个数
	};
}

下节预告:哈希应用!!

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

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

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

相关文章

Ganache结合内网穿透实现远程不同局域网公网访问

文章目录 前言1. 安装Ganache2. 安装cpolar3. 创建公网地址4. 公网访问连接5. 固定公网地址 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站…

3GPP协议解读(一)_23.501_23.502_PDU Session_SMF与UDP的交互

UE发起计算服务申请后&#xff0c;网络侧处理的流程 UE发起服务的流程&#xff1a;service request网络侧处理服务涉及的通信数据通过PDU Session进行传输&#xff0c;涉及到SMF与UPF的交互。PDU Session的建立、管理全部由SMF&#xff08;Session Management Function&#x…

在混料配料输送系统中使用485modbus转profinet网关案例

485Modbus转Profinet网关是一种在工业自动化控制系统中常用的设备&#xff0c;能够实现不同通信协议之间的转换&#xff0c;对于混料配料输送系统的优化和控制具有重要作用。 通过使用485Modbus转Profinet网关&#xff0c;混料配料输送系统能够实现与不同设备之间的通信和数据交…

Android 框架

MVC: MVP MVVM Model 数据以及业务数据 View 视图 Control 控制器 simple code MVP OnFinishInflate ViewGroup 加载完成 MVC 优化 Struts MVC- MVP MVC-单次调用逻辑把 MVP / 把C拆分出来 MVVM 2017Google推出ViewModel DataBind MVVM是一种框架规则,双向绑定 Model…

电脑技巧:U盘装系统跟光盘装系统有什么区别,看完你就懂了!

目录 一、制作方法 二、优点比较 2.1 U盘 2.2 光盘 三、缺点比较 一、制作方法 U盘&#xff1a;是通过制作U盘系统盘&#xff0c;插在电脑上启动U盘&#xff0c;然后从U盘上启动PE系统&#xff0c;在PE系统里加载预先下载好的镜像&#xff0c;然后开始安装系统。 光盘&am…

使用git上传代码至gitee入门(1)

文章目录 一、gitee注册新建仓库 二、git的下载三、git的简单使用&#xff08;push、pull&#xff09;1、将本地文件推送至gitee初始化配置用户名及邮箱将本地文件提交至gitee补充 2、将远程仓库文件拉取至本地直接拉拉至其他本地文件夹 一、gitee 注册 官网&#xff1a;http…

Java中的Maven项目使依赖和自己写的代码的分开的部署的部署方式

文章目录 优点maven中配置执行maven 打包项目部署查看服务启动状态 优点 随着项目的功能越来越多&#xff0c;如果把所有代码都打包到一个jar里&#xff0c;这样不利于传输。把源码和依赖包分开。这样如果依赖包没有变化的话&#xff0c;再此部署时&#xff0c;就不需要往服务…

人脸106和240点位检测解决方案

人脸识别技术已经深入到我们生活的各个领域&#xff0c;从手机解锁、门禁系统到视频娱乐化等&#xff0c;都离不开高精度的人脸关键点检测。美摄科技作为行业的领军企业&#xff0c;一直致力于提供最先进、最稳定的人脸识别技术&#xff0c;近日&#xff0c;我们推出了全新的10…

懒人福利:6款Sketch插件合集,提升设计效率爆款推荐!

Sketch作为一种在线设计工具&#xff0c;一直是许多设计师的最爱。它不仅能快速建立原型&#xff0c;还能提供丰富的插件&#xff0c;以满足不同的需求。 今天&#xff0c;我想和大家分享六款流行的Sketch插件供参考。这些插件都是精心挑选的&#xff0c;它们支持Windows、Mac…

【LeetCode刷题-滑动窗口】--1695.删除子数组的最大得分

1695.删除子数组的最大得分 注意&#xff1a;子数组为不同元素 方法&#xff1a;滑动窗口 使用变长滑动窗口寻找数组nums中的以每个下标作为结束下标的元素各不相同的最长子数组。用[start,end]表示滑动窗口&#xff0c;初始时startend0&#xff0c;将滑动窗口的右端点end向右…

开发者的第一台服务器 ECS云服务器低至99元:新老同享

“阿里云始终围绕‘稳定、安全、性能、成本、弹性’的目标不断创新&#xff0c;为客户创造业务价值。”10月31日&#xff0c;杭州云栖大会上&#xff0c;阿里云弹性计算计算产品线负责人张献涛表示&#xff0c;通过持续的产品和技术创新&#xff0c;阿里云发布了HPC优化实例等多…

行情分析——加密货币市场大盘走势(11.16)

大饼昨日突然回调诱多上涨到38000附近&#xff0c;现在又重新跌回到37500&#xff0c;现在仓位小的可以加仓入场&#xff0c;而已经有仓位的不要动即可。 空单策略&#xff1a;入场37500附近 止盈34000-32000 止损39000 以太今日可以入场空单2060附近即可 策略&#xff1a;入…

AWD比赛中的一些防护思路技巧

## 思路1&#xff1a; 1、改服务器密码 &#xff08;1&#xff09;linux&#xff1a;passwd &#xff08;2&#xff09;如果是root删除可登录用户&#xff1a;cat /etc/passwd | grep bash userdel -r 用户名 &#xff08;3&#xff09;mysql&#xff1a;update mysql.user set…

易点易动设备管理系统:提升企业设备备品备件的管理效率

在现代企业中&#xff0c;设备备品备件的管理是一个重要而繁琐的任务。良好的备件管理可以提高设备的可用性&#xff0c;减少停机时间&#xff0c;提高生产效率。然而&#xff0c;传统的备件管理方式存在许多问题&#xff0c;如信息不透明、库存过剩或不足、难以追踪等。为了解…

μC/OS-II---整理学习1

目录 系统功能系统结构图 μC/OS-II是用 C 语言&#xff08;绝大部分&#xff09;和汇编语言&#xff08;与处理器密切相关的代码&#xff09;编写的。 系统功能 实时内核&#xff1a;μC/OS-II—内核&#xff1a;任务调度&#xff08;oc_core.c&#xff09;任务管理&#xf…

92.Linux的僵死进程以及处理方法

目录 1.什么是僵死进程&#xff1f; 2.代码演示僵死进程 3.解决办法 1.什么是僵死进程&#xff1f; 僵死进程是指一个子进程在父进程之前结束&#xff0c;但父进程没有正确地等待&#xff08;使用 wait 或 waitpid 等系统调用&#xff09;来获取子进程的退出状态。当一个进…

群晖7.2版本套件安装CloudDriver2

CloudDrive是一个强大的多云盘管理工具&#xff0c;为用户提供包含云盘本地挂载的一站式的多云盘解决方案。挂载到本地后&#xff0c;可以像本地文件一样进行操作。 一、套件库添加矿神源 二、安装CloudDriver2 1、搜索安装 搜索框输入【clouddrive】&#xff0c;搜索到Clou…

μC/OS-II---进程间通信方式

目录 信号量&#xff08; Semaphores &#xff09;- 用于最基本的互斥、同步操作互斥信号量&#xff08;Mutual Exclusion Semaphores &#xff09;-专门用于互斥消息队列&#xff08; Message Queues &#xff09;- 用于消息通信消息邮箱&#xff08;Message Box&#xff09; …

还不知道怎么发成绩?

选择一个适合的发布平台先选择一个适合的软件平台&#xff0c;这里推荐使用小程序。微信已经成为学生们日常生活中必不可少的社交工具&#xff0c;通过微信小程序来进行成绩查询&#xff0c;可以让学生们更方便的获取信息。你可以在微信中搜索并选择合适的小程序&#xff0c;比…

安装node.js指定任意版本详解

Node.js是一种基于Chrome V8引擎的JavaScript运行时环境。它允许开发人员使用JavaScript编写服务器端和网络应用程序。与传统的JavaScript在浏览器中执行不同&#xff0c;Node.js使得JavaScript可以在服务器端运行。 Node.js具有以下特点&#xff1a; 1. 非阻塞式I/O&#xf…