
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:零基础入门C++
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
C++ STL篇(十五) ------ 哈希表封装 unordered_map 和 unordered_set
** 在上一篇文章中,我们深入讲解了哈希表的实现,如果你对这些内容还不太熟悉,建议先回顾一下 【C++ STL篇(十四)】哈希表实现:开放定址法与链地址法**
** 本文假定你已经掌握了哈希表实现的基础操作,接下来我们要完成一个更酷的挑战:** 用我们自己实现的哈希表,模拟出 STL 中的 unordered_map 和 unordered_set !
全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧
文章目录
- [C++ STL篇(十五) ------ 哈希表封装 unordered_map 和 unordered_set](#C++ STL篇(十五) —— 哈希表封装 unordered_map 和 unordered_set)
-
- [1. 站在巨人的肩膀上:SGI-STL 源码框架分析](#1. 站在巨人的肩膀上:SGI-STL 源码框架分析)
- [2. 我们已有的哈希桶](#2. 我们已有的哈希桶)
- [3. 打造通用型 HashTable(支持泛型 T)](#3. 打造通用型 HashTable(支持泛型 T))
-
- [3.1 节点模板改为只含 `T`](#3.1 节点模板改为只含
T) - [3.2 HashTable 模板参数增加 `KeyOfT`](#3.2 HashTable 模板参数增加
KeyOfT) - [3.3 在 `Insert`、`Find`、`Erase` 中使用 `KeyOfT`](#3.3 在
Insert、Find、Erase中使用KeyOfT) - [3.4 改造 `Insert` 返回值,为 `operator\[\]` 准备](#3.4 改造
Insert返回值,为operator[]准备)
- [3.1 节点模板改为只含 `T`](#3.1 节点模板改为只含
- [4. 实现哈希表迭代器 ------ 最难的点 `operator++`](#4. 实现哈希表迭代器 —— 最难的点
operator++) -
- [4.1 解引用和访问成员](#4.1 解引用和访问成员)
- [4.2 不等于比较](#4.2 不等于比较)
- [4.3 最核心的 `operator++`:跨桶找下一个节点](#4.3 最核心的
operator++:跨桶找下一个节点) - [4.4 友元声明与类型别名](#4.4 友元声明与类型别名)
- [4.5 `Begin()` 和 `End()` 的实现](#4.5
Begin()和End()的实现)
- [5. 完整的 HashTable 实现解析](#5. 完整的 HashTable 实现解析)
- [6. 封装 unordered_map 和 unordered_set](#6. 封装 unordered_map 和 unordered_set)
-
- [6.1 unordered_set 的封装](#6.1 unordered_set 的封装)
- [6.2 unordered_map 的封装](#6.2 unordered_map 的封装)
- [7. 容易踩的坑与设计细节回顾](#7. 容易踩的坑与设计细节回顾)
-
- [7.1 迭代器为什么需要 `_ht` 指针](#7.1 迭代器为什么需要
_ht指针) - [7.2 拷贝构造时的链表深度复制](#7.2 拷贝构造时的链表深度复制)
- [7.3 `SetKeyOfT` / `MapKeyOfT` 的作用](#7.3
SetKeyOfT/MapKeyOfT的作用) - [7.4 如何保证键不可修改](#7.4 如何保证键不可修改)
- [7.5 扩容时元素重新映射](#7.5 扩容时元素重新映射)
- [7.6 `operator\[\]` 的细节](#7.6
operator[]的细节)
- [7.1 迭代器为什么需要 `_ht` 指针](#7.1 迭代器为什么需要
- 结语:
1. 站在巨人的肩膀上:SGI-STL 源码框架分析
SGI-STL30 版本发布于 C++11 之前,那时候还没有 unordered_map/unordered_set,但它已经实现了功能基本相同的 hash_map 和 hash_set,只是它们属于"非标准容器"。源码就在 stl_hash_map.h、stl_hash_set.h 和 stl_hashtable.h 里。
我们截取出核心结构,先来感受一下:
cpp
// stl_hash_set.h
template <class Value, class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
class hash_set
{
private:
typedef hashtable<Value, Value, HashFcn, identity<Value>,
EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct() const { return rep.hash_funct(); }
key_equal key_eq() const { return rep.key_eq(); }
};
cpp
// stl_hash_map.h
template <class Key, class T, class HashFcn = hash<Key>,
class EqualKey = equal_to<Key>,
class Alloc = alloc>
class hash_map
{
private:
typedef hashtable<pair<const Key, T>, Key, HashFcn,
select1st<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
};
而底层的 hashtable 声明大概是这样的(简化):
cpp
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey,
class Alloc>
class hashtable {
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*, Alloc> buckets;
size_type num_elements;
public:
typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> iterator;
pair<iterator, bool> insert_unique(const value_type& obj);
const_iterator find(const key_type& key) const;
};
节点很简单,就是单链表:
cpp
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
核心设计思路:
hash_set存储纯 key 值 :传给hashtable的两个类型参数都是Value(也就是Key),并用identity<Value>作为"从值中取出 key"的仿函数(它的operator()就是直接返回自己)。hash_map存储 key-value 对 :传给hashtable的是pair<const Key, T>(值类型)和Key(键类型),并用select1st<pair<const Key, T>>来从 pair 中取出first(即 key)。hashtable只认一个模板参数Value(它存在节点里),但它不知道这个Value到底是什么。于是额外需要一个ExtractKey仿函数,帮它从Value中提取出Key,用来计算哈希值和比较相等。- 一个容器只需要持有一个
hashtable对象,把自己收到的操作全部转发给它。
这个设计简直神了!一份哈希表,两份复用,完美适配两种数据结构。
接下来我们就照着这个思路,把我们之前写的哈希桶版本改造成能复用的 HashTable,再用它封装出 unordered_map 和 unordered_set。
2. 我们已有的哈希桶
在上一篇博客中,我们完成了一个开放地址法(线性探测)和哈希桶(链地址法)的 HashTable。其中哈希桶版本结构大致如下(namespace hash_bucket):
cpp
template<class K, class V>
struct HashNode {
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K,V>& kv)
: _kv(kv)
, _next(nullptr)
{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
// ...
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 有效数据个数
};
它只能存储 pair<K,V>,Find 查找也是直接用 cur->_kv.first == key 比较。现在我们要让它能适应 unordered_set(只存 K)和 unordered_map(存 pair<const K, V>)两种类型。怎么做?最关键的一步就是把数据类型抽象成模板参数 T,并引入 KeyOfT 仿函数。
3. 打造通用型 HashTable(支持泛型 T)
3.1 节点模板改为只含 T
原来节点里是 pair<K, V> _kv,现在改成 T _data。因为 T 可以是 K(对 set),也可以是 pair<const K, V>(对 map)。节点也相应变成:
cpp
namespace hash_bucket
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
// ...
}
3.2 HashTable 模板参数增加 KeyOfT
原版:template<class K, class V, class Hash = HashFunc<K>>
新版:template<class K, class T, class KeyOfT, class Hash>
K 依然是键类型,T 是节点中真正存储的数据类型(可能是 K 也可能是 pair<const K, V>)。KeyOfT 是一个仿函数类型,它的任务就是从 T 类型的对象中提取出 K 类型的键。
例如:
- 对
unordered_set:T = K,KeyOfT直接返回key本身。- 对
unordered_map:T = pair<const K, V>,KeyOfT返回pair.first。
3.3 在 Insert、Find、Erase 中使用 KeyOfT
原先 Find 里这样比较:if (cur->_kv.first == key)
现在要变成:KeyOfT kot; if (kot(cur->_data) == key)
在计算哈希桶下标时,也同样先用 kot 取出键,再进行哈希和取模:
cpp
KeyOfT kot;
size_t hashi = hash(kot(data)) % _tables.size();
这样一来,HashTable 就完全不再关心 T 到底是个啥,只要 KeyOfT 能从中取出 K,它就能正常工作。
3.4 改造 Insert 返回值,为 operator[] 准备
原来插入返回 bool,但是 map::operator[] 需要返回 mapped_type 的引用,通常是通过 insert 返回的迭代器实现的。标准库的做法是:insert 返回 pair<iterator, bool>。
因此我们把哈希桶的 Insert 返回值类型改成:
cpp
pair<Iterator, bool> Insert(const T& data)
同时在函数内部去重检查时也利用迭代器。插入成功时返回 {Iterator(newnode, this), true};失败时返回 {it, false}。
代码实现:
cpp
pair<Iterator, bool> Insert(const T& data)
{
KeyOfT kot;
Iterator it = Find(kot(data));
if (it != End())
return {it, false}; // 已存在
Hash hash;
// 负载因子 1:当 _n == 表大小时扩容
if (_n == _tables.size())
{
vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTable.size();
// 头插到新表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTable);
}
size_t hashi = hash(kot(data)) % _tables.size();
Node* newnode = new Node(data);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return {Iterator(newnode, this), true};
}
4. 实现哈希表迭代器 ------ 最难的点 operator++
哈希表迭代器是单向迭代器 (只支持 ++)。它的实现思路和 list 迭代器很像:封装一个节点指针,然后重载 *、->、!=、++。
但哈希表的 operator++ 有一个特有的难点:当当前桶走完了,如何找到下一个非空桶?
答案在 SGI-STL 源码里就给出了:迭代器内部不仅要保存当前节点的指针,还要保存整个哈希表的指针 。有了哈希表指针,就能访问 _tables 数组,计算出当前所在桶的下标,然后向后找下一个非空桶。

因此我们的迭代器设计如下:
cpp
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* _node; // 当前指向的节点
const HT* _ht; // 哈希表对象指针,用于寻找下一个桶
HTIterator(Node* node, const HT* ht)
:_node(node)
,_ht(ht)
{}
// ...
};
为什么 _ht 是 const HT*? 因为我们希望普通 Iterator 和 ConstIterator 都持有 const HT*,这样可以给 ConstIterator 传入 const HashTable 时也能兼容。如果你理解了 const_iterator 的本质是一个不能修改元素的迭代器,就明白这里把哈希表指针用 const 是合适的。
4.1 解引用和访问成员
cpp
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
模板参数 Ref 和 Ptr 用来区分普通迭代器和 const 迭代器:
- 普通迭代器:
Ref = T&,Ptr = T* - const 迭代器:
Ref = const T&,Ptr = const T*
4.2 不等于比较
cpp
bool operator!=(const Self& s)
{
return _node != s._node;
}
4.3 最核心的 operator++:跨桶找下一个节点
流程:
- 如果当前节点的
_next不为空,直接走链表下一个节点。- 如果当前桶已经走完(
_next == nullptr),就计算出当前节点 key 对应的桶下标hashi,然后从hashi+1开始遍历_tables数组,找到第一个非空的桶,将其头节点赋给_node。- 如果一直到数组末尾都没有非空桶,说明遍历结束,
_node置为nullptr,即为end()。
代码实现:
cpp
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next; // 桶内还有数据,走到当前桶的下一个节点
}
else
{
// 当前桶走完了,找下一个不为空的桶
KeyOfT kot;
Hash hash;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
_node = _ht->_tables[hashi];
if (_node)
break;
else
++hashi;
}
if (hashi == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
特别注意 :计算当前节点 key 的哈希值时,我们使用了 KeyOfT kot 和 Hash hash,这正是泛型的威力。对于 unordered_map,_node->_data 是 pair,kot 会取出它的 first;对于 unordered_set,就是 key 本身。哈希函数同样支持自定义。
4.4 友元声明与类型别名
为了让 HTIterator 访问 HashTable 的私有成员 _tables,我们需要在 HashTable 中前置声明并友元化整个迭代器模板。
注意:由于 HTIterator 和 HashTable 互相引用,在 HashTable 定义之前必须先前置声明 HashTable。我们在 HTIterator 之前加了:
cpp
template<class K, class T, class KeyOfT, class Hash>
class HashTable; // 前置声明
然后在 HashTable 内部声明友元:
cpp
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HTIterator;
4.5 Begin() 和 End() 的实现
Begin() 要返回第一个非空桶的第一个节点的迭代器;如果哈希表为空,返回 End()。
cpp
Iterator Begin()
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return Iterator(cur, this); // 迭代器绑定当前哈希表
}
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this); // end 迭代器的节点指针为空
}
// const 版本类似
ConstIterator Begin() const
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return ConstIterator(cur, this);
}
}
return End();
}
ConstIterator End() const
{
return ConstIterator(nullptr, this);
}
这样上层容器直接调用 _ht.Begin() 和 _ht.End() 即可获得迭代器。
5. 完整的 HashTable 实现解析
现在我们把改造后的 HashTable 完整展示出来。
cpp
#pragma once
#include<iostream>
#include<vector>
#include <utility>
#include <string>
#include <algorithm>
using namespace std;
// ---------- 哈希函数 ----------
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string> // 特化 string 的哈希函数
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 131; // BKDR 哈希
}
return hash;
}
};
// ---------- SGI-STL 质数表 ----------
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
// ---------- 哈希桶命名空间 ----------
namespace hash_bucket
{
// 通用节点
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
// 前置声明,以便 HTIterator 使用
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
// 哈希表迭代器
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* _node;
const HT* _ht; // 哈希表指针,用于跨桶
HTIterator(Node* node, const HT* ht)
:_node(node)
,_ht(ht)
{}
Ref operator*() { return _node->_data; }
Ptr operator->() { return &_node->_data; }
bool operator!=(const Self& s) { return _node != s._node; }
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
_node = _ht->_tables[hashi];
if (_node) break;
else ++hashi;
}
if (hashi == _ht->_tables.size())
_node = nullptr;
}
return *this;
}
};
// 通用哈希表
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
// 让迭代器访问私有成员 _tables
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HTIterator;
typedef HashNode<T> Node;
public:
typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;
// ---------- begin / end ----------
Iterator Begin()
{
if (_n == 0) return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur) return Iterator(cur, this);
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
ConstIterator Begin() const
{
if (_n == 0) return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur) return ConstIterator(cur, this);
}
return End();
}
ConstIterator End() const
{
return ConstIterator(nullptr, this);
}
// 构造、拷贝、赋值、析构
HashTable()
:_tables(__stl_next_prime(0))
,_n(0)
{}
HashTable(const HashTable& ht)
:_tables(ht._tables.size(), nullptr), _n(ht._n)
{
for (size_t i = 0; i < ht._tables.size(); i++)
{
Node* cur = ht._tables[i];
Node* tail = nullptr;
while (cur)
{
Node* newnode = new Node(cur->_data);
if (tail == nullptr)
_tables[i] = newnode;
else
tail->_next = newnode;
tail = newnode;
cur = cur->_next;
}
}
}
void swap(HashTable& tmp)
{
_tables.swap(tmp._tables);
std::swap(_n, tmp._n);
}
HashTable& operator=(HashTable tmp)
{
swap(tmp);
return *this;
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
// ---------- 核心功能 ----------
pair<Iterator, bool> Insert(const T& data)
{
KeyOfT kot;
Iterator it = Find(kot(data));
if (it != End())
return {it, false}; // 已存在
Hash hash;
// 负载因子 1:当 _n == 表大小时扩容
if (_n == _tables.size())
{
vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTable.size();
// 头插到新表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTable);
}
size_t hashi = hash(kot(data)) % _tables.size();
Node* newnode = new Node(data);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return {Iterator(newnode, this), true};
}
Iterator Find(const K& key)
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
return Iterator(cur, this);
cur = cur->_next;
}
return End();
}
bool Erase(const K& key)
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)
_tables[hashi] = cur->_next;
else
prev->_next = cur->_next;
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 有效数据个数
};
}
补充说明:
- 扩容策略与之前一致:使用 SGI-STL 质数表
__stl_next_prime获取下一个桶大小,并将旧表节点重新映射到新表(头插法)。 - 拷贝构造需要深拷贝所有节点和链表,保持原结构。
swap和operator=使用现代 C++ 的 copy-and-swap 惯用法,既安全又简洁。Begin()通过遍历找到第一个非空桶的头节点;End()返回持有nullptr的迭代器。
6. 封装 unordered_map 和 unordered_set
现在 HashTable 已经通用了,我们只需在 unordered_map 和 unordered_set 中:
- 定义自己的
KeyOfT仿函数。- 用具体的模板参数实例化
hash_bucket::HashTable。- 把容器接口转发给
_ht成员。
6.1 unordered_set 的封装
cpp
#include "HashTable.h"
namespace hj
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
// 仿函数:从 K 中取出 K(就返回自身)
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
// 注意:HashTable 第二个模板参数是 const K,因为 set 的 key 不可修改
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
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> insert(const K& key)
{
return _ht.Insert(key);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};
}
要点解析:
SetKeyOfT:operator()接收K并返回K,非常直接。- 传给 HashTable 的
T是const K。这保证了iterator指向的元素是不可修改的,即使你使用普通迭代器,也不能通过*it修改值(因为类型是const K)。 insert返回pair<iterator, bool>,方便了后续的unordered_map::operator[]。- 对外接口只暴露 key,完全隐藏了底层的哈希表细节。
6.2 unordered_map 的封装
cpp
#include "HashTable.h"
namespace hj
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
// 仿函数:从 pair<const K, V> 中取出键 K
struct MapKeyOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
iterator begin() { return _ht.Begin(); }
iterator end() { return _ht.End(); }
const_iterator begin() const { return _ht.Begin(); }
const_iterator end() const { return _ht.End(); }
// operator[] 的实现依赖 insert
V& operator[](const K& key)
{
// 插入一个 key,如果不存在则映射值为默认构造的 V()
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
}
要点解析:
MapKeyOfT从pair中取出first,注意 pair 的 Key 是const K。- 传给 HashTable 的
T是pair<const K, V>,key 类型不可变,但 value 可变。 operator[]的行为与标准库一致:- 若
key已存在,Insert返回已存在节点的迭代器,并返回bool = false,然后ret.first->second就是那个 value 的引用。 - 若
key不存在,Insert插入一个pair(key, V())并返回新节点的迭代器,ret.first->second是默认构造的V()。
- 若
- 这样无论是读还是写,
operator[]都能正确工作。
7. 容易踩的坑与设计细节回顾
7.1 迭代器为什么需要 _ht 指针
在实现 ++ 时,我们已经强调过:哈希表的桶不是连续内存,节点之间只有同一个桶内的 _next 指向关系,跨桶是无法用单纯的节点指针解决的。因此迭代器必须持有一个指向哈希表的指针,以便在走到桶末尾时访问 _tables 数组找到下一个非空桶。
7.2 拷贝构造时的链表深度复制
哈希表在拷贝构造时,不能简单地将 _tables 指针数组复制过来,因为那会导致两个对象指向同一批节点,析构时会发生重复释放。正确做法是逐桶遍历原表,为每个节点创建新的副本,并按相同顺序链接。
7.3 SetKeyOfT / MapKeyOfT 的作用
这是整个封装的核心。底层哈希表完全不知道它存的是单独的 K 还是 pair<const K, V>,它只通过 KeyOfT 这个仿函数从 T 类型的对象中取出 K,用于计算哈希值和比较是否相等。上层容器通过提供不同的仿函数,实现了数据结构的复用。
7.4 如何保证键不可修改
unordered_set:将T指定为const K,节点中存储的就是不可修改的值。unordered_map:将T指定为pair<const K, V>,pair的first自动成为const,而second可修改。
两者都从类型系统层面阻止了用户通过迭代器修改键。
7.5 扩容时元素重新映射
我们在 Insert 中,当 _n == _tables.size() 时进行扩容。扩容后每个节点的桶位置可能发生变化,因此需要遍历原表所有节点,重新计算哈希值并挂到新表的对应桶上。这里我们使用了头插法,避免遍历链表找尾节点,提高效率。
7.6 operator[] 的细节
operator[] 用了 make_pair(key, V()),V() 会调用值类型的默认构造函数生成一个默认值。如果键已存在,插入失败,返回已存在节点;如果不存在,则新插入一个包含默认值的节点,最后统一返回值的引用。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _