目录
[0.1 开篇·导读](#0.1 开篇·导读)
[一、什么是 vector](#一、什么是 vector)
[2.1 四种构造(速查表格)](#2.1 四种构造(速查表格))
[2.3 一道小题引入:圆括号还是花括号?](#2.3 一道小题引入:圆括号还是花括号?)
[4.1 size 与 capacity 的区别](#4.1 size 与 capacity 的区别)
[4.2 插曲:vector 到底按几倍扩容?(面试高频)](#4.2 插曲:vector 到底按几倍扩容?(面试高频))
[六、一道错题引入:迭代器失效(vector 的头号大坑)](#六、一道错题引入:迭代器失效(vector 的头号大坑))
[6.1 为什么会失效、](#6.1 为什么会失效、)
[6.2 错题实战:删除 vector 中所有偶数](#6.2 错题实战:删除 vector 中所有偶数)
[七、OJ 实战](#七、OJ 实战)
[7.1 杨辉三角(LeetCode 118)](#7.1 杨辉三角(LeetCode 118))
0.1 开篇·导读
这里是白杨(),string 三篇收官之后,没看过的同学请走传送门:C++入门篇(十二):string(下)------OJ实战与手写模拟实现:浅拷贝、深拷贝、三个swap-CSDN博客STL 第二站------vector。
如果你后面要刷 OJ,vector 大概率是你使用频率第一的容器,没有之一。本篇拆成上下两篇:
- 上篇(本文):认识 vector------它和 string 哪里一样、哪里不一样;构造与定义;空间增长机制(面试高频);增删查改;迭代器失效(vector 的头号大坑);两道 OJ 实战
- 下篇:手撕模拟实现------三个指针的内幕、扩容逻辑、memcpy 陷阱,把"为什么"彻底讲透
所有打印结果均为实测。让我们开始吧。
一、什么是 vector
先说结论:
vector是一个类模板,其本意是矢量,实际上是一个"顺序表"------动态增长的数组。说人话就是:一个能自动扩容的数组。
在 **<vector>**头文件下。学 STL 有三个境界:能用、明理、能扩展。上篇奔着"能用",下篇奔着"明理"。
一张表看清 vector 和 string 的异同(求同存异,你已经会 string 了,这张表能帮你省一半力气):
| 维度 | string | vector |
|---|---|---|
| 本质 | 动态字符数组 | 动态数组(任意类型 T) |
| 类模板? | 是(basic_string 的别名) | 是,必须显式指定类型 |
| 构造 | 多种构造,重点三个 | 同左,多个"迭代器区间构造" |
| 遍历 | operator\[\] / 迭代器 / 范围 for | 完全一样 |
| 容量 | size/capacity/reserve/resize | 完全一样 |
| 尾插 | +=(首选) | push_back(没有 +=) |
| 查找 | find 成员函数 | 无成员 find,用库函数 find |
| insert | 支持下标位置 | 只支持迭代器位置 |
| 流重载 | 有 << 和 >> | 没有,自己写循环 |
| 扩容 | SSO 小字符串优化 | VS 1.5 倍 / g++ 2 倍 |
| 迭代器失效 | 有 | 有,更频繁、更典型 |
查接口的传送门 :https://legacy.cplusplus.com/reference/vector/vector/
二、构造与定义
2.1 四种构造(速查表格)
| 构造方式 | 说明 |
|---|---|
| vector v;(重点) | 无参构造,空容器 |
| vector v(n, val); | n 个 val(val 缺省时用 T 的默认构造,即内置类型给 0) |
| vector v(v2);(重点) | 拷贝构造 |
| vector v(first, last); | 用任意容器的迭代器区间构造 |
2.2测试代码:
cpp
#include<iostream>
#include<vector>
using namespace std;
//代码一:验证几个构造
int main()
{
vector<int> v1; // (1) 无参构造
vector<int> v2(4, 100); // (2) 4个100
vector<int> v3(v2.begin(), v2.end()); // (3) 迭代器区间构造
vector<int> v4(v3); // (4) 拷贝构造
vector<int> v5{ 1, 2, 3, 4, 5 }; // (5) C++11 初始化列表
cout << "v2表内数据:";
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
cout << "v3表内数据:";
for (auto e : v3)
{
cout << e << " ";
}
cout << endl;
cout << "v4表内数据:";
for (auto e : v4)
{
cout << e << " ";
}
cout << endl;
cout << "v5表内数据:";
vector<int>::iterator it = v5.begin();
while (it != v5.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
打印结果(白杨实测,环境:VS x64 Debug)
v2表内数据:100 100 100 100
v3表内数据:100 100 100 100
v4表内数据:100 100 100 100
v5表内数据:1 2 3 4 5

C++11 的初始化列表是怎么实现的? {1,2,3,4,5} 会被转换成一个 std::initializer_list<int>对象,再调用 **vector 专门为它准备的构造函数(**C++11 新增)。string 篇提过一嘴,这里正式见面。
2.3 一道小题引入:圆括号还是花括号?
下面两个 vector 分别装了什么?
cpp
vector<int> a(10, 1); // ?
vector<int> b{ 10, 1 }; // ?
答案:
- a装了 10 个 1(圆括号:n 个 val)
- b装了 10 和 1 两个元素(花括号:初始化列表)
原因与原理 : 这是构造函数重载匹配的问题------圆括号优先匹配"n 个 val"构造,花括号优先匹配 initializer_list 构造。面试高频坑点,务必记住:想传个数用圆括号,想列元素用花括号。
三、遍历三件套
与 string 完全一致,一句话回顾即可(细节传送门:C++入门篇(十):string(上)------认识string:构造与三大遍历(一条龙讲透operator\[\]、迭代器、auto、范围for)-CSDN博客)
cpp
vector<int> v{ 1,2,3,4,5 };
// 1. operator[]
for (size_t i = 0; i < v.size(); ++i)
cout << v[i] << " ";
// 2. 迭代器
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
cout << *it << " ";
// 3. 范围 for(最常用)
for (auto e : v)
cout << e << " ";
三种都输出 1 2 3 4 5。O(1) 随机访问是 vector 的核心优势,所以日常遍历更推荐vi------顺手就是下标。
四、空间增长机制
4.1 size 与 capacity 的区别
先说结论:
size 是"装了几个",capacity 是"底层空间能装几个"。当 size 追上 capacity 时,vector 会自动开辟更大的空间------这就是扩容。
| 接口 | 作用 | 说明 |
|---|---|---|
| size() | 数据个数 | 日常最常用 |
| capacity() | 容量大小 | 观察扩容用 |
| empty() | 是否为空 | 等价 size()==0 |
| resize(n, val)(重点) | 改变 size | 增多会填 val(可能扩容),减少只截断 |
| reserve(n)(重点) | 改变 capacity | 只开空间,不初始化,不影响 size |
cpp
int main()
{
vector<int>v;
cout << "size=" << v.size() << " capacity=" << v.capacity() << endl; // 0 0
v.reserve(100);
cout << "reserve(100)后: size=" << v.size() << " capacity=" << v.capacity() << endl; // 0 100
v.resize(5, 1);
cout << "resize(5,1)后: size=" << v.size() << " capacity=" << v.capacity() << endl; // 5 100
v.resize(2);
cout << "resize(2)后: size=" << v.size() << " capacity=" << v.capacity() << endl; // 2 100
return 0;
}
打印结果(白杨实测,环境:VS x64 Debug)
size=0 capacity=0
reserve(100)后: size=0 capacity=100
resize(5,1)后: size=5 capacity=100
resize(2)后: size=2 capacity=100

一句话记忆:reserve 只租房不装修(只扩容量),resize 租房又装修(改 size 还填值)。
4.2 插曲:vector 到底按几倍扩容?(面试高频)
还是老办法,往 vector 里狂插 100 个元素,每次容量变化打印出来:
cpp
int main()
{
vector<int>v;
size_t sz = v.capacity();
for (size_t i = 0; i < 100; i++)
{
v.push_back(1);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed:" << sz << endl;
}
}
return 0;
}
打印结果(白杨实测,环境:VS x64 Debug)
capacity changed:1
capacity changed:2
capacity changed:3
capacity changed:4
capacity changed:6
capacity changed:9
capacity changed:13
capacity changed:19
capacity changed:28
capacity changed:42
capacity changed:63
capacity changed:94
capacity changed:141

先说结论:
VS(PJ 版本 STL)按 1.5 倍扩容;g++(SGI 版本 STL)按 2 倍扩容(1→2→4→8→16→32→64→128)。
面试考点:**不要固化地认为 vector 扩容就是 2 倍,**具体增长多少是库实现者根据需求定的。VS 选 1.5 倍的理由:倍数太高浪费空间,太低扩容次数多;1.5 倍还能保证扩容后地址复用率更高(细节进阶篇聊)。
reserve 的意义:如果提前知道要存多少元素(比如 10 万个),先 reserve(100000) 把空间一次性开好,避免边插边扩容反复搬数据------这就是 reserve 存在的价值。
五、增删查改
| 接口 | 作用 | 说明 |
|---|---|---|
| push_back(x)(重点) | 尾插 | 注意:vector 没有 += |
| pop_back()(重点) | 尾删 | 无返回值 |
| emplace_back(args)(了解) | 尾插 | 直接在尾部构造,比 push_back 少一次拷贝 |
| find(first, last, x)(重点) | 查找 | 算法模块的库函数,不是成员 ,#include <algorithm> |
| insert(pos, x) | 在迭代器 pos 前插入 | 只支持迭代器位置(与 string 不同) |
| erase(pos) | 删除迭代器 pos 处元素 | 返回下一个元素的迭代器(下节细讲) |
| swap(v2) | 交换两个 vector 的数据空间 | 只换三根指针,O(1) |
| operator\[\](重点) | 像数组一样访问 | 越界不检查,直接崩 |
| at(i) / front() / back() | 访问 | at 越界抛异常;front/back 取首尾 |
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> v{ 1, 2, 3, 4, 5 };
v.push_back(6); // 尾插
v.pop_back(); // 尾删
// 查找:用库函数 find,返回迭代器
auto it = find(v.begin(), v.end(), 3);
if (it != v.end())
cout << "find(3) 在下标 " << (it - v.begin()) << " 处" << endl;
v.insert(v.begin(), 0); // 头部插入 0
v.erase(v.begin() + 1); // 删除下标1的元素
v[0] = 10; // operator[] 支持修改
cout << "at(0)=" << v.at(0) << " front=" << v.front() << " back=" << v.back() << endl;
return 0;
}
打印结果(白杨实测,环境:VS x64 Debug)
find(3) 在下标 2 处
at(0)=10 front=10 back=5

小贴士:
传给 erase 的迭代器超出了 vector 合法范围
简单一句话:你调用
vector.erase(it)的时候,这个迭代器it是v.end()!
如图当v.erase(v,end())时运行截图:
原理:
v.end()是尾后地带器,它不指向容器里的任何元素,是不能拿来erase删除的。
很多同学可能已经遗忘了这一小部分知识这里简单提一下:
v.end() 不是最后一个元素!
end() 是尾后迭代器:站在最后一个元素的后面,相当于容器外面的哨兵,不指向任何真实元素。
举例子,vector:1, 2, 3

- begin()→指向第一个元素
- 指向3的迭代器:v.begin()+2(最后一个真实元素)
- end()→在3的后边,不属于容器内元素,只是标记终点。
所以一句话记忆:
begin 指向第一个元素;end 是末尾的墙外哨兵,不存东西,只用来标记边界。
与 string 的三个差异点(重点)
- vector 没有 +=,尾插一律 push_back
- vector 没有成员 find ,查找用库函数
find(v.begin(), v.end(), x),找不到返回 v.end() - vector 没有流重载 ,
cin >> v会编译报错,输入输出自己写循环
小贴士:vector<char> 能不能代替 string?:
存储层面可以,接口层面不行------没有 c_str()、没有 +=、没有 find 成员。string 的字符串语义是 vector<char> 给不了的,各司其职。
六、一道错题引入:迭代器失效(vector 的头号大坑)
6.1 为什么会失效、
先说结论:
vector 的迭代器底层就是原生指针 it*。当扩容发生时,旧空间被释放、数据搬到新空间,而你手里的迭代器还指着旧空间------它失效了。继续使用 = 访问已释放内存,程序崩溃。
会引起底层空间改变的操作都有失效风险:resize、reserve、insert、assign、push_back。另外 erase 删除元素后,该位置的迭代器也失效**(VS 认为任意位置的 erase 后迭代器都失效)**。
cpp
//迭代器失效
int main()
{
vector<int>v{ 1,2,3,4,5,6,7 };
auto it = v.begin();
v.reserve(100);
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
打印结果(白杨实测,环境:VS x64 Debug)
直接奔溃(迭代器越界)

解决办法一句话:扩容/插入/删除操作之后,如果还想用迭代器,重新赋值即可(it = v.begin())。
6.2 错题实战:删除 vector 中所有偶数
下面代码想删除 v 里所有偶数,运行结果会怎样?
cpp
vector<int> v{ 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it); // it 已经失效
++it; // 对失效迭代器 ++
}
答案:奔溃。如图:

原因:erase(it)之后 it 已失效,再对失效迭代器 ++、解引用,就是在操作一块不该操作的空间。
正确写法:用 erase 的返回值更新迭代器(erase 返回被删元素的下一个位置):
cpp
vector<int> v{ 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it); // 用返回值更新,it 指向下一个有效位置
else
++it;
}
打印结果(白杨实测,环境:VS x64 Debug)
1 3

再次重申该细节 :如果 erase 的是最后一个元素,删完后返回的是 end(),此时 ++it 同样越界。原则:erase 之后,永远用返回值更新迭代器,别再用旧的。
补充一句:string 在插入、扩容、erase 之后迭代器同样会失效,处理方式一模一样------"在使用前重新赋值"。
七、OJ 实战
7.1 杨辉三角(LeetCode 118)
题目: 给定 numRows,生成杨辉三角的前 numRows 行。
思路 : 每行头尾都是 1;中间第 j 个数 = 上一行 j-1 + j。先给每一行 resize 成 i+1 个 1,再填中间------这是 vector<vector<int>> 的经典用法。
cpp
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows); // 先开 numRows 行
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1); // 第 i 行 resize 成 i+1 个 1
}
for (int i = 2; i < numRows; ++i) // 从第 2 行开始填中间
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
};
测试提交:

这里简单提一下该题要点:vvij 本质是两次 operator\[\]:先 vvi 拿到第 i 行的 vector<int>,再 j 拿到元素。二维 vector 是"间接层级模型"------每一行是独立的一段连续空间,行与行之间不连续(与 C 静态二维数组的连续存储不同)。
如对此类题感兴趣此处推荐几道(按难度梯度):
- 只出现一次的数字(LeetCode 136)------异或秒杀
- 只出现一次的数字 II(LeetCode 137)
- 数组中出现次数超过一半的数字(LeetCode 169)
- 电话号码的字母组合(LeetCode 17)------选做,回溯入门
如有问题欢迎到评论区留言,作者看到的话一定会尽可能的为你讲解明白。
八、重点接口速查总表(重点)
| 分类 | 接口 | 一句话说明 |
|---|---|---|
| 构造 | vector v; | 空容器 |
| 构造 | vector v(n, val); | n 个 val |
| 构造 | vector v{...}; | 初始化列表 |
| 容量 | size()/ capacity() / empty() | 个数 / 容量 / 判空 |
| 容量 | reserve(n) | 只开空间不改 size |
| 容量 | resize(n, val) | 改 size,增多填 val |
| 增删 | push_back(x) / pop_back() | 尾插 / 尾删 |
| 查找 | find(v.begin(), v.end(), x) | 库函数,找不到返回 end() |
| 插入 | insert(pos, x) | 迭代器 pos 前插入 |
| 删除 | erase(pos) | 删除并返回下一个位置 |
| 访问 | vi / at(i) / front() / back() | at 越界抛异常 |
| 其他 | swap(v2) | 交换两个 vector(O(1)) |
简单记忆:尾插 push_back,查找用库函数 find,删除必须接返回值,扩容之后迭代器重新赋值。
九、最终总结
- vector = 自动扩容的数组(类模板,必须显式指定类型)
- 构造:圆括号传个数 (10,1) 是 10 个 1;花括号列元素 {10,1} 是两个元素
- 遍历与 string 完全一致;O(1) 随机访问,日常推荐 vi
- reserve 只开空间;resize 改 size 还填值;扩容:VS 1.5 倍、g++ 2 倍
- vector 没有 +=、没有成员 find、没有流重载------三大差异
- 迭代器底层就是指针;扩容/插入/erase 后迭代器失效,重新赋值或用 erase 返回值
- 二维 vector:vvij 两次 operator\[\],行内连续、行间不连续
- 杨辉三角 = resize 成 1 + 填中间;删除重复项 = it = erase(it)
好了本期内容就到这里。下篇我们把手伸进 vector 的肚子里------三个指针、扩容逻辑、memcpy 陷阱,手撕一个自己的 vector。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。