C++ 中的 std::vector
一、底层结构与内存管理
1. 核心数据结构
- 连续内存块:元素在内存中连续存储,支持指针算术运算,实现 O(1) 随机访问。
- 三指针管理(简化模型):
-
_start
:指向首个元素的地址(begin()
迭代器)。 -
_finish
:指向最后一个有效元素的下一个位置(end()
迭代器),size = _finish - _start
。 -
_end_of_storage
:指向已分配内存的末尾,capacity = _end_of_storage - _start
。
2. 动态扩容机制
- 当
size == capacity
时插入元素会触发扩容:
- 分配新内存:通常扩容至原容量的 2 倍(不同编译器实现可能不同)。
- 迁移数据:将旧元素拷贝/移动到新内存(涉及构造与析构)。
- 释放旧内存:更新三指针指向新空间。
- 均摊时间复杂度:
push_back()
的均摊复杂度为 O(1),因扩容频率随元素增长降低。
3. 迭代器与失效问题
- 迭代器类型:随机访问迭代器(支持
+
、-
、[]
操作)。 - 失效场景:
- 扩容后:所有迭代器、指针、引用失效(地址变更)。
- 中间插入/删除:操作位置之后的迭代器失效。
二、常用函数详解
1. 构造与初始化
方法 | 示例 | 说明 |
默认构造 |
| 创建空 vector |
指定大小和初值 |
| 5 个元素,值均为 10 |
列表初始化 |
| C++11 初始化列表 |
范围构造 |
| 复制另一容器的区间 |
2. 元素访问
函数 | 示例 | 特性 |
|
| 不检查边界,效率高 |
|
| 边界检查,越界抛 |
|
| 访问首/尾元素 |
|
| 返回底层数组指针(C++11) |
3. 容量操作
函数 | 说明 |
| 返回当前元素个数 |
| 返回当前分配的内存容量 |
| 预分配内存,避免多次扩容(如 |
| 释放多余内存(容量降至 |
| 判断容器是否为空 |
4. 修改操作
函数 | 示例 | 时间复杂度 |
|
| 尾部插入,均摊 O(1) |
|
| 直接构造,避免拷贝(更高效) |
|
| 尾部删除,O(1) |
|
| 中间插入,O(n) |
|
| 中间删除,O(n) |
|
| 清空元素(不释放内存) |
|
| 调整元素个数 |
5. 迭代器与遍历
// 1. 下标遍历
for (size_t i = 0; i < v.size(); ++i) {
cout << v[i];
}
// 2. 迭代器遍历
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it;
}
// 3. 范围 for 循环(C++11)
for (int num : v) {
cout << num;
}
三、关键特性与性能优化
- 与数组/链表对比:
- 优势:随机访问 O(1)、缓存友好(连续内存)、自动内存管理。
- 劣势:中间插入/删除效率低(需移动元素)。
- 性能优化技巧:
- 预分配内存:
reserve()
减少扩容开销。 - 使用
emplace_back
:避免临时对象构造(尤其对复杂类型)。 - 避免中间修改:频繁插入/删除时考虑
std::list
或std::deque
。
- 高级用法:
- 二维 vector:
vector<vector<int>> matrix(5, vector<int>(5, 0));
。 - 排序与查找:结合
<algorithm>
中的sort()
、find()
。
四、总结
- 适用场景:需频繁随机访问、尾部操作居多的场景(如数据缓存、矩阵运算)。
- 慎用场景:频繁在中间位置插入/删除数据(链表更优)。
- 设计哲学:以空间换时间,通过动态扩容和连续内存实现高效访问,是现代 C++ 高性能容器的代表。
vector 之键值对
在 C++ 标准库中,std::vector
本身不直接支持键值对(Key-Value)结构,但可以通过组合其他类型(如 std::pair
或自定义结构)间接实现类似功能。以下是详细分析:
1. std::vector
的默认设计
- 本质:
std::vector
是一个动态数组容器,存储单一类型的元素(如int
、string
等)。 - 内存布局:元素在内存中连续存储,支持通过索引(
vec[i]
)高效随机访问(时间复杂度 O(1))。 - 无键值概念:原生不支持通过“键”(Key)查找或管理数据,仅依赖下标索引。
2. 如何实现键值对功能?
虽然 std::vector
不直接支持键值对,但可通过以下方式模拟:
方法一:使用 std::pair
存储键值对
#include <vector>
#include <utility> // for std::pair
std::vector<std::pair<std::string, int>> vec;
vec.push_back(std::make_pair("apple", 10)); // 添加键值对
vec.push_back(std::make_pair("banana", 20));
// 遍历查找(线性搜索,O(n))
for (const auto& kv : vec) {
if (kv.first == "apple") {
std::cout << "Found: " << kv.second << std::endl;
}
}
- 特点:
- 键值对作为整体存储在连续内存中。
- 查找需遍历,效率较低(O(n)),适合少量数据或频繁遍历场景。
方法二:自定义结构体
struct KeyValue {
std::string key;
int value;
};
std::vector<KeyValue> vec;
vec.push_back({"apple", 10});
- 适用场景:需扩展更多字段(如时间戳、状态等)。
3. 与专用键值容器的对比
以下对比 std::vector<std::pair>
与 std::map
/std::unordered_map
:
特性 |
|
|
|
内存连续性 | 连续存储,缓存友好 | 红黑树节点分散 | 哈希表桶分散 |
查找复杂度 | O(n)(无序)/ O(log n)(有序+二分查找) | O(log n)(自动排序) | O(1) 平均(哈希表) |
插入效率 | 尾部插入 O(1),中间插入 O(n) | O(log n)(需调整红黑树) | O(1) 平均(可能触发 rehash) |
内存占用 | ⭐️ 低(仅需存储数据) | ⭐️⭐️ 高(每个节点含指针) | ⭐️⭐️ 高(桶+链表) |
是否自动去重/排序 | 需手动维护 | 按键排序且去重 | 去重,无序 |
4. 适用场景分析
- 推荐用
vector<std::pair>
的情况:
- 数据量小(≤1000),需频繁遍历或按索引访问(如渲染数据列表)。
- 键值对一次性加载,后续仅需遍历(如配置文件读取)。
- 内存敏感场景(嵌入式系统)。
- 推荐用
map
/unordered_map
的情况:
- 需高频按键查找、插入或删除(如缓存、字典)。
- 需自动去重或排序(如词频统计)。
- 数据量较大(>1000)且对查找效率敏感。
5. 注意事项
- 查找效率:
vector
的线性查找效率低,若需高效查找,需先排序再用std::binary_search
(复杂度 O(log n))。 - 插入开销:在
vector
中间插入键值对需移动后续元素,避免频繁操作。 - 线程安全:与所有 STL 容器一样,多线程环境下需手动加锁。
6.总结
std::vector
本身不原生支持键值对,但可通过 std::pair
或自定义结构间接实现。其优势在于内存紧凑和遍历高效,劣势是查找效率低。若需高频按键操作,应优先选择 std::map
(有序)或 std::unordered_map
(无序哈希表)。
简单来说:用索引访问选 vector
,用键名查找选 map
!