侯捷C++系列笔记:标准库体系结构(二)
本文系统解析了STL核心容器的底层实现原理。list采用双向循环链表结构,通过哨兵节点简化边界处理;vector基于连续内存布局,随机访问高效但扩容成本高;deque实现分段连续内存,兼顾双端操作和随机访问;RB树作为set/map的底层,通过模板参数实现高度解耦;哈希表采用拉链法解决冲突,是unordered容器的实现基础。文章详细剖析了各容器的迭代器设计、内存管理策略和关键操作实现,揭示了ST
目录
5. Traits 的扩展(type traits/char traits)
2. queue 和 stack:容器适配器(Adapter)
3. 配套仿函式(unary_function/binary_function)
2. select1st 仿函式(提取 pair 的 Key)
深入探索 list
核心解析
1. list 的底层节点结构(老版本源码)
list 是双向循环链表(带哨兵节点),每个节点存储 “前驱指针、后继指针、数据”,用void*兼容任意类型指针:
template<class T>
class __list_node{
typedef void* void_pointer;
void_pointer prev; // 前驱节点指针(void* 通用指针,避免类型耦合)
void_pointer next; // 后继节点指针
T data; // 节点存储的实际数据
};
- 哨兵节点:list 的
node成员是 “空节点”(不存数据),prev指向尾节点,next指向第一个节点,实现 “循环链表”; - 优势:增删节点仅需调整指针(O (1)),无需移动其他元素。
2. list 的类结构
list 封装链表的核心信息(哨兵节点、迭代器类型),分配器负责节点的内存管理:
template<class T, class _Alloc = std::allocator<T>>
class list{
protected:
typedef __list_node<T> list_node; // 链表节点类型
public:
typedef list_node* link_type; // 节点指针类型
typedef __list_iterator<T,T&,T*> iterator; // 双向迭代器
protected:
link_type node; // 哨兵节点(链表的“头”,不存数据)
};
3. 迭代器操作限制的本质
{
int i(6);
++++i; // 合法:前置++返回引用(int&),可连续调用
//i++++; // 非法:后置++返回值(临时int),临时对象不能再++
}
- 前置 ++(
++it):返回迭代器的引用(iterator&),无副本开销,支持连续调用; - 后置 ++(
it++):返回迭代器的副本(iterator),临时对象无法再次 ++; - list 迭代器是双向迭代器:仅支持
++/--,不支持it+n/it1-it2(无随机访问能力),因此std::sort无法直接排序 list(需用list::sort)。
关键要点
- list 底层是双向循环链表,增删任意位置 O (1),但随机访问 O (n);
- list 迭代器是双向迭代器,前置 ++ 效率高于后置 ++;
- list 的哨兵节点简化了空链表 / 首尾操作的边界判断。
Iterator 遵循原则和 Traits 的作用和设计
核心解析
1. 迭代器的 5 个核心义务(必须回答算法的提问)
STL 算法依赖迭代器提供 5 类信息,否则无法通用化:
| 信息类型 | 作用 | 示例(list 迭代器) |
|---|---|---|
iterator_category |
迭代器类型(决定算法的实现方式) | bidirectional_iterator_tag(双向迭代器) |
difference_type |
两个迭代器的差值类型 | ptrdiff_t(有符号整数,指针差值) |
value_type |
迭代器指向的元素类型 | T(list 存储的元素类型) |
reference_type |
元素的引用类型 | T& / const T& |
pointer_type |
元素的指针类型 | T* / const T* |
2. 类迭代器的信息暴露(直接嵌套类型)
类迭代器(如__list_iterator)通过嵌套类型直接暴露上述信息:
template<class T, class Ref, class Ptr>
struct __list_iterator{
// 1. 迭代器类型:双向迭代器
typedef bidirectional_iterator_tag iterator_category;
// 2. 元素类型
typedef T value_type;
// 3. 指针类型
typedef Ptr pointer;
// 4. 差值类型(指针差值)
typedef ptrdiff_t difference_type;
// 5. 引用类型
typedef Ref reference;
};
3. Traits 的核心问题:原生指针不是类,无嵌套类型
vector的迭代器是原生指针(T*),指针不是类,无法定义嵌套类型(如T*::value_type)。Traits 通过偏特化解决此问题:
步骤 1:泛化版本(处理类迭代器)
template <class I>
struct iterator_traits{
// 取类迭代器的嵌套value_type
typedef typename I::value_type value_type;
// 同理可萃取其他4类信息(category/difference_type等)
};
步骤 2:偏特化版本(处理原生指针)
// 偏特化1:处理非const原生指针(T*)
template<class T>
struct iterator_traits<T*>{
typedef T value_type; // 指针的value_type是指向的类型
};
// 偏特化2:处理const原生指针(const T*)
template<class T>
struct iterator_traits<const T*>{
typedef T value_type; // 算法需要“可修改的类型”,而非const T
};
4. Traits 的实战应用(rotate 算法)
rotate算法根据迭代器类型选择不同实现(随机访问迭代器效率更高):
// 外层接口:统一调用,不关心迭代器类型
template<typename _ForwardIterator>
inline void rotate(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last){
// 萃取迭代器类型,转发到对应版本
std::__rotate(first, middle, last,std::__iterator_category(first));
}
// 萃取迭代器类型的函数
template<typename _list>
inline typename iterator_traits<_list>::iterator_category __iterator_category(const _list&){
return typename iterator_traits<_list>::iterator_category();
}
// 随机访问迭代器的rotate实现(高效,支持下标/减法)
template<typename _RandomAccessIterator>
void __rotate(_RandomAccessIterator first, _RandomAccessIterator middle, _RandomAccessIterator last,random_access_iterator_tag){
typedef typename std::iterator_traits<_RandomAccessIterator>::value_type _ValueType;
typedef typename std::iterator_traits<_RandomAccessIterator>::difference_type _Distance;
_Distance __n = last - first; // 随机访问迭代器支持减法
_Distance __k = middle - first;
/* 基于下标旋转,效率O(n) */
}
5. Traits 的扩展(type traits/char traits)
type traits:萃取类型的特性(如是否是指针、是否是类、是否可拷贝);char traits:萃取字符类型的特性(如char/wchar_t的比较、拷贝逻辑);- 核心思想:“萃取” 类型的隐藏信息,让算法通用化。
关键要点
- Traits 是 STL 泛型编程的 “粘合剂”,解决 “类迭代器” 和 “原生指针” 的统一处理;
- 迭代器的分类(随机访问 / 双向 / 前向)是算法优化的核心;
- Traits 的核心实现依赖模板偏特化。
深入探索 vector
核心解析
1. vector 的内存布局(3 个核心迭代器)
vector 用 3 个指针(迭代器)管理连续内存,是其所有操作的基础
template<class T, class Alloc = std::allocator<T>>
class vector{
public:
typedef T value_type;
typedef value_type* pointer; // vector迭代器是原生指针
typedef value_type& reference;
typedef size_t size_type;
protected:
iterator start; // 指向数组第一个元素
iterator end; // 指向数组最后一个元素的下一个位置(已用尾)
iterator end_of_storage; // 指向数组内存的最后位置(总容量尾)
};
size()=end - start:已使用元素数;capacity()=end_of_storage - start:总容量(已用 + 备用);empty()=start == end:是否为空。
2. 核心成员函数解析
(1)随机访问:operator []
reference operator[](size_type i){
return *(begin()+i); // 原生指针支持+i,随机访问O(1)
}
- 优势:随机访问效率极高(连续内存);
- 风险:无越界检查(
at()有越界检查,抛异常)。
(2)push_back:尾插(核心逻辑)
void push_back(const value_type& value){
if(end != end_of_storage){ // 有备用空间
construct(end, value); // 构造对象(分配器的construct)
++end; // 已用尾后移
}
else{ // 无备用空间,调用扩容辅助函数
insert_aux(end(), value);
}
}
(3)insert_aux:扩容核心
当无备用空间时,vector 会重新分配内存→拷贝旧数据→释放旧内存,是 vector 性能损耗的核心:
template<class T, class Alloc = std::allocator<T>>
void vector<T,Alloc>::insert_aux(iterator position, const T& x){
if(end() != end_of_storage){ // 有备用空间(其他函数调用时可能有)
construct(end,*(end-1)); // 尾位置构造最后一个元素的副本
++end;
T x_copy = x;
// 从position开始后移元素,腾出position位置
copy_backward(position,end-2,end-1);
*position = x_copy;
}
else{ // 无备用空间,扩容
const size_type old_size = size();
// 扩容规则:旧容量非0则翻倍,否则初始化为1
const size_type new_size = old_size != 0 ? 2*old_size : 1;
// 分配新内存(new_size大小)
iterator new_start = data_allocator::allocate(new_size);
iterator new_end = new_start;
// 1. 拷贝旧数据到新内存([start, position))
new_end = uninitialized_copy(start, position, new_start);
// 2. 构造新元素x
construct(new_end, x);
++new_end;
// 3. 拷贝剩余旧数据([position, end))
new_end = uninitialized_copy(position, end, new_end);
// 4. 释放旧内存
destroy(begin(), end());
deallocate(start, end_of_storage - start);
// 5. 更新指针
start = new_start;
end = new_end;
end_of_storage = new_start + new_size;
}
}
3. vector 的核心特性
- 连续内存:随机访问 O (1),但扩容时拷贝数据(性能损耗);
- 迭代器是原生指针:支持
++/--/+/+=等所有指针操作; - 扩容因子:默认 2 倍(不同编译器可能调整,如 1.5 倍),减少扩容次数。
关键要点
- vector 的性能瓶颈在 “扩容拷贝”,提前
reserve()指定容量可避免频繁扩容; - 连续内存是 vector 的核心优势,也是其扩容的根源;
- vector 迭代器是原生指针,属于 “随机访问迭代器”。
深入探索 deque, queue 和 stack
核心解析
1. deque 的底层结构(分段连续内存)
deque 是 “双端队列”,解决 vector “头部插入效率低” 和 list “随机访问效率低” 的问题,底层是:
- 用
map(指针数组)管理多个固定大小的buffer(分段连续内存); - 每个
buffer存储若干元素,map的每个元素指向一个buffer; - 迭代器封装
cur(当前元素)、first(buffer 头)、last(buffer 尾)、node(指向 map 的指针)。
(1)deque 迭代器设计
template<class T, class Ref, class Ptr>
struct __deque_iterator{
typedef random_access_iterator_tag iterator_category; // 随机访问迭代器
typedef T value_type;
typedef Ptr pointer;
typedef ptrdiff_t difference_type;
typedef Ref reference;
typedef T** map_pointer; // 指向map的指针(map是T**类型)
typedef size_t size_type;
typedef __deque_iterator self;
T* cur; // 指向当前buffer中的当前元素
T* first; // 指向当前buffer的头
T* last; // 指向当前buffer的尾
map_pointer node; // 指向map中当前buffer的指针
// 前置++:跨buffer处理
self operator++(){
++cur; // 先移动到当前buffer的下一个元素
if( cur == last ){ // 到达当前buffer尾部
set_node(node + 1); // 切换到下一个buffer
cur = first; // 指向新buffer的头部
}
return *this;
}
// 后置++:返回副本
self operator++(int){
self tmp = *this;
++*this; // 复用前置++
return tmp;
}
};
(2)deque 的类结构
template<class T, class Alloc = std::allocator<T>, size_t BufSize = 0>
class deque{
public:
typedef __deque_iterator<T, T&, T*, BufSize> iterator;
protected:
typedef pointer* map_pointer; // T**,map的类型
iterator start; // 指向第一个元素的迭代器
iterator end; // 指向最后一个元素的下一个位置
map_pointer map; // 指向map数组
size_type map_size; // map数组的大小
};
(3)deque 的插入逻辑
- 头部 / 尾部插入:检测对应端的 buffer 是否有空位,无则新增 buffer;
- 中间插入:检测离头部 / 尾部更近,向近的一端移动元素,减少移动次数;
- 迭代器是随机访问迭代器:支持
it+n(底层计算跨 buffer 的偏移)。
2. queue 和 stack:容器适配器(Adapter)
queue(队列,FIFO)和 stack(栈,LIFO)不是独立容器,而是 “容器适配器”—— 封装底层容器(默认 deque),提供受限接口:
template<class T, class Sequence = deque<T> >
class queue{
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::pointer pointer;
typedef typename Sequence::reference reference;
protected:
Sequence c; // 底层容器(默认deque)
public:
// 所有操作都转发给底层容器c
bool empty() const { return c.empty(); }
size_t size() const { return c.size(); }
reference front() { return c.front(); }
reference back() { return c.back(); }
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_front(); }
};
核心特性
- 适配器本质:“封装底层容器,提供特定接口”,可替换底层容器(如
stack<T, vector<T>>); - 无迭代器:queue/stack 不允许遍历(屏蔽底层容器的迭代器),保证 “FIFO/LIFO” 的语义;
- 底层默认 deque:兼顾 vector 的随机访问和 list 的双端增删效率。
关键要点
- deque 是 “分段连续内存”,双端增删 O (1),随机访问 O (1)(底层计算偏移);
- queue/stack 是适配器,不是独立容器,核心是 “接口封装”;
- deque 迭代器是随机访问迭代器,但实现比 vector 复杂(跨 buffer 处理)。
深入探索 RB 树
核心解析
1. RB 树的模板参数(核心解耦)
RB 树是 “自平衡二叉搜索树”,STL 中 RB 树的模板参数设计最大化解耦:
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc = std::allocator<Key> >
class rb_tree{
// Key:键类型(排序依据)
// Value:节点存储的数值类型(Key+Data)
// KeyOfValue:从Value中提取Key的仿函式
// Compare:Key的比较规则(默认less<Key>)
// Alloc:分配器
};
2. 核心插入接口
insert_unique():插入的 Key 必须唯一(重复则插入失败);insert_equal():允许 Key 重复(set 用前者,multiset 用后者)。
3. 配套仿函式(unary_function/binary_function)
STL 仿函式继承这两个基类,统一 “参数 / 返回值类型” 的命名,方便算法萃取:
(1)unary_function:一元仿函式基类
template<class Arg, class Result>
struct unary_function{
typedef Arg argument_type; // 输入参数类型
typedef Result result_type; // 返回值类型
};
// identity:返回参数本身(从Value中提取Key,当Value=Key时用)
template<class T>
struct identity: public unary_function<T, T>{
const T& operator()(const T& x) const{
return x;
}
};
(2)binary_function:二元仿函式基类
template<class Arg1, class Arg2, class Result>
struct binary_function{
typedef Arg1 first_argument_type; // 第一个参数类型
typedef Arg2 second_argument_type; // 第二个参数类型
typedef Result result_type; // 返回值类型
};
// less:小于比较(默认排序规则)
template<class T>
struct less: public binary_function<T, T, bool>{
bool operator()(const T& x, const T& y) const{
return x < y;
}
};
4. RB 树的使用示例
// Key=int,Value=int,KeyOfValue=identity<int>(Value=Key),Compare=less<int>
rb_tree<int, int, identity<int>, less<int>, allocator<int>> tree;
关键要点
- RB 树是 set/map 的底层,保证插入 / 删除 / 查找的时间复杂度 O (logn);
- KeyOfValue 仿函式实现 “Value 到 Key 的解耦”(如 map 的 Value 是 pair<Key,T>);
- insert_unique/insert_equal 是区分 set/multiset 的核心。
深入探索 set,multiset
核心解析
1. set 的模板设计(基于 RB 树)
set 是 “有序不重复集合”,完全封装 RB 树,仅暴露受限接口:
template<class Key, class Compare = std::less<Key>, class Alloc = std::allocator<Key> >
class set{
typedef Key key_type;
typedef Key value_type; // set的Key=Value(无数据部分)
typedef Compare key_compare;
typedef Compare value_compare;
private:
// 底层RB树:Key=Key,Value=Key,KeyOfValue=identity<Key>
typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;
rep_type t; // 底层RB树对象
public:
// 迭代器是const的:不允许通过迭代器修改元素(破坏排序)
typedef typename rep_type::const_iterator iterator;
};
2. set 与 multiset 的核心区别
| 特性 | set | multiset |
|---|---|---|
| 底层接口 | 调用 rb_tree::insert_unique () | 调用 rb_tree::insert_equal () |
| Key 重复性 | 不允许重复 | 允许重复 |
| 插入返回值 | pair<iterator, bool>(是否插入成功) | iterator(仅返回插入位置) |
3. 核心特性
- 有序性:基于 RB 树的排序,遍历是有序的;
- const 迭代器:禁止修改元素(否则破坏 RB 树的排序规则);
- 适配器本质:set 可视为 “RB 树的适配器”,封装 RB 树的接口。
关键要点
- set 的 Key=Value,无数据部分(仅存储键);
- multiset 允许 Key 重复,其余与 set 一致;
- set 的迭代器是 const 的,无法修改元素。
深入探索 map,multimap
核心解析
1. map 的模板设计(基于 RB 树)
map 是 “有序键值对集合”,Value 是pair<const Key, T>(Key 不可修改):
template<class Key, class T, class Compare = std::less<Key>, class Alloc = std::allocator<Key> >
class map{
public:
typedef Key key_type; // 键类型
typedef T data_type; // 数据类型
typedef T mapped_type; // 映射值类型(等价于data_type)
typedef pair<const Key, T> value_type; // 节点存储的Value(Key不可改)
typedef Compare key_compare; // 键比较规则
private:
// 底层RB树:Key=Key,Value=pair<const Key,T>,KeyOfValue=select1st(提取pair的first)
typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t; // 底层RB树对象
};
2. select1st 仿函式(提取 pair 的 Key)
template<class Arg, class Result>
struct unary_function{
typedef Arg argument_type;
typedef Result result_type;
};
// select1st:从pair中提取第一个元素(Key)
template<class Pair>
struct select1st:public unary_function<Pair, typename Pair::first_type>{
const typename Pair::first_type& operator()(const Pair& x) const{
return x.first; // 提取Key(pair的first)
}
};
3. map 的核心特性
(1)value_type 是 pair<const Key, T>
- Key 是 const 的:禁止修改(破坏 RB 树排序);
- T 是可修改的:通过
iterator->second修改数据部分。
(2)operator [](核心特色)
// 简化版逻辑
T& operator[](const Key& k){
// 1. 尝试插入pair<k, T()>,若k已存在则返回已有的second
pair<iterator, bool> res = t.insert(value_type(k, T()));
// 2. 返回数据部分的引用
return res.first->second;
}
- 逻辑:“查找→不存在则插入默认值→返回数据引用”;
- 风险:若 Key 不存在,会插入默认值(空对象),可能导致意外的内存占用。
(3)map 与 multimap 的区别
- map:调用 rb_tree::insert_unique (),Key 唯一;
- multimap:调用 rb_tree::insert_equal (),Key 可重复,无 operator [](无法确定返回哪个 Value)。
关键要点
- map 的 Value 是 pair<const Key, T>,Key 不可改,T 可改;
- operator [] 是 map 的特色,底层是 “插入 + 查找”;
- multimap 无 operator [],允许 Key 重复。
深入探索 hashtable
核心解析
1. hashtable 的核心设计
hashtable 是 “哈希表”,追求 O (1) 的查找效率,STL 中 hashtable 的模板参数:
template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = std::allocator<Value> >
class hashtable{
public:
typedef HashFcn hasher; // 哈希函数(将Key映射为哈希值)
typedef EqualKey key_equal; // Key的相等判断规则
typedef size_t size_type;
private:
key_equal equals; // 相等判断对象
hasher hash; // 哈希函数对象
ExtractKey get_key; // 从Value提取Key的仿函式
typedef __hashtable_node<Value> node; // 哈希桶节点(链表节点)
std::vector<node*,Alloc> buckets; // 桶数组(每个桶是链表头)
size_type num_elements; // 元素总数
};
2. 哈希冲突解决方式
STL hashtable 采用拉链法(链表法):
- 每个桶(
buckets的元素)是一个链表的头指针; - 哈希值相同的元素存入同一个桶的链表中;
- 对比 “开放寻址法”:拉链法删除逻辑简单,无 “墓碑” 问题。
3. 扩容规则
当哈希表的负载因子(num_elements / buckets.size())超过阈值时,触发扩容:
- 扩容大小:距离当前
buckets.size() * 2最近的素数(减少哈希冲突); - 扩容逻辑:新建更大的桶数组→重新计算所有元素的哈希值→迁移到新桶→释放旧桶。
4. 核心特性
- 无序性:哈希表不保证元素顺序;
- 查找效率:理想 O (1),最坏 O (n)(哈希冲突严重);
- 哈希函数:默认
hash<Key>,可自定义(如 string 的哈希)。
关键要点
- hashtable 是 unordered_set/unordered_map 的底层,追求极致查找效率;
- 拉链法解决哈希冲突,扩容选素数桶大小减少冲突;
- 负载因子是扩容的核心阈值(通常 0.75)。
更多推荐



所有评论(0)