当前位置: 首页 > news >正文

从底层到应用:开散列哈希表与_map/_set 的完整实现(附逐行注释)

从 0 到 1 实现哈希关联容器:开散列哈希表与_map/_set(附完整可运行代码)

一、引言:为什么要搞懂哈希关联容器?

在 C++ 开发中,unordered_mapunordered_set是高频使用的容器 —— 前者用于键值映射(如配置表、缓存),后者用于存储唯一元素(如 ID 集合)。它们的高效性依赖开散列(链地址法)哈希表,但很多开发者只知其然,不知其所以然。

本文将从底层原理出发,手把手实现:

  1. 基础组件(哈希函数、质数工具)
  2. 开散列哈希表(节点、迭代器、核心操作)
  3. 上层封装(_map 与_set)
  4. 实战测试(验证功能完整性)

所有代码均附带详细注释,复制即可运行,帮你彻底吃透哈希关联容器的实现逻辑。

二、基础组件:哈希表的 “地基”

哈希表的高效运行依赖两个基础:均匀的哈希函数(将键映射为索引)和合理的表大小(质数减少冲突)。

2.1 哈希函数:键→索引的转换器

哈希函数的核心目标是 “均匀分布”,避免不同键映射到同一索引(哈希冲突)。我们实现通用模板(适配基本类型)和string特化版本(适配字符串)。

#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;// 1. 通用哈希函数模板(适配int、char、long等基本类型)
template<class K>
struct HashFunc {// 重载(),使结构体成为“函数对象”(可像函数一样调用)size_t operator()(const K& key) const {// 基本类型直接转换为size_t(哈希表索引的标准类型)return static_cast<size_t>(key);}
};// 2. 哈希函数特化:适配string类型(经典BKDR算法,减少字符串冲突)
template<>
struct HashFunc<string> {size_t operator()(const string& str) const {size_t hashVal = 0;for (char ch : str) {hashVal += static_cast<size_t>(ch);  // 累加字符ASCII值hashVal *= 131;                      // 乘以质数131(经验值,增强分布均匀性)// 为什么选131?131=128+2+1,二进制运算高效,且能避免“abc”与“cba”类冲突}return hashVal;}
};

2.2 质数工具:哈希表大小的 “调节器”

哈希表的大小必须为质数—— 质数的约数少,能显著降低不同键映射到同一索引的概率(若为合数,某些哈希值会集中映射到特定桶)。

我们实现 “寻找大于等于 n 的最小质数” 的工具函数,基于预定义质数表(覆盖常用大小):

// 查找大于等于n的最小质数(用于哈希表初始化和扩容)
inline unsigned long __stl_next_prime(unsigned long n) {// 预定义质数表(从53到4294967291,共28个,覆盖主流场景)static const int kPrimeCount = 28;static const unsigned long kPrimeList[kPrimeCount] = {53,        97,        193,       389,       769,1543,      3079,      6151,      12289,     24593,49157,     98317,     196613,    393241,    786433,1572869,   3145739,   6291469,   12582917,  25165843,50331653,  100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};// 二分查找第一个≥n的质数(效率O(log2(28))≈5,极快)const unsigned long* pStart = kPrimeList;const unsigned long* pEnd = kPrimeList + kPrimeCount;const unsigned long* pTarget = lower_bound(pStart, pEnd, n);// 若n超过最大质数,返回最大质数;否则返回目标质数return (pTarget == pEnd) ? *(pEnd - 1) : *pTarget;
}

三、底层核心:开散列哈希表完整实现

开散列哈希表的结构是 “桶数组 + 链表”:

  • 桶数组:存储链表头指针,每个桶对应一个索引;
  • 链表:同一桶内的冲突元素通过链表串联。

下面分三部分实现,每部分均附核心逻辑解析。

3.1 哈希节点:链表的 “最小单元”

每个节点存储 “数据” 和 “下一个节点指针”,用于串联同一桶内的冲突元素:

// 哈希表节点结构(链地址法的基本存储单元)
template<class T>
struct HashNode {T _data;               // 存储的数据(_map存pair<const K, V>,_set存const K)HashNode<T>* _next;    // 指向下一个节点的指针(串联冲突元素)// 构造函数:初始化数据和指针(避免野指针)HashNode(const T& data) : _data(data), _next(nullptr)   // 初始无后续节点,指针置空{}
};

3.2 哈希迭代器:跨桶遍历的 “关键”

哈希表的迭代器和数组 / 链表迭代器不同 —— 需要支持 “跨桶遍历”(当一个桶的链表遍历完后,自动跳转到下一个非空桶)。

因此迭代器必须持有两个核心成员:

  • _pNode:当前指向的节点;
  • _pHashT:指向所属哈希表(用于访问桶数组,寻找下一个非空桶)
// 前置声明哈希表类(迭代器需访问哈希表的私有成员,如桶数组_tables)
template<class K, class T, class KeyOfT, class Hash>
class HashTable;// 哈希表迭代器类模板(支持普通迭代器和const迭代器)
// 参数说明:
// K:键类型;T:存储数据类型;Ref:数据引用类型(T&/const T&);Ptr:数据指针类型(T*/const T*)
// KeyOfT:从T中提取K的仿函数;Hash:哈希函数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HTIterator {typedef HashNode<T> Node;                  // 节点类型别名typedef HashTable<K, T, KeyOfT, Hash> Ht;  // 哈希表类型别名typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;  // 迭代器自身类型Node* _pNode;  // 当前指向的节点Ht* _pHashT;   // 指向所属哈希表(用于跨桶查找)// 构造函数:初始化节点和哈希表指针HTIterator(Node* pNode, Ht* pHashT): _pNode(pNode), _pHashT(pHashT){}// 1. 解引用运算符:返回数据引用(支持访问数据)Ref operator*() const {return _pNode->_data;}// 2. 箭头运算符:返回数据指针(支持->访问成员,如it->first)Ptr operator->() const {return &(_pNode->_data);}// 3. 前置++:迭代到下一个有效节点(核心逻辑)Self& operator++() {// 情况1:当前链表有下一个节点,直接跳转到下一个节点if (_pNode->_next != nullptr) {_pNode = _pNode->_next;}// 情况2:当前链表遍历完,寻找下一个非空桶else {KeyOfT keyExtractor;  // 键提取仿函数(从T中取K)Hash hashFunc;        // 哈希函数(计算桶索引)// 步骤1:计算当前节点所在的桶索引size_t curBucketIdx = hashFunc(keyExtractor(_pNode->_data)) % _pHashT->_tables.size();curBucketIdx++;  // 从下一个桶开始查找// 步骤2:遍历后续桶,找到第一个非空桶while (curBucketIdx < _pHashT->_tables.size()) {if (_pHashT->_tables[curBucketIdx] != nullptr) {_pNode = _pHashT->_tables[curBucketIdx];  // 指向非空桶的头节点return *this;}curBucketIdx++;}// 步骤3:所有桶遍历完,迭代器指向nullptr(表示end())_pNode = nullptr;}return *this;}// 4. 迭代器比较:判断是否指向同一节点bool operator!=(const Self& other) const {return _pNode != other._pNode;}bool operator==(const Self& other) const {return _pNode == other._pNode;}
};

3.3 哈希表主体:核心操作封装

哈希表主体类封装 “桶数组、元素个数” 和三大核心操作(插入、查找、删除),模板参数设计支持_map_set复用。

核心设计思路:
  • T表示存储的数据类型(_map 存pair<const K, V>,_set 存const K);
  • KeyOfT仿函数从T中提取键(解耦数据类型和键的提取逻辑);
  • 扩容条件:负载因子≥1(开散列特性,负载因子 = 元素个数 / 桶数)。
// 开散列哈希表类模板(通用设计,支持_map/_set复用)
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable {// 声明迭代器为友元(允许迭代器访问私有成员,如桶数组_tables)template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct HTIterator;typedef HashNode<T> Node;  // 节点类型别名public:// 迭代器类型定义(普通迭代器和const迭代器)typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;// -------------------------- 1. 迭代器接口 --------------------------// 普通迭代器begin:指向第一个非空桶的头节点Iterator begin() {for (size_t i = 0; i < _tables.size(); ++i) {if (_tables[i] != nullptr) {return Iterator(_tables[i], this);}}return end();  // 空表返回end()}// 普通迭代器end:指向nullptr(表示遍历结束)Iterator end() {return Iterator(nullptr, this);}// const迭代器begin/end:与普通迭代器逻辑一致,仅返回const版本ConstIterator begin() const {for (size_t i = 0; i < _tables.size(); ++i) {if (_tables[i] != nullptr) {return ConstIterator(_tables[i], const_cast<HashTable*>(this));}}return end();}ConstIterator end() const {return ConstIterator(nullptr, const_cast<HashTable*>(this));}// -------------------------- 2. 构造与析构 --------------------------// 构造函数:初始化桶数组(默认大小为53)HashTable(): _tables(__stl_next_prime(1), nullptr)  // 1的下一个质数是53, _size(0)                               // 初始元素个数为0{}// 析构函数:释放所有节点内存(避免内存泄漏)~HashTable() {for (size_t i = 0; i < _tables.size(); ++i) {Node* pCur = _tables[i];  // 指向当前桶的头节点while (pCur != nullptr) {Node* pNext = pCur->_next;  // 保存下一个节点(避免释放后丢失)delete pCur;                // 释放当前节点pCur = pNext;               // 跳转到下一个节点}_tables[i] = nullptr;  // 桶指针置空(避免野指针)}}// -------------------------- 3. 核心操作:插入 --------------------------// 返回值:pair<Iterator, bool> // - Iterator:指向插入的节点(或已存在的节点)// - bool:true=插入成功(新元素),false=插入失败(元素已存在)pair<Iterator, bool> insert(const T& data) {KeyOfT keyExtractor;  // 键提取仿函数(从T中取K)Hash hashFunc;        // 哈希函数(计算桶索引)// 步骤1:检查元素是否已存在(哈希表键唯一,不允许重复)K key = keyExtractor(data);  // 从插入数据中提取键Iterator it = find(key);     // 查找键是否已存在if (it != end()) {return { it, false };  // 已存在,返回现有节点和false}// 步骤2:负载因子≥1时扩容(避免链表过长,导致查询效率下降)if (_tables.size() <= _size) {size_t newBucketCount = __stl_next_prime(_tables.size() + 1);  // 新桶数为下一个质数vector<Node*> newTables(newBucketCount, nullptr);              // 创建新桶数组// 步骤2.1:旧节点重新哈希到新桶(扩容后桶数变,索引需重新计算)for (size_t i = 0; i < _tables.size(); ++i) {Node* pCur = _tables[i];while (pCur != nullptr) {Node* pNext = pCur->_next;  // 保存下一个节点// 计算当前节点在新桶中的索引K nodeKey = keyExtractor(pCur->_data);size_t newIdx = hashFunc(nodeKey) % newBucketCount;// 头插法插入新桶(效率O(1),无需遍历链表)pCur->_next = newTables[newIdx];newTables[newIdx] = pCur;pCur = pNext;  // 处理下一个旧节点}_tables[i] = nullptr;  // 旧桶置空(避免野指针)}// 步骤2.2:交换新旧桶数组(旧桶数组会在函数结束后被销毁)_tables.swap(newTables);}// 步骤3:插入新节点(头插法)size_t targetIdx = hashFunc(key) % _tables.size();  // 计算目标桶索引Node* pNewNode = new Node(data);                    // 创建新节点pNewNode->_next = _tables[targetIdx];               // 新节点的next指向桶的头节点_tables[targetIdx] = pNewNode;                      // 桶的头节点更新为新节点_size++;                                            // 元素个数+1return { Iterator(pNewNode, this), true };  // 插入成功,返回新节点迭代器}// -------------------------- 4. 核心操作:查找 --------------------------// 根据键查找,返回迭代器(未找到返回end())Iterator find(const K& key) {Hash hashFunc;        // 哈希函数KeyOfT keyExtractor;  // 键提取仿函数// 步骤1:计算键对应的桶索引size_t targetIdx = hashFunc(key) % _tables.size();Node* pCur = _tables[targetIdx];  // 指向目标桶的头节点// 步骤2:遍历桶内链表,查找匹配的键while (pCur != nullptr) {if (keyExtractor(pCur->_data) == key) {return Iterator(pCur, this);  // 找到,返回节点迭代器}pCur = pCur->_next;  // 未找到,继续遍历下一个节点}return end();  // 遍历完链表仍未找到,返回end()}// -------------------------- 5. 核心操作:删除 --------------------------// 根据键删除,返回是否删除成功(true=成功,false=未找到)bool erase(const K& key) {Hash hashFunc;        // 哈希函数KeyOfT keyExtractor;  // 键提取仿函数// 步骤1:计算键对应的桶索引size_t targetIdx = hashFunc(key) % _tables.size();Node* pCur = _tables[targetIdx];  // 指向目标桶的头节点Node* pPrev = nullptr;            // 前驱节点(用于删除节点)// 步骤2:遍历桶内链表,查找待删除节点while (pCur != nullptr) {if (keyExtractor(pCur->_data) == key) {// 情况1:待删除节点是桶的头节点(前驱为nullptr)if (pPrev == nullptr) {_tables[targetIdx] = pCur->_next;  // 桶的头节点更新为下一个节点}// 情况2:待删除节点是中间节点(前驱非nullptr)else {pPrev->_next = pCur->_next;  // 前驱的next跳过当前节点}delete pCur;  // 释放节点内存_size--;      // 元素个数-1return true;  // 删除成功}pPrev = pCur;     // 前驱节点后移pCur = pCur->_next;  // 当前节点后移}return false;  // 遍历完链表仍未找到,删除失败}private:size_t _size = 0;               // 有效元素个数vector<Node*> _tables;          // 桶数组(存储链表头指针)
};

四、上层封装:_map 与_set 的实现

基于底层哈希表,我们只需通过 “键提取仿函数” 和 “存储类型适配”,即可快速封装_map_set—— 这就是通用设计的魅力!

4.1 _set:唯一键集合

_set的核心特性:

  • 存储唯一键(键即值);
  • 键不可修改(避免哈希表索引失效)。

实现思路:

  • 哈希表的存储类型T设为const K(键不可修改);
  • 键提取仿函数直接返回const K(键即数据本身)。
// 命名空间bobo:避免与标准库冲突
namespace bobo {
// _set类:基于开散列哈希表的唯一键集合
template<class K, class Hash = HashFunc<K>>
class _set {// 键提取仿函数:从存储的const K中提取键(键即数据本身)struct KeyOfT {const K& operator()(const K& data) const {return data;}};public:// 迭代器复用哈希表的迭代器(_set迭代器不可修改键,故直接用const迭代器逻辑)typedef typename HashTable<K, const K, KeyOfT, Hash>::Iterator iterator;typedef typename HashTable<K, const K, KeyOfT, Hash>::ConstIterator const_iterator;// 迭代器接口(直接复用哈希表的begin/end)iterator begin() { return _ht.begin(); }iterator end() { return _ht.end(); }const_iterator begin() const { return _ht.begin(); }const_iterator end() const { return _ht.end(); }// 插入:返回pair<iterator, bool>(键唯一,重复插入返回false)pair<iterator, bool> insert(const K& key) {return _ht.insert(key);  // 直接调用哈希表的insert}// 查找:根据键查找,返回迭代器(未找到返回end())iterator find(const K& key) {return _ht.find(key);  // 直接调用哈希表的find}// 删除:根据键删除,返回是否成功bool erase(const K& key) {return _ht.erase(key);  // 直接调用哈希表的erase}private:// 底层哈希表:键类型K,存储类型const K,键提取仿函数KeyOfTHashTable<K, const K, KeyOfT, Hash> _ht;
};
}  // namespace bobo

4.2 _map:键值对映射

_map的核心特性:

  • 存储键值对(pair<const K, V>);
  • 键不可修改(firstconst),值可修改(secondconst);
  • 支持[]运算符(访问 / 插入值)。

实现思路:

  • 哈希表的存储类型T设为pair<const K, V>
  • 键提取仿函数返回pairfirst(键);
  • []运算符通过insert实现(插入默认值,返回值的引用)。
namespace bobo {
// _map类:基于开散列哈希表的键值对映射
template<class K, class V, class Hash = HashFunc<K>>
class _map {// 键提取仿函数:从pair<const K, V>中提取键(返回first)struct KeyOfT {const K& operator()(const pair<const K, V>& data) const {return data.first;  // 键是pair的first成员}};public:// 迭代器复用哈希表的迭代器(解引用返回pair<const K, V>)typedef typename HashTable<K, pair<const K, V>, KeyOfT, Hash>::Iterator iterator;typedef typename HashTable<K, pair<const K, V>, KeyOfT, Hash>::ConstIterator const_iterator;// 迭代器接口(直接复用哈希表的begin/end)iterator begin() { return _ht.begin(); }iterator end() { return _ht.end(); }const_iterator begin() const { return _ht.begin(); }const_iterator end() const { return _ht.end(); }// 插入:返回pair<iterator, bool>(键唯一,重复插入返回false)pair<iterator, bool> insert(const pair<const K, V>& kv) {return _ht.insert(kv);  // 直接调用哈希表的insert}// 核心:[]运算符(访问/插入值)V& operator[](const K& key) {// 插入{key, V()}:若键不存在,插入默认值;若存在,返回已有值pair<iterator, bool> ret = _ht.insert({ key, V() });// 返回值的引用(ret.first是迭代器,解引用是pair,second是值)return ret.first->second;}// 查找:根据键查找,返回迭代器(未找到返回end())iterator find(const K& key) {return _ht.find(key);  // 直接调用哈希表的find}// 删除:根据键删除,返回是否成功bool erase(const K& key) {return _ht.erase(key);  // 直接调用哈希表的erase}private:// 底层哈希表:键类型K,存储类型pair<const K, V>,键提取仿函数KeyOfTHashTable<K, pair<const K, V>, KeyOfT, Hash> _ht;
};
}  // namespace bobo

五、实战测试:验证功能完整性

我们编写测试代码,验证_map_set的核心功能(插入、查找、删除、遍历、[]访问),确保代码可运行。

5.1 测试代码

// 测试函数:_set功能测试
void TestSet() {cout << "------------------- TestSet -------------------" << endl;bobo::_set<int> s;// 1. 插入(包含重复键)auto ret1 = s.insert(10);auto ret2 = s.insert(20);auto ret3 = s.insert(10);  // 重复插入cout << "插入10:" << (ret1.second ? "成功" : "失败") << endl;cout << "插入20:" << (ret2.second ? "成功" : "失败") << endl;cout << "重复插入10:" << (ret3.second ? "成功" : "失败") << endl;// 2. 遍历cout << "遍历_set:";for (auto it = s.begin(); it != s.end(); ++it) {cout << *it << " ";}cout << endl;// 3. 查找auto findIt1 = s.find(10);auto findIt2 = s.find(30);cout << "查找10:" << (findIt1 != s.end() ? "存在" : "不存在") << endl;cout << "查找30:" << (findIt2 != s.end() ? "存在" : "不存在") << endl;// 4. 删除bool eraseRet1 = s.erase(10);bool eraseRet2 = s.erase(30);  // 删除不存在的键cout << "删除10:" << (eraseRet1 ? "成功" : "失败") << endl;cout << "删除30:" << (eraseRet2 ? "成功" : "失败") << endl;// 5. 遍历验证删除结果cout << "删除后遍历_set:";for (auto it = s.begin(); it != s.end(); ++it) {cout << *it << " ";}cout << endl << endl;
}// 测试函数:_map功能测试
void TestMap() {cout << "------------------- TestMap -------------------" << endl;bobo::_map<string, int> m;// 1. 插入(包含重复键)auto ret1 = m.insert({ "张三", 20 });auto ret2 = m.insert({ "李四", 25 });auto ret3 = m.insert({ "张三", 30 });  // 重复插入cout << "插入{张三,20}:" << (ret1.second ? "成功" : "失败") << endl;cout << "插入{李四,25}:" << (ret2.second ? "成功" : "失败") << endl;cout << "重复插入{张三,30}:" << (ret3.second ? "成功" : "失败") << endl;// 2. []访问/插入m["王五"] = 35;  // 插入新键值对m["李四"] = 28;  // 修改已有值cout << "[]访问王五:" << m["王五"] << endl;cout << "[]修改后李四:" << m["李四"] << endl;// 3. 遍历cout << "遍历_map:";for (auto it = m.begin(); it != m.end(); ++it) {cout << "{" << it->first << ":" << it->second << "} ";}cout << endl;// 4. 查找auto findIt1 = m.find("张三");auto findIt2 = m.find("赵六");cout << "查找张三:" << (findIt1 != m.end() ? "存在" : "不存在") << endl;cout << "查找赵六:" << (findIt2 != m.end() ? "存在" : "不存在") << endl;// 5. 删除bool eraseRet1 = m.erase("张三");bool eraseRet2 = m.erase("赵六");  // 删除不存在的键cout << "删除张三:" << (eraseRet1 ? "成功" : "失败") << endl;cout << "删除赵六:" << (eraseRet2 ? "成功" : "失败") << endl;// 6. 遍历验证删除结果cout << "删除后遍历_map:";for (auto it = m.begin(); it != m.end(); ++it) {cout << "{" << it->first << ":" << it->second << "} ";}cout << endl;
}// 主函数:执行测试
int main() {TestSet();TestMap();return 0;
}

5.2 测试结果

编译运行代码后,输出如下(符合预期):

plaintext

------------------- TestSet -------------------
插入10:成功
插入20:成功
重复插入10:失败
遍历_set:10 20 
查找10:存在
查找30:不存在
删除10:成功
删除30:失败
删除后遍历_set:20 ------------------- TestMap -------------------
插入{张三,20}:成功
插入{李四,25}:成功
重复插入{张三,30}:失败
[]访问王五:35
[]修改后李四:28
遍历_map:{张三:20} {李四:28} {王五:35} 
查找张三:存在
查找赵六:不存在
删除张三:成功
删除赵六:失败
删除后遍历_map:{李四:28} {王五:35} 

六、常见问题与总结

6.1 关键问题解答

  1. 为什么_set的键和_mapfirstconst若允许修改键,会导致键的哈希值变化,原索引失效,哈希表无法找到该元素,因此必须设为const

  2. 迭代器什么时候会失效?扩容时(哈希表重建桶数组,旧节点指针迁移),原迭代器指向的节点可能已被重新哈希到新桶,此时原迭代器失效。删除时,仅被删除节点的迭代器失效,其他迭代器不受影响。

  3. 开散列为什么选负载因子 1 作为扩容阈值?开散列的冲突元素存储在链表中,负载因子 1 表示 “平均每个桶有 1 个元素”,此时链表长度较短,查询效率仍接近 O (1);若阈值过大(如 2),链表过长会导致效率下降。

6.2 总结

本文从底层到上层,完整实现了开散列哈希表与基于它的_map/_set,核心收获:

  1. 通用设计的重要性:通过KeyOfT仿函数和解耦的哈希函数,让哈希表支持不同存储类型(const Kpair<const K, V>);
  2. 迭代器的核心逻辑:哈希表迭代器需处理 “跨桶遍历”,这是区别于其他容器迭代器的关键;
  3. 性能权衡:开散列通过 “桶 + 链表” 平衡空间和时间效率,扩容和重新哈希是保证性能的核心手段。

该实现与 C++ STL 的unordered_map/unordered_set原理一致,只是 STL 还做了更多优化(如桶的负载均衡、异常安全、内存池等)。掌握本文内容,你就能轻松理解 STL 哈希容器的底层逻辑!

http://www.dtcms.com/a/419086.html

相关文章:

  • MoonBit 异步网络库发布
  • OpenLayers地图交互 -- 章节十六:双击缩放交互详解
  • Kubernetes HPA从入门到精通
  • 株洲做网站的公司网站页面设计
  • 汕头企业网站建设价格视频作为网站背景
  • 视频抽帧完全指南:使用PowerShell批量提取与优化图片序列
  • 1、User-Service 服务设计规范文档
  • 企业网站模板购买企业级网站建设
  • 路由器设置手机网站打不开wordpress跳转二级域名
  • MySQL在线DDL:零停机改表实战指南
  • 哪个做图网站可以挣钱马鞍山网站建设公司排名
  • 杭州公司做网站电商是干什么工作的
  • 揭秘InnoDB磁盘I/O与存储空间管理
  • 【深度相机术语与概念】
  • Android studio 依赖jar包里的类引用时红名,但能构建打包运行。解决红名异常
  • 做设计常用的素材网站网站seo啥意思
  • 云南最便宜的网站建设农村电商平台简介
  • AI时代下,我们需要新一代的金融基础软件
  • 挪威网站后缀网站服务器ip
  • Salesforce 生态中的缓存、消息队列和流处理
  • 【开源】基于STM32的无线条码扫描仪控制系统设计
  • 南京我爱我家网站建设新村二手房有限责任公司和有限公司的区别
  • WebStorm 快捷键大全(Windows / macOS 双平台对照)
  • 多线程顺序输出abc
  • CSS盒模型全面解析
  • 免费开源cms网站源码网页设计公司网站设计
  • [pytest] autouse 参数:自动使用fixture
  • 上海市建上海市建设安全协会网站wordpress盲注
  • 论文阅读三-第二章(3)
  • 在 Windows 系统上怎么使用rabbitmq相关命令,比如:rabbitmqctl list_queues 命令