【简易版tinySTL】 哈希表与移动语义

基本概念

哈希表(HashTable)是一个重要的底层数据结构, 无序关联容器包括unordered_set, unordered_map内部都是基于哈希表实现。

  • 哈希表是一种通过哈希函数将键映射到索引的数据结构,存储在内存空间中。
  • 哈希函数负责将任意大小的输入映射到固定大小的输出,即哈希值。这个哈希值用作在数组中存储键值对的索引。

用途

那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。

例如要查询一个名字是否在这所学校里。

要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。

我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。

将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数

冲突解决

由于哈希函数的映射不是一对一的,可能会出现两个不同的键映射到相同的索引,即冲突。可以使用链地址法解决冲突,即在哈希表的每个槽中维护一个链表,将哈希值相同的元素存储在同一个槽中的链表中。

哈希表的扩容与rehashing

为了避免哈希表中链表过长导致性能下降,会在需要时进行扩容。

扩容过程涉及到重新计算所有元素的哈希值,并将它们分布到新的更大的哈希表中。这一过程称为rehashing

思路

class HashNode{
public:Key key;Value value;......;
}using Bucket = std::list<HashNode>;
std::vector<Bucket> buckets;    // 定义由多个槽连续组成的数组

在上述代码中,我们会定义以下几个变量名,它们的意义如下图所示:

  • HashNode表示链表的一个节点
  • Bucket表示一个桶,桶里面装的是链表,实际上因为冲突,桶里面的链表往往只有一个节点
  • buckets表示系统开辟一块块Bucket大小的连续内存空间

image-20240523174055456

然后我们模拟哈希表的插入流程,如下图的①②③④⑤:

image-20240525202756758

  • 首先我们定义std::vector<Bucket> buckets,它是两块Bucket大小的连续内存空间,也就是两个桶。但桶里面是没有东西的
  • 当向哈希表中插入10时(insert(10)),首先会通过哈希函数计算出索引(hash(10)=0),那么就在第一个桶(Bucket)中放入节点
  • 当向哈希表中插入0时(insert(0)),首先会通过哈希函数计算出索引(hash(0)=1),那么就在第二个桶(Bucket)中放入节点
  • 当向哈希表中插入20时(insert(20)),首先会通过哈希函数计算出索引(hash(20)=1),出现两个不同的键映射到相同的索引,即冲突。可以使用链地址法解决冲突,即在哈希表的每个槽中维护一个链表。
  • 由于桶的数量不够了,需要扩容哈希表(rehash),扩容后的容量翻倍。通过哈希函数计算出索引(hash(20)=3),那么就在第四个桶(Bucket)中放入节点

代码实现

HashTable.h

#include <algorithm>
#include <cstddef>
#include <functional>
#include <iostream>
#include <list>
#include <utility>
#include <vector>
#include <sstream>
#include <string>namespace mystl{
template <typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable{// 链表中要维护的jie'dclass HashNode{public:Key key;Value value;// 从Key构造节点,Value使用默认构造explicit HashNode(const Key &key): key(key), value(){}// 从Key和Value构造节点HashNode(const Key &key, const Value &value):key(key), value(value){}// 比较运算符重载,比较keybool operator==(const HashNode &other) const { return key == other.key;}bool operator!=(const HashNode &other) const { return key != other.key; }bool operator<(const HashNode &other) const { return key < other.key; }bool operator>(const HashNode &other) const { return key > other.key; }bool operator==(const Key &key_) const { return key == key_; }void print() const{std::cout<<"(" << key <<","<< value<<")" << " ";}};private:// 定义表中一个桶(Bucket),桶里面装的是一个个HashNode节点组成的链表using Bucket = std::list<HashNode>;std::vector<Bucket> buckets;    // 定义由多个槽连续组成的数组std::hash<Key> hashFunction;    // 定义一个哈希函数size_t tableSize;               // 哈希表的最大容量size_t numElements;             // 哈希表中当前元素的数量float maxLoadFactor = 0.75;     // 默认的最大负载因子// 哈希函数计算key的值,取模防止溢出,作为哈希表的索引size_t hash(const Key &key) const { return hashFunction(key)%tableSize; }// 当元素数量大于最大容量时,增加桶的数量并重新分配所有键void rehash(size_t newSize){std::vector<Bucket> newBuckets(newSize);// 创建一个新的桶数组,大小为newsizefor(Bucket &bucket : buckets)           // 遍历原来的桶数组buckets,轮流取出其中的一个桶(bucket){// 链表遍历for(HashNode &hashNode : bucket)    // 遍历原来的桶bucket,它是一个链表,轮流取出其中的一个节点(hashNode){// 新的索引与原来的索引相同size_t newIndex = hashFunction(hashNode.key)%newSize;// 计算新的索引newBuckets[newIndex].push_back(hashNode);}}// 移动语义buckets = std::move(newBuckets);tableSize = newSize;}public:// 哈希表构造函数初始化, 注意:typename Hash = std::hash<Key>/* std::hash<Key>() 创建了一个临时的 std::hash<Key> 对象。因为 std::hash<Key> 有一个默认的构造函数(无参数的构造函数),所以可以直接这样调用它来创建一个临时对象。这个临时对象被用来初始化之后,就销毁*/ HashTable(size_t size = 0, const std::hash<Key> &hashFunc = Hash()):buckets(size),hashFunction(hashFunc),tableSize(size),numElements(0){}// 将键值对插入哈希表中void insert(const Key &key, const Value &value){if((numElements + 1) > maxLoadFactor * tableSize)   // 乘以一个负载因子,保证哈希表预留的空间足够多,计算出的索引不容易发生冲突,减少拷贝次数{if(tableSize == 0)tableSize = 1;rehash(tableSize * 2);}size_t index = hash(key);   // 计算索引Bucket &bucket = buckets[index];    // 找出该索引对应的桶if(std::find(bucket.begin(),bucket.end(),key) == bucket.end())  //  如果桶中没有链表,则在该桶中插入该链表;如果有,则直接跳过{bucket.push_back(HashNode(key, value));numElements++;}}void insertKey(const Key &key) { insert(key, Value{}); }    // 值为空的情况void erase(const Key &key){size_t index = hash(key);   // 计算索引auto &bucket = buckets[index];  // 找出该索引对应的桶auto it = std::find(bucket.begin(), bucket.end(), key);if(it != bucket.end()){// 找到该链表,删除它bucket.erase(it);numElements--;}}Value* find(const Key &key){size_t index = hash(key);auto &bucket = buckets[index];auto ans = std::find(bucket.begin(), bucket.end(), key);if(ans != bucket.end()){return &ans->value; // 返回结点value所在的地址}return nullptr;}size_t size() const { return numElements; }void print() const {for(size_t i = 0; i < buckets.size(); i++){for(const HashNode &element : buckets[i]){element.print();    // 调用HashNode类的成员函数print}}std::cout << std::endl;}void clear(){this->buckets.clear();this->numElements = 0;this->tableSize = 0;}
};
}

test.cpp

#include "vector.h"
#include "list.h"
#include "deque.h"
#include "HashTable.h"void HashTableTest()
{mystl::HashTable<int, int> hashTable;for(int i = 0;i<5;i++){hashTable.insert(i, i*2);hashTable.print();}hashTable.print();int* t = hashTable.find(3);std::cout << *t << std::endl;hashTable.erase(3);hashTable.print();hashTable.clear();hashTable.print();
}int main()
{HashTableTest();system("pause");return 0;
}

代码详解

代码的注释已经很详细啦,所以就不一个个讲了(其实我就是懒~😜)

主要讲解一下移动语义这个知识点

移动语义

参考这篇博客,写得非常好🙂:[c++11]我理解的右值引用、移动语义和完美转发 - 简书 (jianshu.com)

std::move()可以让一个左值进行右值引用,这样在给其它变量赋值的时候,就不用额外拷贝一次临时变量

那什么叫左、右值?什么叫左值引用、右值引用呢?

左值右值

C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的有名字的变量或者对象都是左值,而右值则没有名字。

  • 左值:int a = 10
  • 右值:如 1+2 产生的临时变量,2,'c',true,"hello"

很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

左值引用,右值引用

左值引用就是我们经常说的引用,也就是给变量取别名,要注意不能给右值取左值引用,因为当我们修改b的值时,就是修改1的值,但由于1没有内存空间,修改不了,所以不符合左值引用的要求

int a = 10; 
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用
int& b = 1; // !编译错误! 1是右值,不能够使用左值引用

c++11中的右值引用使用的符号是&&,它允许我们对右值进行引用,如:

int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {public:int a;
};
A getTemp()
{return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(临时变量)

同样我们也要注意不能给左值取右值引用

在上面代码中,getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

注意:这里a的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。因此,编译器会认为a是个左值。

万能引用

那有没有一种引用,既可以左值引用,也可以右值引用呢?

有,它就是常量引用。常量左值引用是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

const int & a = 1; //常量左值引用绑定 右值, 不会报错class A {public:int a;
};
A getTemp()
{return A();
}
const A & a = getTemp();   //不会报错 而 A& a 会报错

总结一下:

  1. 左值引用, 使用 T&, 只能绑定左值
  2. 右值引用, 使用 T&&, 只能绑定右值
  3. 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值

移动语义、拷贝

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;class MyString
{
public:static size_t CCtor; //统计调用拷贝构造函数的次数
//    static size_t CCtor; //统计调用拷贝构造函数的次数
public:// 构造函数MyString(const char* cstr=0){if (cstr) {m_data = new char[strlen(cstr)+1];strcpy(m_data, cstr);}else {m_data = new char[1];*m_data = '\0';}}// 拷贝构造函数MyString(const MyString& str) {CCtor ++;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);}// 拷贝赋值函数 =号重载MyString& operator=(const MyString& str){if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);return *this;}~MyString() {delete[] m_data;}char* get_c_str() const { return m_data; }
private:char* m_data;
};
size_t MyString::CCtor = 0;int main()
{vector<MyString> vecStr;vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000for(int i=0;i<1000;i++){vecStr.push_back(MyString("hello"));}cout << MyString::CCtor << endl;
}

在这段代码中,vecStr.push_back(MyString("hello"))工作流程如图:它会依次调用MyString(const char* cstr=0)创建一个右值、在插入的时候通过MyString(const MyString& str)`拷贝一份Mystring变量

image-20240525195943736

如果MyString("hello")构造出来的字符串(比如MyString("hello abcdefghigklmnopqrstuvwsyzasd……………………"))本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString("hello")只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作。

那能不能去掉这个copy(黄色箭头)拷贝过程,直接将这个右值插入呢?而C++11新增加的移动语义就能够做到这一点。

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;class MyString
{
public:static size_t CCtor; //统计调用拷贝构造函数的次数static size_t MCtor; //统计调用移动构造函数的次数static size_t CAsgn; //统计调用拷贝赋值函数的次数static size_t MAsgn; //统计调用移动赋值函数的次数public:// 构造函数MyString(const char* cstr=0){if (cstr) {m_data = new char[strlen(cstr)+1];strcpy(m_data, cstr);}else {m_data = new char[1];*m_data = '\0';}}// 拷贝构造函数MyString(const MyString& str) {CCtor ++;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);}// 移动构造函数MyString(MyString&& str) noexcept:m_data(str.m_data) {MCtor ++;str.m_data = nullptr; //不再指向之前的资源了}// 拷贝赋值函数 =号重载MyString& operator=(const MyString& str){CAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);return *this;}// 移动赋值函数 =号重载MyString& operator=(MyString&& str) noexcept{MAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = str.m_data;str.m_data = nullptr; //不再指向之前的资源了return *this;}~MyString() {delete[] m_data;}char* get_c_str() const { return m_data; }
private:char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{vector<MyString> vecStr;vecStr.reserve(1000); //先分配好1000个空间for(int i=0;i<1000;i++){vecStr.push_back(MyString("hello"));}cout << "CCtor = " << MyString::CCtor << endl;cout << "MCtor = " << MyString::MCtor << endl;cout << "CAsgn = " << MyString::CAsgn << endl;cout << "MAsgn = " << MyString::MAsgn << endl;
}/* 结果
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

在上述代码中,我们新增了一个移动拷贝构造:

// 移动构造函数
MyString(MyString&& str) noexcept:m_data(str.m_data) {MCtor ++;str.m_data = nullptr; //不再指向之前的资源了
}

移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数MyString&& str,是右值引用,而MyString("hello")是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。

img

通过这种方法,我们就可以让一个右值,不用进行拷贝,直接移动到该去的地方

对于一个左值,肯定是调用拷贝构造函数了。比如上面我们将main函数修改一下:

int main()
{vector<MyString> vecStr;vecStr.reserve(1000); //先分配好1000个空间for(int i=0;i<1000;i++){MyString s = MyString("hello");vecStr.push_back(s);}cout << "CCtor = " << MyString::CCtor << endl;cout << "MCtor = " << MyString::MCtor << endl;cout << "CAsgn = " << MyString::CAsgn << endl;cout << "MAsgn = " << MyString::MAsgn << endl;
}

可以看出这个左值,进入到了拷贝构造里面了,而不是移动构造中!

但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。

我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。

现在我们再将main函数修改一下:

int main()
{vector<MyString> vecStr;vecStr.reserve(1000); //先分配好1000个空间for(int i=0;i<1000;i++){MyString s = MyString("hello");vecStr.push_back(std::move(s));}cout << "CCtor = " << MyString::CCtor << endl;cout << "MCtor = " << MyString::MCtor << endl;cout << "CAsgn = " << MyString::CAsgn << endl;cout << "MAsgn = " << MyString::MAsgn << endl;
}

可以看出这个左值,进入到了移动构造里面了!

所以:std::move()可以让一个左值进行右值引用,这样在给其它变量赋值的时候,就不用额外拷贝一次临时变量。

C11中元素遍历Bucket &bucket:buckets

等同于

for(int i =0;i<buckets.size();i++)
{bucket = buckets[i];…………
}

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

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

相关文章

【C++】内存分区

目录 内存分区代码运行前后区别各分区详细解释C内存申请和释放 内存分区 不同的操作系统对程序内存的管理和划分会有所不同。 此处是C内存区域划分主要是针对通用的情况&#xff0c;并不限定在某个特定操作系统上 一般分为4个区&#xff08;有时把全局区拆分成数据区未初始化…

微服务之服务保护策略【持续更新】

文章目录 线程隔离一、滑动窗口算法二、漏桶算法三、令牌桶算法 面试题1、Sentinel 限流和Gateway限流的区别 线程隔离 两种实现方式 线程池隔离&#xff08;Hystix隔离&#xff09;&#xff0c;每个被隔离的业务都要创建一个独立的线程池&#xff0c;线程过多会带来额外的CPU…

【C语言】C语言-体育彩票的模拟生成和兑奖(源码+论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

【涵子来信科技潮流】——WWDC24回顾与暑假更新说明

期末大关&#xff0c;即将来袭。在期末之前&#xff0c;我想发一篇文章&#xff0c;介绍有关WWDC24的内容和暑假中更新的说明。本篇文章仅为个人看法和分享&#xff0c;如需了解更多详细内容&#xff0c;请通过官方渠道或者巨佬文章进行进一步了解。 OK, Lets go. 一、WWDC24 …

力扣每日一题 6/30 记忆化搜索/动态规划

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;IT竞赛 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 494.目标和【中等】 题目&#xff1a; 给你一个非负整数数组 nums 和一个…

VMware17.0 安装过程

VMware17.0 VMware 17.0 是一款功能强大的虚拟机软件&#xff0c;用于在计算机上创建和管理虚拟机。它能够同时运行多个操作系统&#xff0c;如 Windows、Linux 等&#xff0c;并且在这些虚拟机之间提供无缝的切换和共享功能。 VMware 17.0 支持最新的硬件和操作系统&#xf…

Chrome浏览器web调试(js调试、css调试、篡改前置)

目录 1. 打开开发者工具(Dev Tool) 2. 打开命令菜单 截图 3. 面板介绍 4. CSS调试 右键检查快速到达元素处 查找DOM数 利用面板Console查找DOM节点 内置函数查找上一个选择点击的元素 5. 调试JS代码(Javascript调试) 日志调试 选择查看日志等级 眼睛观测变量 …

数据资产铸就市场竞争优势:运用先进的数据分析技术,精准把握市场脉搏,构建独特的竞争优势,助力企业实现市场领先地位,赢得持续成功

目录 一、引言 二、数据资产的重要性 三、先进数据分析技术的应用 1、大数据分析技术 2、人工智能与机器学习 3、数据可视化技术 四、精准把握市场脉搏 1、深入了解客户需求 2、预测市场趋势 3、优化资源配置 五、构建独特的竞争优势 1、定制化产品和服务 2、精准营…

数据结构—判断题

1.数据的逻辑结构说明数据元素之间的顺序关系&#xff0c;它依赖于计算机的存储结构。 答案&#xff1a;错误 2.(neuDS)在顺序表中逻辑上相邻的元素&#xff0c;其对应的物理位置也是相邻的。 答案&#xff1a;正确 3.若一个栈的输入序列为{1, 2, 3, 4, 5}&#xff0c;则不…

接口自动化测试关联token的方法?

引言&#xff1a; 在接口自动化测试中&#xff0c;有时候我们需要关联token来进行身份验证或权限管理。本文将从零开始&#xff0c;介绍如何详细且规范地实现接口自动化测试中token的关联。 步骤一&#xff1a;准备工作 在开始之前&#xff0c;我们需要确保以下准备工作已完成…

如何在 Linux 中后台运行进程?

一、后台进程 在后台运行进程是 Linux 系统中的常见要求。在后台运行进程允许您在进程独立运行时继续使用终端或执行其他命令。这对于长时间运行的任务或当您想要同时执行多个命令时特别有用。 在深入研究各种方法之前&#xff0c;让我们先了解一下什么是后台进程。在 Linux 中…

Kafka~特殊技术细节设计:分区机制、重平衡机制、Leader选举机制、高水位HW机制

分区机制 Kafka 的分区机制是其实现高吞吐和可扩展性的重要特性之一。 Kafka 中的数据具有三层结构&#xff0c;即主题&#xff08;topic&#xff09;-> 分区&#xff08;partition&#xff09;-> 消息&#xff08;message&#xff09;。一个 Kafka 主题可以包含多个分…

3-linux命令行与基本命令

目录 什么是shell linux命令 命令组成 几个简单的命令 linux文件系统导航 什么是shell linux学习路径&#xff1a;学习shell→配置和环境→见任务和主要工具→编写shell脚本 shell是一个接收由键盘输入的命令&#xff0c;并将其传递给操作系统来执行的程序。几乎所有…

C++学习全教程(Day2)

一、数组 在程序中为了处理方便,常常需要把具有相同类型的数据对象按有序的形式排列起来&#xff0c;形成“一组”数据&#xff0c;这就是“数组”(array&#xff09; 数组中的数据&#xff0c;在内存中是连续存放的&#xff0c;每个元素占据相同大小的空间&#xff0c;就像排…

【Spring】DAO 和 Repository 的区别

DAO 和 Repository 的区别 1.概述2.DAO 模式2.1 User2.2 UserDao2.3 UserDaoImpl 3.Repository 模式3.1 UserRepository3.2 UserRepositoryImpl 4.具有多个 DAO 的 Repository 模式4.1 Tweet4.2 TweetDao 和 TweetDaoImpl4.3 增强 User 域4.4 UserRepositoryImpl 5.比较两种模式…

深度学习基准模型Mamba

深度学习基准模型Mamba Mamba(英文直译&#xff1a;眼镜蛇)具有选择性状态空间的线性时间序列建模&#xff0c;是一种先进的状态空间模型 (SSM)&#xff0c;专为高效处理复杂的数据密集型序列而设计。 Mamba是一种深度学习基准模型&#xff0c;专为处理长序列数据而设计&…

【鸿蒙学习笔记】位置设置

官方文档&#xff1a;位置设置 目录标题 align&#xff1a;子元素的对齐方式direction&#xff1a;官方文档没懂&#xff0c;看图理解吧 align&#xff1a;子元素的对齐方式 Stack() {Text(TopStart)}.width(90%).height(50).backgroundColor(0xFFE4C4).align(Alignment.TopS…

<Python><ffmpeg>基于python使用PyQt5构建GUI实例:音频格式转换程序(MP3/aac/wma/flac)(优化版2)

前言 本文是基于python语言使用pyqt5来构建的GUI,功能是使用ffmpeg来对音频文件进行格式转换,如mp3、aac、wma、flac等音乐格式。 UI示例: 环境配置 系统:windows 平台:visual studio code 语言:python 库:pyqt5、ffmpeg 概述 本文是建立在之前的博文的基础上的优化版…

在线教育项目(一):如何防止一个账号多个地方登陆

使用jwt做验证&#xff0c;使用账号作为redis中的key,登录的时候生成token放到redis中&#xff0c;每次申请资源的时候去看token 有没有变&#xff0c;因为token每次登录都会去覆盖&#xff0c;只要第二次登录token就不一样了