C++ STL 知识点整理

如果是 C++ / UE / 游戏客户端面试,STL 不需要把整个标准库背完,真正高频的是:容器底层结构 + 时间复杂度 + 迭代器失效 + 常用算法 + C++11 之后的移动/emplace + map/set 与 unordered 系列区别。

你可以把 STL 准备分成 6 块。

1. 先把 STL 整体框架说清楚

STL 主要由容器、迭代器、算法、函数对象、适配器、分配器等部分组成。容器负责存数据,迭代器负责统一访问,算法通过迭代器操作容器,从而做到算法和数据结构解耦。

最核心的三个:

  • Container 容器
  • Iterator 迭代器
  • Algorithm 算法

比如:

std::vector<int> nums = {3, 1, 2};
std::sort(nums.begin(), nums.end());

这里:

vector
↓
容器
begin/end
↓
迭代器
sort
↓
算法

2. 容器是绝对重点

至少要熟练这几个:

  • vector
  • list
  • deque
  • map
  • set
  • unordered_map
  • unordered_set
  • stack
  • queue
  • priority_queue

其中面试最重要的是:

  • vector
  • map / set
  • unordered_map / unordered_set

3. vector:STL 面试第一大户

先记本质:

vector 底层是动态连续数组。

例如:

std::vector<int> v;

内存大致:

[1][2][3][4][5]

连续排列。

所以支持:

v[3]

O(1) 随机访问。

vector 为什么尾插快,中间插入慢?

尾部:

v.push_back(10);

如果还有容量:

直接放到最后

O(1)

中间:

v.insert(v.begin() + 2, 10);

后面的元素都得往后移动:

1 2 3 4 5
插入 9
1 2 9 3 4 5
      ↑ ↑ ↑
      后面全部移动

所以:

  • 随机访问 O(1)
  • 尾插 平均 O(1)
  • 中间插入 O(n)
  • 中间删除 O(n)

注意是:

push_back 均摊 O(1),不是永远 O(1)。

4. vector 的扩容机制

非常高频。

假设:

v.size() == v.capacity()

再:

v.push_back(x);

容量不够。

vector 会:

  1. 申请更大的连续内存
  2. 把旧元素拷贝/移动过去
  3. 释放旧内存
  4. 插入新元素

大概:

原来:
[1][2][3][4]
容量不够
↓
新内存:
[1][2][3][4][ ][ ][ ][ ]

至于具体扩容倍率:

C++ 标准没有规定必须是 1.5 倍还是 2 倍,由具体实现决定。

这个别背死。

5. size 和 capacity 区别

v.size();
v.capacity();

size:

当前实际有多少个元素。

capacity:

当前已经申请的内存最多能放多少元素而不用再次扩容。

例如:

size = 5

capacity = 8

表示:

[1][2][3][4][5][ ][ ][ ]

6. reserve 和 resize 的区别

这是经典题。

v.reserve(100);

意思:

提前申请至少能容纳 100 个元素的容量。

通常:

  • capacity 改变
  • size 不变

而:

v.resize(100);

表示:

把实际元素数量调整为 100。

于是:

  • size 改变

而:

v.resize(100);

表示:

把实际元素数量调整为 100。

于是:

  • size 改变

如果变大,还会构造新元素。

一句话:

reserve

→ 改容量

resize

→ 改元素数量

7. vector 迭代器为什么会失效?

这一块面试特别爱问。

例如:

std::vector<int> v = {1,2,3};
auto it = v.begin();
v.push_back(4);

如果 push_back 导致扩容:

原内存释放

整个 vector 搬家

那么:

it

还指向旧内存。

于是失效。

所以:

vector 发生重新分配时,原来的指针、引用、迭代器都会失效。

如果没有扩容,中间插入或删除,也可能导致插入/删除位置及之后的迭代器失效。

这个要会。

8. vector 和 list 区别

第二高频。

vector:

  • 连续内存
  • 随机访问 O(1)
  • 中间插删 O(n)
  • 缓存友好

list:

  • 双向链表
  • 内存不连续
  • 随机访问 O(n)
  • 已知位置插删 O(1)
  • 缓存局部性差

画出来:

vector:
[1][2][3][4]
list:
[1] ↔ [2] ↔ [3] ↔ [4]

面试官问:

插删很多是不是一定选 list?

不要直接说是。

现代 CPU 上 vector 的缓存局部性很好,即使理论上移动元素,很多实际场景仍然可能比 list 快。

所以更好的回答:

如果需要高频随机访问,优先 vector;如果确实需要在已知节点位置频繁插删且稳定迭代器,才考虑 list。

9. deque 是什么

deque:

双端队列。

支持:

  • push_front
  • push_back
  • pop_front
  • pop_back

通常都比较高效。

它不是简单的“一整块连续数组”。

可以粗略理解成:

索引结构
   ↓
[块1] [块2] [块3] [块4]

分段连续。

所以:

deque[i]

仍然支持随机访问。

10. map / set 底层

这个必须背。

一般实现:

map

set

multimap

multiset

红黑树

因此:

  • 查找 O(log n)
  • 插入 O(log n)
  • 删除 O(log n)

并且:

元素天然有序。

11. map 和 set 区别

set:

std::set<int>

保存:

key

而 map:

std::map<int, std::string>

保存:

key-value

例如:

map<int, string> players;
players[1001] = "Tom";

本质:

1001 → Tom

12. 为什么 set 的 key 不能修改?

你之前也问过类似问题。

因为 set 通常基于有序树。

假设:

        10
       /  \
      5    20

如果你直接把:

5 → 100

树的有序关系直接被破坏:

        10
       /
     100

错误。

所以:

set 中元素本质上承担 key 的角色,不能随意原地修改。

map 的:

pair<const Key, Value>

key 也是 const。

13. unordered_map / unordered_set 底层

一般:

哈希表

例如:

std::unordered_map<int, string>

查找大致:

key
↓
hash(key)
↓
计算桶位置
↓
在 bucket 中查找

平均复杂度:

  • 查找 O(1)
  • 插入 O(1)
  • 删除 O(1)

但最坏情况:

O(n)

平均复杂度:

  • 查找 O(1)
  • 插入 O(1)
  • 删除 O(1)

但最坏情况:

O(n)

14. map 和 unordered_map 区别

高频到不能再高频。

mapunordered_map
红黑树哈希表
O(log n)平均 O(1)
有序无序
不需要 hashkey 需要可哈希
内存通常相对规整通常有 bucket 等额外开销

一句话:

需要有序遍历、范围查询用 map;主要追求平均快速查找通常考虑 unordered_map。

15. unordered_map 为什么最坏 O(n)

因为哈希冲突。

理想:

bucket0 → A
bucket1 → B
bucket2 → C

极端:

bucket0 → A → B → C → D → E

所有 key 都碰到同一个桶。

那么查找退化成遍历:

O(n)

16. 什么是哈希冲突?

两个不同 key:

hash(key1)
hash(key2)

最终落到同一个 bucket。

就叫:

哈希冲突。

常见解决:

  • 链地址法
  • 开放寻址

标准库具体实现不要求固定方案,不过常见 unordered_* 实现使用 bucket 结构。

17. load_factor 和 rehash

稍微深入一点就会问。

unordered_map

桶越来越满:

元素越来越多

冲突越来越多

查找性能下降

所以达到一定负载后会:

rehash

重新增加 bucket 数量,再重新分配元素位置。

常见接口:

m.load_factor();
m.max_load_factor();
m.rehash();
m.reserve();

18. map 的 operator[] 有坑

例如:

std::map<int, std::string> m;
std::cout << m[10];

即使 key 10 原来不存在:

m[10]

也会:

插入一个默认构造的 value。

所以只是想查找:

m.find(10);

或者 C++20:

m.contains(10);

更合适。

面试官经常问:

map::operator[] 和 find() 区别?

这个要会。

19. stack / queue 本质是什么?

它们严格来说是:

容器适配器。

例如:

std::stack<int>

提供:

  • push
  • pop
  • top

不让你随便访问内部。

默认底层通常是:

deque

queue 也是类似。

20. priority_queue

高频算法题常用。

本质:

优先队列,通常基于 heap。

默认:

std::priority_queue<int>

是大顶堆。

最大元素在:

pq.top();

常见复杂度:

  • top O(1)
  • push O(log n)
  • pop O(log n)

21. STL 的 heap 要掌握什么

至少知道:

  • make_heap
  • push_heap
  • pop_heap
  • sort_heap

以及:

priority_queue

通常基于堆。

堆不是“完全排序”。

例如:

        100
       /   \
     80     90
    / \
   20 50

只保证:

父节点 >= 子节点

大顶堆。

所以取最大值非常快。

22. 迭代器的分类

面试中级题。

常见:

  • input iterator
  • output iterator
  • forward iterator
  • bidirectional iterator
  • random access iterator

重点知道:

random access iterator

重点知道:

  • vector / deque → random access iterator
  • list / set / map → bidirectional iterator
  • forward_list → forward iterator

这就解释了为什么:

std::sort(v.begin(), v.end());

可以。

但:

std::list<int> l;
std::sort(l.begin(), l.end()); // ❌

不行。

因为 std::sort 要求随机访问迭代器。

list 自己提供:

l.sort();

23. STL 算法必须会哪些

至少:

  • sort
  • find
  • find_if
  • count
  • count_if
  • for_each
  • lower_bound
  • upper_bound
  • binary_search
  • remove
  • remove_if
  • transform

其中面试最容易问:

  • sort
  • lower_bound
  • remove

24. std::sort 底层是什么?

这是超级高频。

不能简单回答:

快速排序。

现代标准库实现通常采用:

introsort 思想。

粗略:

先快速排序

递归太深

切堆排序

小区间可能用插入排序

所以兼顾平均性能和最坏情况。

面试回答可以说:

std::sort 通常使用 introsort,是快速排序、堆排序和插入排序的混合策略,复杂度要求为 O(n log n)。

25. lower_bound 是什么?

对有序区间:

std::lower_bound(begin, end, x);

返回:

第一个 >= x 的位置。

upper_bound:

第一个 > x 的位置。

例如:

1 2 2 2 5 7
找 2:
lower_bound
   ↓
1 [2] 2 2 5 7
upper_bound
         ↓
1 2 2 2 [5] 7

底层思想就是二分查找。

26. remove 为什么不真正删除元素?

经典 STL 坑。

std::vector<int> v = {1,2,3,2,4};
std::remove(v.begin(), v.end(), 2);

它不会改变:

v.size()

remove 做的是:

把需要保留的元素往前挪,然后返回新的逻辑结尾。

所以常见:

v.erase(
    std::remove(v.begin(), v.end(), 2),
    v.end()
);

这叫:

erase-remove idiom

C++20 之后还有:

std::erase(v, 2);

更方便。

27. emplace_back 和 push_back

C++11 高频。

v.push_back(Player(100, "Tom"));

和:

v.emplace_back(100, "Tom");

emplace_back:

直接用参数在容器内部构造对象。

它支持完美转发。

大概:

template<class... Args>
void emplace_back(Args&&... args)
{
    new (...) T(std::forward<Args>(args)...);
}

所以这正好和你前面学的:

&&

std::forward

完美转发

串起来。

不过现代编译器 + 移动语义下,不要把它理解成“emplace_back 永远一定比 push_back 快”。

更准确:

更准确:

emplace_back 可以避免某些临时对象构造,并且直接转发构造参数。

28. STL 与移动语义

比如:

std::vector<std::string> v;
std::string s = "hello";
v.push_back(s);

这里是:

拷贝

如果:

v.push_back(std::move(s));

可以:

移动

于是避免复制字符串资源。

这就是为什么 C++11 后 STL 性能提升很大。

29. vector 扩容为什么优先 move?

假设:

vector<Player>

扩容:

旧地址
↓
Player Player Player
新地址
↓
Player Player Player _ _ _

旧对象要转过去。

如果 Player 有:

Player(Player&&) noexcept;

容器就可以高效移动资源。

这就是你之前学:

移动构造

noexcept

vector

的联系。

30. allocator 要掌握到什么程度?

初级/中级面试不用写 allocator。

但至少知道:

STL 容器通过 allocator 抽象内存分配策略,把“内存分配”和“对象构造”与容器逻辑解耦。

以前经典 allocator 模型会区分:

  • allocate
  • construct
  • destroy
  • deallocate

现代标准中实现细节和接口演进很多。

游戏开发里面,自定义内存池、arena allocator 的思想也经常和 STL allocator 联系起来。

31. 面试高频:vector 和 array 区别

std::array<int, 10>

大小:

编译期固定

而:

std::vector<int>

大小:

运行时动态变化

std::array 本质上更接近固定数组包装。

没有动态扩容。

32. 面试高频:vector 和 deque 区别

vector:

  • 整体连续
  • 尾部插入高效
  • 中间/头部插入较慢
  • 缓存友好

deque:

  • 分段存储
  • 头尾插入都高效
  • 也支持随机访问
  • 整体内存不保证连续

如果需要:

&d[0] + 1 == &d[1]

不要依赖 deque 像 vector 那样整体连续。

33. 面试高频:map 为什么用红黑树,不用普通 BST?

普通 BST 极端情况下:

1
 \
  2
   \
    3
     \
      4

退化成链表:

O(n)

红黑树是自平衡 BST。

保证树高度近似:

O(log n)

所以:

查找 / 插入 / 删除

O(log n)

34. 面试高频:红黑树和 AVL 区别

不用背旋转细节到特别深。

知道:

AVL

→ 平衡更严格

→ 查找性能很好

→ 插入删除调整可能更多

红黑树

→ 平衡要求相对宽松

→ 插删综合性能好

STL map/set 常见实现选择红黑树,就是因为动态增删场景综合性能比较均衡。

35. 面试高频:unordered_set 为什么 key 也不能改?

你之前就碰到过这个问题。

虽然它不像 set 一样靠排序:

unordered_set

hash(key)

bucket

如果直接修改 key:

原来:

hash(10) → bucket 3

你把 10 改成 100

hash(100) → 理论应该在 bucket 8

但是对象还躺在 bucket 3

哈希表结构就被破坏了。

所以 key 也不能随便修改。

36. 如果是面试,优先级怎么排?

我建议你按这个顺序学:

vector

底层

我建议你按这个顺序学:

  • vector
  • 底层
  • size/capacity
  • 扩容
  • reserve/resize
  • 迭代器失效
  • map/set
  • 红黑树
  • O(log n)
  • 有序
  • key 为什么不能修改
  • unordered_map/set
  • 哈希表
  • 哈希冲突
  • bucket
  • 平均 O(1)、最坏 O(n)
  • rehash
  • vector / list / deque 对比
  • 迭代器
  • 分类
  • 失效问题
  • 算法
  • sort
  • lower_bound
  • find
  • remove/erase
  • C++11 STL
  • move
  • emplace_back
  • perfect forwarding
  • 适配器
  • stack
  • queue
  • priority_queue

37. 面试前最少要能回答的 15 道题

如果下面这些你都能自己讲出来,STL 基础面试已经比较稳:

  1. STL 有哪几个主要组成部分?
  2. vector 底层是什么?
  3. vector 为什么随机访问 O(1)?
  4. vector 扩容时发生了什么?
  5. size 和 capacity 区别?
  6. reserve 和 resize 区别?
  7. vector 什么情况下迭代器失效?
  8. vector 和 list 区别?
  9. vector 和 deque 区别?
  10. map/set 底层是什么?为什么 O(log n)?
  11. map 和 unordered_map 怎么选?
  12. unordered_map 为什么平均 O(1),最坏 O(n)?
  13. 哈希冲突是什么?rehash 是什么?
  14. std::sort 大致是什么算法?
  15. push_back 和 emplace_back 有什么区别?

然后再加 5 道进阶:

  1. 为什么 set 的 key 不能改?
  2. 为什么 unordered_set 的 key 也不能改?
  3. remove 为什么不真正 erase?
  4. 为什么 vector 扩容会导致迭代器失效?
  5. 为什么移动构造最好 noexcept?

这 20 道基本就是 STL 面试主干。

最后给你一张脑图式记忆

STL
│
├─ 容器
│   ├─ vector → 动态连续数组
│   ├─ list → 双向链表
│   ├─ deque → 分段连续
│   ├─ map/set → 红黑树
│   └─ unordered_* → 哈希表
│
├─ 迭代器
│   ├─ forward
│   ├─ bidirectional
│   └─ random access
│
├─ 算法
│   ├─ sort
│   ├─ find
│   ├─ lower_bound
│   └─ remove
│
├─ 适配器
│   ├─ stack
│   ├─ queue
│   └─ priority_queue
│
└─ C++11
    ├─ move
    ├─ emplace
    └─ perfect forwarding
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhangdaliangmvp

如果有帮助,请喝瓶水吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值