C++ STL八股文
Vector
vector的底层实现⭐
C++ 标准库中的 std::vector
是一个动态数组,底层通过连续内存块实现,支持快速随机访问和动态扩容。其核心实现细节如下:
1. 底层数据结构
vector
内部维护三个指针(或等效的成员变量)来管理内存:
_start
:指向数组的第一个元素(begin()
)。_finish
:指向最后一个元素的下一个位置(end()
)。_end_of_storage
:指向内存块的末尾(表示当前分配的容量上限)。
1 | template <class T> |
2. 内存分配与扩容
初始化
- 默认构造时,
vector
为空,三个指针均为nullptr
。 - 添加元素时,若当前容量不足,触发动态扩容。
扩容机制
- 当
size() == capacity()
(即_finish == _end_of_storage
)时,扩容:- 分配新内存块,容量通常是原容量的 2倍 或 1.5倍(标准未规定,但通用实现如此)。
- 将旧元素移动或拷贝到新内存(C++11 后优先用移动语义)。
- 释放旧内存,更新指针。
扩容复杂度
- 均摊时间复杂度为 O(1)(例如,每次扩容为 2 倍时,均摊分析后插入单个元素的开销为常数)。
3. 关键操作实现
push_back
1 | void push_back(const T& value) { |
随机访问
1 | T& operator[](size_t index) { |
size()
和 capacity()
1 | size_t size() const { |
4. 迭代器失效问题
- 扩容操作会导致所有迭代器、指针、引用失效(内存地址改变)。
- 插入/删除元素可能导致后续元素位置变动,具体是否失效取决于操作位置和容器状态。
总结
- 动态数组:连续内存,支持 O(1) 随机访问。
- 自动扩容:容量按几何级数增长,均摊 O(1) 时间复杂度。
- 内存管理:通过指针跟踪元素范围,
reserve()
可优化性能。 - 迭代器安全:扩容会导致迭代器失效,需谨慎操作。
STL中使用vector要注意什么⭐
在使用C++ STL中的 vector
时,需要注意以下关键点:
1. 扩容机制
- 预分配内存(
reserve()
):当预先知道元素数量时,使用reserve(n)
提前分配足够内存,避免多次扩容(如push_back
导致反复重新分配)。 - 大小(
size
)等于容量(capacity
)时进行扩容:扩容策略通常是倍增(如2倍),频繁扩容可能导致性能损失。
2. 元素访问与边界安全
- 安全访问(
at()
vsoperator[]
):at(index)
:进行边界检查,越界时抛出std::out_of_range
异常。operator[]
:不检查边界,越界访问导致未定义行为。
3. 迭代器失效问题
- 操作导致迭代器失效的场景:
- 插入元素:若引发扩容(
size > capacity
),所有迭代器、指针、引用失效。 - 删除元素:被删元素之后的迭代器失效。
reserve()
/resize()
:可能引发重新分配,导致所有迭代器失效。
- 插入元素:若引发扩容(
4. 高效插入与构造
- 使用
emplace_back
替代push_back
:emplace_back
直接在容器内构造元素,避免临时对象的拷贝/移动(C++11起支持)。
5. 对象生命周期与资源管理
-
存储原始指针:需手动释放内存,避免内存泄漏。
-
推荐使用智能指针:
使用std::unique_ptr
或std::shared_ptr
自动管理资源。1
2std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(42)); // 自动释放
6. 类型要求与移动语义
-
元素类型的要求:
- 可拷贝构造(若使用
push_back
拷贝)。 - 可移动构造(若使用移动语义或
emplace_back
)。
- 可拷贝构造(若使用
-
利用移动语义:
对于大型对象(如std::string
),使用std::move
避免拷贝。1
2
3std::vector<std::string> vec;
std::string str = "Hello";
vec.push_back(std::move(str)); // 移动而非拷贝
vector的扩容机制⭐
vector 的扩容机制是其动态数组特性的核心实现,目的是在元素数量超过当前容量时自动扩展内存空间。
- 扩容触发条件
当向 vector 中添加元素(如 push_back、emplace_back 或 insert)时,若当前元素数量(size())已达到容量(capacity()),则会触发扩容: - 扩容步骤
- 分配新内存:分配一块更大的连续内存(通常是当前容量的 1.5 或 2 倍,取决于编译器实现)。
- 迁移元素:将旧内存中的元素拷贝或移动到新内存。C++11 后,若元素类型支持移动语义(如 std::string、std::unique_ptr),优先使用移动而非拷贝。
- 释放旧内存:销毁旧元素并释放原内存。
- 扩容策略
- 倍数增长:每次扩容将容量乘以固定倍数(如 1.5 或 2),时间复杂度均摊为 O(1)。
- MSVC:按 1.5 倍扩容。GCC/Clang:通常按 2 倍扩容。
- 优势:倍数增长在时间(减少扩容次数)和空间(避免过度浪费)之间取得平衡。
- 时间复杂度
单次扩容:O(n)(需要拷贝/移动 n 个元素)。
均摊分析:多次插入操作的均摊时间复杂度为 O(1)。
例如:从空 vector 插入 n 个元素,总扩容次数为 O(log n),总操作次数为 O(n)。 - 手动优化扩容
预分配内存:使用 reserve() 提前分配足够容量,避免多次扩容。
vector和list的底层实现及区别
底层实现
- vector的底层实现是动态数组。它使用一块连续的内存空间来存储元素。当需要增加容量时,vector会分配一块更大的内存(通常是当前容量的两倍),然后将原有数据复制到新内存中,并释放旧内存。这种实现方式使得vector支持快速随机访问,即可以通过索引直接访问任意元素。
- list的底层实现是双向链表。它由一系列节点组成,每个节点包含数据和指向前后节点的指针。由于节点在内存中不一定是连续的,每次插入或删除操作只需调整相关指针即可。这种实现方式使得list支持快速插入和删除操作,但不支持随机访问。
主要区别
- 存储结构:vector是顺序存储,底层是数组;list是链式存储,底层是双向链表。
- 内存分配:vector一次性分配较大的内存,不够时进行扩容;list每次插入新节点都会进行内存申请。
- 访问性能:vector支持快速随机访问,时间复杂度为O(1);list不支持随机访问,需要遍历,时间复杂度为O(n)。
- 插入删除性能:vector插入和删除数据需要移动现有数据,时间复杂度为O(n);list插入和删除数据只需调整指针,时间复杂度为O(1)。
使用场景
- vector适用于需要频繁随机访问元素的场景,如动态数组、栈等。
- list适用于需要频繁插入和删除元素的场景,如队列、链表等。
map和unordered_map的区别⭐⭐
map
和 unordered_map
都是 C++ 标准库中用于存储键值对的容器,它们在功能上有相似之处,但在实现原理、性能特点和使用场景等方面存在明显区别。
1. 实现原理
- map:
map
基于红黑树(一种平衡二叉搜索树)实现,所有元素按照键(Key)的严格升序排列。插入元素时会自动维护树的平衡,确保查找效率稳定。 - unordered_map:
unordered_map
基于哈希表实现。哈希表使用哈希函数将键映射到一个哈希桶中,通过哈希函数计算出的哈希值来快速定位元素。当发生哈希冲突(即不同的键映射到同一个哈希桶)时,通常会使用开放定址法和链表法等来解决冲突。
2. 元素顺序
- map:元素会按照键的大小进行排序,默认是按照键的升序排列。因此,当你遍历
map
时,会按照键的有序顺序依次访问元素。 - unordered_map:元素是无序的,其存储顺序取决于哈希函数和哈希冲突的处理方式,与键的大小无关。遍历
unordered_map
时,元素的访问顺序是不确定的。
3. 时间复杂度
- map:由于基于红黑树,查找、插入和删除操作的平均时间复杂度都是 。
- unordered_map:在理想情况下,基于哈希表的
unordered_map
查找、插入和删除操作的平均时间复杂度为 。然而,在最坏情况下(如哈希冲突严重),时间复杂度可能会退化为 。
4. 内存占用
- map:红黑树的每个节点除了存储键值对之外,还需要额外的指针和红/黑性质来维护树的结构,因此内存占用相对较高。
- unordered_map:哈希表只需要维护一个哈希桶数组,内存开销通常较小,但需要额外的空间处理哈希冲突。
5. 使用场景
- map:当你需要元素按照键的顺序进行排序,或者需要频繁进行范围查找(如查找某个区间内的所有元素)时,应该选择
map
。例如,在实现一个字典,需要按照字母顺序遍历单词时,map
是一个不错的选择。 - unordered_map:当你只关心元素的插入、查找和删除操作的速度,而不关心元素的顺序时,
unordered_map
是更好的选择。例如,在实现一个缓存系统,需要快速查找某个键对应的值时,unordered_map
可以提供更高效的性能。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 一只大笨熊!
评论