深入剖析底层实现原理,包括核心数据结构设计、内存分配机制、扩容策略等关键技术细节。不仅要掌握API的使用方法,更要理解容器类库的架构思想和实现逻辑,达到知其然更知其所以然的深度。
文章目录
一、标准容器
STL容器分为以下几类:
- 序列容器:vector, deque, list
- 容器适配器:stack, queue, priority_queue
- 关联容器:set, multiset, map, multimap
- 无序关联容器:unordered_set, unordered_multiset, unordered_map, unordered_multimap
1. vector 向量容器
底层数据结构
是一个动态一维数组(连续内存空间)
vector的扩容
- 扩容机制:当容量不足时,按当前容量的2倍重新分配内存
- 扩容代价:需要拷贝原有元素到新空间,时间复杂度O(n)

声明与初始化
vector<int> vec;// 空
vectorvector<int> vec(10);// 10个元素,默认值0
vector<int> vec(10, 5);// 10个元素,值均为5
vector<int> vec2(vec);// 拷贝构造
元素操作
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
push_back(val) | O(1)* | 尾部插入,可能触发扩容 |
pop_back() | O(1) | 尾部删除 |
| insert(it, val) | O(n) | 在迭代器位置插入,可能导致扩容和后续元素移动 |
| erase(it) | O(n) | 删除迭代器位置元素,导致后续元素前移 |
| operator[] | O(1) | 随机访问(不检查越界) |
| at(i) | O(1) | 随机访问(会检查越界,抛出异常) |
*注:均摊时间复杂度为O(1),考虑扩容因素
迭代器与遍历
// 常规迭代器
for(auto it = vec.begin(); it != vec.end(); ++it)
// 反向迭代器
for(auto rit = vec.rbegin(); rit != vec.rend(); ++rit)
// 范围for循环(C++11)
for(auto& val : vec)
// 算法遍历
for_each(vec.begin(), vec.end(), [](int val){...});
insert、erase 导致迭代器失效:
insert插入元素- 若触发扩容(
size == capacity),所有迭代器失效(包括begin(),end())。 - 未触发扩容,仅 插入位置及之后的迭代器失效(后面的元素被移动)。
- 若触发扩容(
erase删除元素- 被删除元素及之后的所有迭代器失效(前面的元素不受影响)。
- 正确做法:用
erase的返回值更新迭代器(指向下一个有效元素)。
常用成员函数
| 函数 | 说明 |
|---|---|
| size() | 当前元素数量 |
| empty() | 判断是否为空 |
| reserve(n) | 预分配至少能容纳n个元素的内存空间(不改变size) |
| resize(n) | 调整容器大小为n个元素(多删少补默认值) |
| v1.swap(v2) | 与另一个vector交换内容(高效操作,O(1)时间复杂度) |
补充说明:
reserve(n)给vector预留空间,只给容器底层开辟指定大小的内存空间,并不会添加新的元素。resize(n)容器扩容用的,不仅给容器底层开辟指定大小的内存空间,还会添加新的元素。v1.swap(v2)两个容器进行元素交换。是高效操作,仅交换内部指针,不复制元素。
2. deque 双端队列
底层数据结构
是一个二维数组,第一维用来存放中控区,第二维用来存放缓冲区的数据。
分段连续空间(多个固定大小的数组 + 中控器)
deque采用一块map作为主控。这里map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段连续线性空间,称为缓冲区,这个缓冲区才是deque的主体,大致如下图:

deque的扩容
- 以两倍的方式进行扩容
- 每次扩容后,原来第二维的数组,从新的第一维数组的下表 oldsize/2 开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加

声明与初始化
deque<int> dq;// 空
deque<int> dq(10);// 10个元素,默认值0
deque<int> dq(10, 5);// 10个元素,值均为5
deque<int> dq2(dq);// 拷贝构造
元素操作
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
push_front(val) | O(1) | 头部插入 |
| push_back(val) | O(1) | 尾部插入 |
pop_front() | O(1) | 头部删除 |
| pop_back() | O(1) | 尾部删除 |
| insert(it, val) | O(n) | 中间插入效率低于list |
| erase(it) | O(n) | 中间删除效率低于list |
| operator[] | O(1) | 随机访问(比vector稍慢) |
特点
- 首尾操作性能接近list,随机访问性能接近vector
- 迭代器失效情况:
- 首尾插入只会使被操作位置的迭代器失效
- 中间插入/删除会使所有迭代器失效
常用成员函数
d.front();// 访问首元素
d.back();// 访问尾元素
d.resize(n);// 调整大小
3. list 双向链表容器
底层数据结构
- 双向循环链表
- 每个节点存储数据+前后指针
- 插入删除不会使其他迭代器失效
声明与初始化
list<int> lst;// 空
list<int> lst(10);// 10个元素,默认值0
list<int> lst(10, 5);// 10个元素,值均为5
list<int> lst2(lst);// 拷贝构造
元素操作
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
push_front(val) | O(1) | 头部插入 |
| push_back(val) | O(1) | 尾部插入 |
pop_front() | O(1) | 头部删除 |
| pop_back() | O(1) | 尾部删除 |
| insert(it, val) | O(1) | 任意位置高效插入 |
| erase(it) | O(1) | 任意位置高效删除 |
remove(val) | O(n) | 删除所有等于val的元素 |
特点
-
不支持随机访问(没有
operator[]) -
插入删除不会使其他迭代器失效(被删除元素的迭代器除外)
-
特有的操作:
lst.sort();// 链表排序(比通用sort算法更高效) lst.unique();// 去重(需先排序) lst.merge(lst2);// 合并两个有序链表 lst.splice(it, lst2);// 将lst2的所有元素转移到指定位置
4.vector、deque、list对比
vector 和 deque 之间的区别?
- 底层数据结构:vector是动态一维数组,deque是二维数组
- 头尾插入元素的时间复杂度:头部 deque O(1) vector O(n), 尾部都是O(1)。如果需要频繁在头部操作,deque更合适
- 对内存的使用效率:vector需要的内存空间必须是连续的;deque可以分块进行数据存储,不需要内存空间是连续的
- 在中间进行 insert 或者erase,时间复杂度都是O(n), vector 的内存完全连续,它移动元素会更加方便。由于deque的第二维内存空间不是连续的,所以在deque中间进行元素的insert或者erase, 造成元素移动的时候,比vector要慢
vector 和 list 之间的区别?
- 底层数据结构:vector是动态一维数组,list是一个双向循环链表
- 随机访问:vector支持随机访问O(1);list不支持随机访问,访问某个元素的效率为O(n)
- 对内存的使用效率:vector需要的内存空间必须是连续的;list不需要内存空间是连续的
5.容器适配器
怎么理解容器适配器
- 容器适配器底层没有自己的数据结构,它是另外一个容器的封装,它的方法全部由底层依赖的容器实现
- 没有实现自己的迭代器
- 主要的容器适配器:stack(栈)、queue(队列)、priority_queue(优先级队列)
stack: 后进先出数据结构,默认基于deque实现
queue: 先进先出数据结构,默认基于deque实现
priority_queue: 元素按优先级排序,最高优先级元素总是位于队首。默认基于vector实现,使用大堆结构
主要成员函数:
| 操作 | stack (栈) | queue (队列) | priority_queue (优先队列) |
|---|---|---|---|
| 插入元素 | push(val) | push(val) | push(val) |
| 删除元素 | pop() | pop() | pop() |
| 访问顶部/首元素 | top() | front() | top() |
| 访问尾部元素 | - | back() | - |
| 检查是否为空 | empty() | empty() | empty() |
| 获取元素数量 | size() | size() | size() |
【思考】stack => deque 为什么不依赖vector? queue => deque 为什么不依赖vector?
- 内存效率与扩容策略
- vector 需要连续内存,扩容时需整体拷贝(O(n)),初始扩容频繁(0→1→2→4→8…)。
- deque 分块分配(默认是 4096/sizeof(int) = 1024),按需增删块,无整体重分配,内存利用率更高。
- 对于queue来说,需要支持尾部插入,头部删除, O(1) 。如果queue依赖vector, 其出队效率很低
- 大规模数据下的稳定性
- vector 大连续内存可能引发碎片化或分配失败。
- deque 分散存储避免连续内存需求,更适合海量数据场景。
【思考】priority_queue 为什么依赖vector?
priority_queue 底层使用 堆(大根堆),而堆依赖 随机访问(O(1)) 和 连续内存 来高效计算父子节点位置(parent = (i-1)/2, left = 2i+1)。vector 的连续内存布局完美匹配这一需求,而 deque 的分块存储会显著增加计算复杂度。
6.关联容器
关联容器是C++标准库中基于键(Key)高效存储和检索数据的容器,与顺序容器不同,它们不依赖物理存储顺序,而是通过红黑树或哈希表实现快速查找。
| 容器类型 | 底层实现 | 键唯一性 | 元素顺序 | 查找复杂度 |
|---|---|---|---|---|
| set | 红黑树 | 唯一 | 升序 | O(log n) |
| multiset | 红黑树 | 可重复 | 升序 | O(log n) |
| map | 红黑树 | 唯一 | 按键升序 | O(log n) |
| multimap | 红黑树 | 可重复 | 按键升序 | O(log n) |
| unordered_set | 哈希表 | 唯一 | 无序 | O(1)~O(n) |
| unordered_multiset | 哈希表 | 可重复 | 无序 | O(1)~O(n) |
| unordered_map | 哈希表 | 唯一 | 无序 | O(1)~O(n) |
| unordered_multimap | 哈希表 | 可重复 | 无序 | O(1)~O(n) |
注:
- 有序容器(set/map等)基于红黑树,保证元素按键排序,查找稳定(O(log n))。
- 无序容器(unordered_set/unordered_map等)基于哈希表,平均查找O(1),最坏情况O(n)(哈希冲突时)。
核心操作
(1)通用操作(所有关联容器支持)
| 操作 | 说明 | 示例 |
|---|---|---|
| insert(key) | 插入元素 | set.insert(42); |
| erase(key) | 删除指定键的元素 | map.erase("Alice"); |
| find(key) | 查找元素,返回迭代器 | auto it = set.find(42); |
| count(key) | 统计键出现的次数 | multiset.count(5); |
| size() | 返回元素个数 | map.size(); |
| empty() | 判断是否为空 | if (set.empty()) {...} |
| clear() | 清空容器 | unordered_set.clear(); |
(2)有序容器特有操作
| 操作 | 说明 | 示例 |
|---|---|---|
| lower_bound(key) | 返回第一个≥key的迭代器 | auto it = set.lower_bound(10); |
| upper_bound(key) | 返回第一个>key的迭代器 | auto it = set.upper_bound(10); |
| equal_range(key) | 返回键匹配的范围(pair) | auto [first, last] = map.equal_range("Bob"); |
(3)无序容器特有操作
| 操作 | 说明 | 示例 |
|---|---|---|
| bucket_count() | 返回哈希桶数量 | unordered_map.bucket_count(); |
| load_factor() | 返回当前负载因子 | unordered_set.load_factor(); |
| rehash(n) | 调整哈希桶数量 | unordered_map.rehash(100); |
二、近容器
近容器是C++标准库中提供的一组特殊容器,它们在功能上与标准容器相似,但针对特定场景进行了优化或简化。下面总结 array, string, bitset 的常见方法:
array(固定大小数组)
| 操作 | 说明 | 示例 |
|---|---|---|
| size() | 返回元素数量 | arr.size() |
| empty() | 检查是否为空 | arr.empty() |
| operator[] | 无检查访问 | arr[2] = 5 |
| at() | 带边界检查访问 | arr.at(3) |
| front()/back() | 访问首尾元素 | arr.front() |
| fill() | 填充相同值 | arr.fill(0) |
| data() | 返回底层数组指针 | int* p = arr.data() |
string(字符串容器)
| 操作 | 说明 | 示例 |
|---|---|---|
| size()/length() | 返回字符数量 | str.size() |
| empty() | 检查是否为空 | str.empty() |
| operator[] | 无检查访问 | str[1] |
| at() | 带边界检查访问 | str.at(2) |
| c_str() | 返回C风格字符串 | str.c_str() |
| append() | 追加内容 | str.append("abc") |
| substr() | 获取子串 | str.substr(2,3) |
| find() | 查找子串 | str.find("xyz") |
bitset(位集合容器)
| 操作 | 说明 | 示例 |
|---|---|---|
| size() | 返回位数 | bitset.size() |
| count() | 统计1的位数 | bitset.count() |
| test() | 检查指定位 | bitset.test(3) |
| set() | 置位(设为1) | bitset.set(2) |
| reset() | 复位(设为0) | bitset.reset(4) |
| flip() | 翻转位 | bitset.flip(1) |
| to_string() | 转为字符串 | bitset.to_string() |
三、迭代器
以下整理几种常用迭代器类型:
| 迭代器类型 | 读写权限 | 遍历方向 | 获取方法 | 特点 | 示例代码片段 |
|---|---|---|---|---|---|
| iterator | 读写 | 正向 | begin()/end() | 可以修改元素值 | `auyo it = v.begin(); |
| *it = 10;` | |||||
| const_iterator | 只读 | 正向 | cbegin()/cend() | 禁止修改元素值 | `auyo cit = v.cbegin(); |
| int x = *cit;` | |||||
| reverse_iterator | 读写 | 反向 | rbegin()/rend() | 从尾向头遍历,可修改 | `auto rit = v.rbegin(); |
| *rit = 5;` | |||||
| const_reverse_iterator | 只读 | 反向 | crbegin()/crend() | 从尾向头遍历,不可修改 | `auto crit = v.crbegin(); |
| int y = *crit;` |
四、函数对象
函数对象(Function Object),也称为仿函数(Functor),是C++中行为类似函数的对象。它们通过重载 operator() 实现函数调用功能。
class Add {
public:
int operator()(int a, int b) const {
return a + b;
}
};
// 使用
Add adder;
int sum = adder(3, 4); // 调用operator()
c++用函数对象来代替函数指针
1.通过函数对象调用opertator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够用inline内联调用)效率高
2.因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多的信息
五、泛型算法
泛型算法是C++标准库中一组与容器类型无关的通用算法,通过迭代器抽象与各种容器协作,提供高效的数据处理能力。
特点一:泛型算法接收的都是迭代器
特点二:泛型算法的参数还可以接收函数对象
C++常用泛型算法
| 算法类别 | 算法名称 | 功能描述 | 示例用法 |
|---|---|---|---|
| 非修改序列算法 | find | 查找第一个等于值的元素 | auto it = find(v.begin(), v.end(), 42); |
| find_if | 查找第一个满足条件的元素 | find_if(v.begin(), v.end(), [](int x){ return x>10; }); | |
| count | 统计等于值的元素数量 | int n = count(v.begin(), v.end(), 5); | |
| for_each | 对每个元素执行操作 | for_each(v.begin(), v.end(), [](int& x){ x*=2; }); | |
| 修改序列算法 | copy | 复制序列到目标位置 | copy(src.begin(), src.end(), dest.begin()); |
| transform | 对元素进行转换并存储 | transform(v.begin(), v.end(), result.begin(), [](int x){ return x*x; }); | |
| replace | 替换指定值的元素 | replace(v.begin(), v.end(), 0, 42); | |
| 排序与相关算法 | sort | 快速排序(不稳定) | sort(v.begin(), v.end()); |
| stable_sort | 稳定排序 | stable_sort(v.begin(), v.end()); | |
| binary_search | 二分查找(要求已排序) | bool b = binary_search(v.begin(), v.end(), 42); |

213

被折叠的 条评论
为什么被折叠?



