C++ 容器学习系列|vector 核心知识全解析,铺垫下一期模拟实现
很多同学在初学 C++ 容器时,都会有这样的困惑:明明会用 vector 的 push_back、pop_back,但一遇到 “扩容机制”“迭代器失效” 这类问题就发懵;更别说后续要自己模拟实现时,连从哪里下手都不知道。别急,咱们的学习节奏会帮你解决这个问题 —— 上一期我们搞定了 string 类的模拟实现,掌握了 “类与容器设计” 的基本思路;这一期,我们先把 vector 的核心知识拆解开、讲透彻:从它的模板特性、内存管理,到接口使用的注意事项,每一个点都结合实际场景带你理解。等你真正 “吃透” 了 vector 是什么、怎么用,下一期再动手模拟实现时,自然会水到渠成。现在,就让我们正式进入 vector 的学习吧!
目录
1.vector的介绍及使用
1.1vector的介绍
序列式容器
关联式容器
1.2vector的使用
vector的定义
vector的无参构造
构造并初始化n个val
vector的拷贝构造
使用迭代器进行初始化构造
综合示例与常见问题
vector iterator的使用(迭代器的使用)
正向迭代器(Forward Iterator)
反向迭代器(Reverse Iterator)
两种迭代器的对比
编辑
注意事项
vector空间增长问题
size()
capacity()
empty()
resize()
reserve()
关键区别总结
vector增删查改
push_back
pop_back
find
insert
erase
swap
operator[]
通用易错点
结语
1.vector的介绍及使用
1.1vector的介绍
对于STL库的学习我们从前面的string就知道,接触一个新容器第一件事就是去官网上查文档,这是快速了解一个容器最快最好的方法,废话不多说,让我们一起来看看今天的干货吧。
这里给大家总结一下,这里说的大致就是以下几点
- 基本性质:
vector
是序列容器,类似数组但大小可动态变化,元素存储连续,能像数组一样通过指针偏移高效访问元素,且存储由容器自动管理。- 内部实现:内部用动态分配的数组存元素,新增元素时可能需重新分配数组(分配新数组并移动元素,此操作耗时),所以不会每次添加元素都重新分配,会预分配额外存储空间应对可能的增长,实际容量可能大于元素所需的大小(即尺寸)。库可采用不同增长策略平衡内存使用与重新分配,且重新分配按尺寸对数级增长间隔进行,使在末尾插入元素的均摊时间复杂度为常数(即时间复杂度为o(1))。
- 与数组对比:
vector
虽比数组更耗内存,但能高效管理存储并动态增长。- 与其他动态序列容器对比:
vector
访问元素(同数组)和在末尾增删元素效率较高,但在非末尾位置增删元素时表现不如deque
、list
、forward_list
,且迭代器和引用的一致性也不如list
和forward_list
。
这里可能有小伙伴好奇什么是序列容器,这里给大家扩展一下,我们将会接触到两种容器,一种是序列式容器一种是关联式容器
序列式容器
序列式容器按照线性顺序存储元素,元素的位置与插入顺序直接相关。常见类型包括:
vector:动态数组,支持快速随机访问,尾部插入/删除高效。
list:双向链表,任意位置插入/删除高效,但不支持随机访问。
deque:双端队列,头尾插入/删除高效,支持随机访问但性能略低于vector。
关联式容器
关联式容器通过键(key)存储元素,元素顺序由键的比较规则决定,与插入顺序无关。常见类型包括:
set/multiset:基于红黑树,键唯一(set)或可重复(multiset),自动排序。
map/multimap:存储键值对,键唯一(map)或可重复(multimap),基于红黑树有序存储。
**unordered_**前缀容器(如unordered_set):基于哈希表实现,键无序但查询效率更高。
这里一句话总结一下两种容器的本质区别:
序列式容器按元素插入顺序线性存储,可通过顺序遍历或索引访问;关联式容器基于树或哈希表,依键的逻辑关系(或哈希规则)组织,主要通过键高效查找,不按插入顺序存储。
这里我们只需要知道对于序列式容器更改内部元素的顺序不会改变容器的结构,但是对于关联式容器如果随意改变内部元素的顺序,会导致容器结构的损坏
简单来说vector底层是动态数组,它支持增删查改和遍历操作,它的尾插效率高(时间复杂度为o(1)),但是其他地方的插入成本较高,不如list,deque等其他序列容器,它相较普通数组更方便管理。
在了解完vector后下面让我们一起来看看vector的使用吧
1.2vector的使用
在了解完vector之后下面让我们来看看vector如何使用,vector作为我们接触的第二个容器,它在实际应用中也是经常出现,可谓是高人气容器,下面我们开始看看vector常用的接口吧
vector的定义
我们主要掌握一下几个vector常用的构造函数
vector的无参构造
无参构造会创建一个空的vector,不分配任何内存空间。适用于后续通过push_back
或resize
等操作动态添加元素。
注意点
初始容量为0,首次添加元素时会触发内存分配。
适合不确定初始元素数量或需要延迟初始化的场景。
代码示例
#include <vector>
std::vector<int> v1; // 无参构造
构造并初始化n个val
通过指定元素个数和初始值构造vector,所有元素会被初始化为相同的值。
注意点
如果元素类型是类对象,需确保其支持拷贝构造。
避免对大型对象使用此方式,可能引发性能问题。
这里如果不给第二个参数在vs下默认是以0补充,不同编译器具体的规则不同,感兴趣的小伙伴可以自己去了解一下
代码示例
std::vector<int> v2(5, 42); // 包含5个42
std::vector<std::string> v3(3, "hello"); // 包含3个"hello"
vector的拷贝构造
通过另一个vector的副本构造新vector,新vector与原vector独立,本质上是将被拷贝的vector内的元素插入到新的vector中
注意点
深拷贝操作,时间和空间复杂度为O(n)。
修改原vector不会影响新vector。
代码示例
std::vector<int> origin = {1, 2, 3};
std::vector<int> copy(origin); // 拷贝构造
使用迭代器进行初始化构造
通过迭代器范围构造vector,支持从其他容器(如数组、list等)初始化。
注意点
迭代器类型需匹配,且范围是左闭右开[begin, end)
。
可用于截取部分数据或转换容器类型。
代码示例
int arr[] = {10, 20, 30};
std::vector<int> v4(arr, arr + 3); // 数组迭代器std::list<int> lst = {7, 8, 9};
std::vector<int> v5(lst.begin(), lst.end()); // list迭代器
综合示例与常见问题
错误示例
std::vector<int> v6(5); // 注意:这是初始化5个0,而非空vector
std::vector<int> v7{5}; // 注意:这是列表初始化,包含单个元素5
推荐实践
明确区分(n, val)
和{val1, val2}
的语法差异。
迭代器构造时确保范围有效性,避免越界。
vector iterator的使用(迭代器的使用)
上面我们介绍了vector的构造,那么下面我们来看看vector如何遍历吧,我们主要学习两种迭代器,一种是正向迭代器另一种是反向迭代器。我们前面学了范围for,所以vector支持迭代器遍历就说明vector也支持范围for遍历。
正向迭代器(Forward Iterator)
正向迭代器用于从容器的开始到结束顺序遍历元素,支持++
操作符移动至下一个元素,常用于std::vector
、std::list
等容器。
代码示例:
#include <iostream>
#include <vector>int main() {std::vector<int> vec = {1, 2, 3, 4, 5};// 使用正向迭代器遍历for (auto it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << " ";}// 输出:1 2 3 4 5return 0;
}
关键点:
begin()
返回指向第一个元素的迭代器。
end()
返回指向末尾(最后一个元素之后)的迭代器。
支持*it
解引用访问元素值。
反向迭代器(Reverse Iterator)
反向迭代器从容器的末尾向开始方向遍历,支持++
操作符(实际移动到前一个元素),需通过rbegin()
和rend()
获取。
代码示例:
#include <iostream>
#include <vector>int main() {std::vector<int> vec = {1, 2, 3, 4, 5};// 使用反向迭代器遍历for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {std::cout << *rit << " ";}// 输出:5 4 3 2 1return 0;
}
关键点:
rbegin()
返回指向最后一个元素的迭代器。
rend()
返回指向起始位置(第一个元素之前)的迭代器。
++rit
实际移动到前一个元素。
两种迭代器的对比
正向迭代器:
auto it = vec.begin(); // 指向1
++it; // 指向2
反向迭代器:
auto rit = vec.rbegin(); // 指向5
++rit; // 指向4
应用场景:
正向迭代器:默认遍历、插入或修改元素。
反向迭代器:逆序输出、检查对称性(如回文)。
注意事项
反向迭代器的base()
方法可转换为正向迭代器,但需注意位置偏移。
在修改容器(如插入/删除)后,迭代器可能失效,需重新获取。
通过合理选择迭代器类型,可以高效灵活地操作容器元素,这里还需要注意迭代器区间默认是左闭右开,反向迭代器(右闭左开),所以像这样的操作就是有问题的,大家可以了解一下,防止后续用vector做题时出现类似的问题
auto it = v1.end();
*it = 5; //这里就越界访问了
auto it1 = v1.rbegin();
*it1 = 4; //这里同理
vector空间增长问题
size()
作用:返回当前vector中实际存储的元素数量。
注意点:
时间复杂度为O(1)。
初始化和空vector的size()
返回0。
代码示例:
#include <vector>
#include <iostream>int main() {std::vector<int> vec = {1, 2, 3};std::cout << "Size: " << vec.size() << std::endl; // 输出3return 0;
}
capacity()
作用:返回vector当前分配的内存空间能容纳的元素总数(无需重新分配)。
注意点:
capacity() >= size()
始终成立。
动态扩容时,不同编译器的增长策略可能不同(如2倍(Linux下)或1.5倍(vs下))感兴趣的小伙伴可以看这篇文章里面有介绍2倍扩容策略。
https://blog.csdn.net/2402_88639790/article/details/151922743?fromshare=blogdetail&sharetype=blogdetail&sharerId=151922743&sharerefer=PC&sharesource=2402_88639790&sharefrom=from_link
代码示例:
std::vector<int> vec;
vec.reserve(100);
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出100
empty()
作用:检查vector是否为空(无元素)。
注意点:
等效于size() == 0
,但语义更清晰。
代码示例:
std::vector<int> vec;
if (vec.empty()) {std::cout << "Vector is empty" << std::endl;
}
resize()
作用:调整vector的size
,可扩容或截断元素。
注意点:
若新大小大于当前size()
,新增元素默认初始化(或通过参数指定值)。
若新大小小于当前size()
,多余元素被销毁。
可能触发重新分配内存(仅当new_size > capacity()
时)。
代码示例:
std::vector<int> vec = {1, 2, 3};
vec.resize(5); // 扩容至5,新增元素为0
vec.resize(2); // 截断,仅保留前两个元素
reserve()
作用:预分配内存空间以避免多次扩容。
注意点:
仅影响capacity()
,不改变size()
。
若参数小于当前capacity()
,调用无效。
频繁插入大量数据时建议使用一般插入接口中,当size>= capacity时会调用此函数进行空间分配。
代码示例:
std::vector<int> vec;
vec.reserve(1000); // 预分配1000个元素的空间
for (int i = 0; i < 1000; ++i) {vec.push_back(i); // 避免多次扩容
}
关键区别总结
size()
vs capacity()
:实际元素数量 vs 预分配内存容量。
resize()
vs reserve()
:修改元素数量 vs 仅修改内存分配。
empty()
:快速判空,优先于size() == 0
。
vector增删查改
push_back
在vector末尾添加一个元素,时间复杂度为O(1)(均摊)。如果容量不足会触发重新分配内存。
std::vector<int> vec;
vec.push_back(1); // vec: [1]
vec.push_back(2); // vec: [1, 2]// 易错点:迭代器失效
auto it = vec.begin();
vec.push_back(3); // it可能失效
pop_back
删除vector末尾的元素,时间复杂度为O(1)。如果vector为空则行为未定义。
std::vector<int> vec{1, 2, 3};
vec.pop_back(); // vec: [1, 2]// 易错点:空vector调用
std::vector<int> empty_vec;
empty_vec.pop_back(); // UB
find
vector本身没有find方法,需要使用算法库中的std::find,时间复杂度O(n)。
std::vector<int> vec{1, 2, 3, 4};
auto it = std::find(vec.begin(), vec.end(), 3);// 易错点:未检查返回值
if (it != vec.end()) {std::cout << *it; // 输出3
}
insert
在指定位置插入元素,时间复杂度O(n)。可能引起迭代器失效。
std::vector<int> vec{1, 3};
vec.insert(vec.begin() + 1, 2); // vec: [1, 2, 3]// 易错点:错误的位置
vec.insert(vec.end() + 1, 4); // UB
erase
删除指定位置的元素或范围,时间复杂度O(n)。返回下一个有效迭代器。
std::vector<int> vec{1, 2, 3, 4};
vec.erase(vec.begin() + 1); // vec: [1, 3, 4]// 易错点:遍历时删除
for (auto it = vec.begin(); it != vec.end(); ) {if (*it % 2 == 0) {it = vec.erase(it);} else {++it;}
}
swap
交换两个vector的内容,时间复杂度O(1)。不会导致迭代器失效。
std::vector<int> vec1{1, 2};
std::vector<int> vec2{3, 4, 5};
vec1.swap(vec2); // vec1: [3,4,5], vec2: [1,2]// 易错点:认为swap代价高
// 实际上只交换内部指针,非常高效
operator[]
通过下标访问元素,不检查边界,时间复杂度O(1)。
std::vector<int> vec{1, 2, 3};
int x = vec[1]; // x=2
vec[2] = 4; // vec: [1,2,4]// 易错点:越界访问
int y = vec[3]; // UB
通用易错点
- 在循环中修改vector(插入/删除)导致迭代器失效(下期介绍)
- 未预分配空间导致频繁重新分配内存
- 混淆size和capacity概念
- 使用[]访问时未确保下标有效
- 在多线程环境下未同步访问
结语
到这里,我们本期关于 vector 容器的介绍就接近尾声了。在这一期内容里,我们一同深入探索了 vector 容器的特性与用法 —— 从它作为动态数组的本质出发,了解了其支持随机访问的高效优势,也学习了初始化、元素增删改查等核心操作,更通过具体示例体会了它在实际编程场景中,比如数据存储、算法实现等方面的便捷应用。相信大家现在对 vector 容器已经有了较为清晰的认知,也能在后续的代码编写中,合理运用它来提升开发效率。
不过,vector 的知识体系并未就此结束。在实际使用 vector 的过程中,你是否遇到过这样的困惑:明明之前能正常使用的迭代器,在执行某些操作后却突然 “失灵”?这背后就涉及到我们下一期将要重点探讨的迭代器失效问题。除此之外,仅仅掌握用法还不够,深入理解底层原理才能让我们对 vector 的运用更加得心应手。所以下一期,我们还会带大家动手进行vector 的底层模拟实现,从内存管理、扩容机制等核心环节入手,揭开 vector 高效运作的 “神秘面纱”。
期待下一期与大家再次相遇,一同攻克迭代器失效的难题,亲手搭建 vector 的底层架构,让我们对 C++ 容器的理解再上一个台阶!