前言
本文将带你穿越C++容器的迷雾森林:从vector动态扩容的数学玄机,到emplace_back比push_back快在哪的微观真相;从红黑树与哈希表的世纪对决,到连续存储背后的内存博弈。无论你是渴望通过大厂面试的技术追梦人,还是致力于优化千万级代码的性能极客,这里都有你意想不到的硬核干货。
目录
1. STL容器了解那些
简要回答
详细回答
STL的特点如下
知识扩展:
2. vector和array的使用场景分别是什么?
2.1. 基本介绍
2.2. std :: vector 使用场景
2.3. std :: array使用场景
2.4. 对比传统数组的优势
3. 描述一下vector是如何保证数组连续存储的吗?
4. 当vector的空间不足以容纳更多的元素时,它是如何扩容的?
5. vector的push_back和emplace_back有什么区别?
简要回答
详细回答
知识扩展:
1. STL容器了解那些
简要回答
C++STL中主要有三类容器:
- 顺序容器(Sequence Containers):如vector、list、deque、array、forward_list
- 关联容器(Associative Containers):如set、map、multiset、multimap
- 无序容器(Unordered Containers)哈希容器:如unordered_map、unordered_set、unordered_multimap、unordered_multiset
此外,STL还包括配套的迭代器、算法、仿函数、适配器等组件。
详细回答
STL容器可以分为三大类:
- 序列容器:维护元素的线性序列
- vector(动态数组,支持快速随机访问)
- deque(双端队列,两端高效插入和删除)
- forward_list(单向链表,任意位置高效插入和删除)
- array(固定大小数组,更安全的原生数组替代)
- 关联容器:基于键值对的有序存储
- set/multiset(有序唯一/非唯一键集合)
- map/multimap(有序键值对集合,键唯一/非唯一)
- 无序关联容器:基于哈希表的存储
- unordered_set/unordered_multiset
- unordered_map/unordered_multimap
每种容器在时间复杂度、内存布局和使用场景下有所不同。
STL的特点如下
序列容器
容器 特点 vector动态数组,随机访问快,尾部插入快 list双向链表,任意位置插入/删除快 deque双端队列,头尾都能快速插入/删除 array固定大小数组(C++11) forward_list单向链表(C++11) 关联容器(有序,就红黑树)
容器 特点 set唯一元素自动排序 multiset元素允许重复 map键值对,键唯一 multimap键值对,键可重复 无序关联容器(基于哈希表,C++11)
容器 特点 unordered_set无序唯一集合 unordered_map无序键值对 unordered_multiset无序可重复集合 unordered_multimap无序可重复键值对 代码实例:
#include <iostream> #include <vector> #include <map> #include <set> #include <unordered_map> #include <list> using namespace std; int main() { vector<int> vec = {1, 2, 3}; vec.push_back(4); // 动态数组 list<int> lst = {10, 20, 30}; lst.push_front(5); // 双向链表 set<int> s = {5, 2, 3, 2}; // 自动去重排序 map<string, int> m = {{"Tom", 90}, {"Jerry", 85}}; // 字典 unordered_map<string, int> um; um["apple"] = 3; um["banana"] = 5; // 哈希表 return 0; }
知识扩展:
STL的底层结构:
vector、deque 动态数组 list、forward_list 链表 set、map 红黑树 unordered_* 哈希表 性能差异:
- vector 随机访问快,但是插入慢(插入过程中需要移动元素)
- list 插入快,但是不支持随机访问
- map/set 查找、插入都是O(logN),但是是有序的
- unordered_map 查找、插入都是O(1)的平均复杂度
使用场景:
- vector(需要随机访问,动态扩容的情况,如图像处理、线性缓存)
- list(频繁插入删除数据的场景,如编辑器撤回栈)
- map/unordered_map:查找、计数、字典、配置解析,如记录访问次数
- set(需要去重、唯一性判断,如用户ID集合)
- deque(实现滑动窗口算法,双端队列缓存)
2. vector和array的使用场景分别是什么?
2.1. 基本介绍
特性 std::vectorstd::array(C++11 引入)大小 动态可变 固定不变(编译时确定) 内存分配 堆(动态分配) 栈(静态分配) 标准库支持 C++98 起就有 C++11 起引入 元素访问 支持下标访问和迭代器 同样支持 效率 稍慢(涉及动态分配) 更快(在栈上分配,无需额外开销)
2.2. std :: vector 使用场景
适用于元素数量运行时变化、在末尾频繁插入删除、需要自动扩容的场景:
- 动态读取用户输入或文件内容
- 使用STL算法是需要可变容器(如排序)
- 实现堆栈、队列、图等动态结构
#include <iostream> #include <vector> int main() { std::vector<int> nums; for (int i = 0; i < 10; ++i) { nums.push_back(i); // 动态扩容 } for (int n : nums) { std::cout << n << " "; } return 0; }
2.3. std :: array使用场景
适用于元素数量固定、性能要求高、无需动态扩容的场景:
- 编译期已知数组大小
- 算法中需要更快的访问速度
- 嵌入式或高性能场景下使用
#include <iostream> #include <array> int main() { std::array<int, 5> nums = {1, 2, 3, 4, 5}; for (int n : nums) { std::cout << n << " "; } return 0; }
2.4. 对比传统数组的优势
- 支持STL算法、迭代器
- 类型安全、避免退化成指针
3. 描述一下vector是如何保证数组连续存储的吗?
vector内部使用动态数组作为底层存储结构,内部使用了一个单一的内存块来存储所有的元素,并且管理这个内存块的大小和容量。当vector的内存容量不够时,会重新分配一块更大的内存,并把旧数组中的数据复制到新内存块中。
4. 当vector的空间不足以容纳更多的元素时,它是如何扩容的?
vector在空间不足时,会进行扩容。vector内部实现过了一个内存分配函数,内存不够时会申请一块原内存1.5~2倍大小的新内存块,再把旧的数组复制到新的内存当中。
vector扩容时,对于使用移动语义复制对象还是直接拷贝对象,取决于对象是否支持移动语义,实现了移动构造函数
5. vector的push_back和emplace_back有什么区别?
简要回答
两者都用于在末尾添加元素,但是两者添加的元素的方式不同:
- push_back:创建一个元素的副本或移动该元素,然后将其添加到末尾
- emplace_back:在向量的末尾就地构造元素。避免了额外的复制或者移动
详细回答
- push_back:
- push_back方法接收一个参数,该参数可以是一个值、一个已经存在的对象的引用、或者要添加进向量末尾的元素的右值
- 如果这个参数是一个右值,push_back将使用移动构造函数(如果可用的话)来移动它;如果参数是一个左值,它将使用拷贝构造函数来复制
- emplace_back:
- emplace_back不接受任何直接的参数,但是接收构造新元素所需要的参数,并使用这些参数在向量的末尾就地构造一个新的元素
- emplace_back避免了临时对象的创建与销毁,因为它直接在向量的内存上构造新对象
知识扩展:
性能:
- emplace_back通常比push_back更加高效,因为它省去了临时对象的创建和销毁的开销,特别是构造和析构成本较高时
- 当需要传递多个参数来构造向量末尾的元素时,emplace_back时,emplace_back是一个很好的选择,因为它允许你直接传递这些参数该构造函数
- 完美转发:emplace_back能够完美转发参数,这意味着参数的值类别(左值、右值)会被保留,这对于需要区分左值和右值的构造函数特别有用