C++ 容器类 <vector>(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在 C++ 编程中,C++ 容器类 <vector>
的核心特性与使用技巧都至关重要。本文将从基础概念、核心操作、底层原理到实际案例,系统性地解析这一容器类的使用方法与设计逻辑,帮助开发者循序渐进地提升编程能力。
一、基础概念:什么是 vector?
vector 是 C++ 标准库中的一种序列式容器,底层基于动态数组实现。它能够自动管理内存,根据实际需求动态扩展或收缩存储空间。与静态数组相比,vector 的优势在于:
- 动态容量:无需预先指定大小,元素数量可随程序运行实时调整;
- 随机访问:支持通过下标快速访问任意位置的元素;
- 自动内存管理:开发者无需手动分配或释放内存,降低内存泄漏风险。
形象地比喻,vector 就像一个“智能收纳盒”:当你需要存放更多物品时,它会自动扩展空间;当你取出物品后,它会根据需要收缩,避免浪费存储资源。
二、vector 的核心操作与代码示例
1. 初始化与赋值
vector 可以通过多种方式初始化,例如直接声明、列表推导或通过其他容器构造:
// 空初始化
std::vector<int> vec;
// 初始容量指定
std::vector<int> vec(5); // 创建包含 5 个 0 的整型 vector
// 列表初始化
std::vector<int> vec = {1, 2, 3, 4, 5};
// 通过其他容器初始化
std::vector<int> vec2(vec.begin(), vec.end());
2. 元素添加与删除
添加元素
push_back()
:将元素追加到容器末尾,时间复杂度为 O(1)(平均情况)。vec.push_back(6); // 将 6 添加到 vec 末尾
insert()
:在指定位置插入元素或多个元素:vec.insert(vec.begin() + 2, 10); // 在索引 2 处插入 10 vec.insert(vec.end(), {11, 12}); // 在末尾插入多个元素
删除元素
pop_back()
:移除最后一个元素,时间复杂度 O(1):vec.pop_back(); // 移除 vec 的最后一个元素
erase()
:通过迭代器或索引删除指定位置的元素:vec.erase(vec.begin() + 3); // 删除索引 3 处的元素
3. 访问与查询
访问元素
operator[]
:通过下标访问,但无边界检查:int value = vec[2]; // 获取索引 2 的元素
at()
:提供安全访问,超出范围时抛出std::out_of_range
异常:int safe_value = vec.at(2); // 安全访问
容量与状态查询
size_t size = vec.size(); // 返回元素数量
bool is_empty = vec.empty(); // 判断是否为空
size_t capacity = vec.capacity(); // 返回当前分配的内存容量
三、vector 的迭代器与遍历
迭代器(Iterator) 是 C++ 中用于访问容器元素的指针类对象。vector 支持以下迭代器类型:
- 正向迭代器:通过
begin()
和end()
获取,从起始到末尾; - 反向迭代器:通过
rbegin()
和rend()
获取,从末尾到起始。
遍历示例
// 正向遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 范围 for 循环(C++11 起)
for (int num : vec) {
std::cout << num << " ";
}
// 反向遍历
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " ";
}
四、vector 的内存管理与动态扩容机制
vector 的动态扩容机制是其核心特性之一。当元素数量超过当前容量时,vector 会申请更大的内存块,并将原有数据迁移至新地址。这一过程称为“扩容”。
扩容策略
vector 的扩容通常遵循以下规则:
- 初始容量:默认初始容量为 0 或根据构造函数指定;
- 扩容因子:当容量不足时,新容量通常为当前容量的 1.5~2 倍(具体实现可能因编译器而异);
- 内存迁移:扩容会导致所有元素重新拷贝,因此频繁扩容可能影响性能。
示例:观察扩容行为
std::vector<int> vec;
std::cout << "Capacity: " << vec.capacity() << std::endl; // 初始容量可能为 0
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "After " << i+1 << " elements: Capacity = " << vec.capacity() << std::endl;
}
输出可能显示容量随元素数量增长呈指数级增加,例如:0 → 1 → 2 → 4 → 8 → 16...
优化技巧
- 预分配容量:使用
reserve()
减少扩容次数:vec.reserve(100); // 预分配 100 个元素的容量
- 避免不必要的拷贝:使用
emplace_back()
直接在容器内构造对象,而非拷贝:vec.emplace_back(10); // 直接在容器末尾构造元素,效率更高
五、vector 的底层实现解析
内部结构
vector 的底层通常由三个指针组成:
start
:指向容器首元素的指针;finish
:指向容器末尾元素后一个位置的指针;end_of_storage
:指向容器当前分配内存末尾的指针。
通过这三个指针,vector 可以动态管理其容量和大小:
size()
:finish - start
;capacity()
:end_of_storage - start
。
扩容流程图示
当 size()
接近 capacity()
时:
- 计算新容量(通常为原容量的 1.5~2 倍);
- 分配新内存块;
- 将旧数据拷贝到新内存;
- 释放旧内存,更新指针。
六、常见问题与最佳实践
1. 越界访问与迭代器失效
- 越界访问:通过
operator[]
访问超出size()
的索引会导致未定义行为。 - 迭代器失效:在
push_back()
、insert()
或erase()
后,原有迭代器可能失效。
2. 性能优化建议
- 预分配容量:避免频繁扩容,尤其是循环中多次调用
push_back()
; - 避免拷贝 vector:传递或返回 vector 时,优先使用引用或
std::move()
; - 使用
reserve()
和shrink_to_fit()
:根据场景调整内存占用。
七、实际案例:统计单词频率
以下案例演示如何用 vector 实现单词频率统计:
#include <vector>
#include <string>
#include <algorithm>
struct WordCount {
std::string word;
int count = 0;
};
void count_words(const std::vector<std::string>& words) {
std::vector<WordCount> counts;
for (const auto& word : words) {
auto it = std::find_if(counts.begin(), counts.end(),
[&](const WordCount& wc) { return wc.word == word; });
if (it != counts.end()) {
++it->count;
} else {
counts.emplace_back(WordCount{word, 1});
}
}
// 输出结果
}
此案例展示了 vector 在存储自定义结构体、结合算法库(如 std::find_if
)时的灵活性。
八、结论
C++ 容器类 reserve()
、emplace_back()
等优化技巧,并注意迭代器失效等潜在陷阱,以最大化 vector 的性能优势。
掌握 vector 仅仅是 C++ 容器学习的起点,后续可进一步探索 list
、map
等其他容器,逐步构建面向对象编程的完整知识体系。