C++入门篇(十三):vector(上)——动态数组:构造、空间增长与迭代器失效

目录

[0.1 开篇·导读](#0.1 开篇·导读)

[一、什么是 vector](#一、什么是 vector)

二、构造与定义

[2.1 四种构造(速查表格)](#2.1 四种构造(速查表格))

2.2测试代码:

[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 的三个差异点(重点)

  1. vector 没有 +=,尾插一律 push_back
  2. vector 没有成员 find ,查找用库函数 find(v.begin(), v.end(), x),找不到返回 v.end()
  3. 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)

118. 杨辉三角 - 力扣(LeetCode)

题目: 给定 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 静态二维数组的连续存储不同)。

如对此类题感兴趣此处推荐几道(按难度梯度):

  1. 只出现一次的数字(LeetCode 136)------异或秒杀
  2. 只出现一次的数字 II(LeetCode 137)
  3. 数组中出现次数超过一半的数字(LeetCode 169)
  4. 电话号码的字母组合(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,删除必须接返回值,扩容之后迭代器重新赋值。

九、最终总结

  1. vector = 自动扩容的数组(类模板,必须显式指定类型)
  2. 构造:圆括号传个数 (10,1) 是 10 个 1;花括号列元素 {10,1} 是两个元素
  3. 遍历与 string 完全一致;O(1) 随机访问,日常推荐 vi
  4. reserve 只开空间;resize 改 size 还填值;扩容:VS 1.5 倍、g++ 2 倍
  5. vector 没有 +=、没有成员 find、没有流重载------三大差异
  6. 迭代器底层就是指针;扩容/插入/erase 后迭代器失效,重新赋值或用 erase 返回值
  7. 二维 vector:vvij 两次 operator\[\],行内连续、行间不连续
  8. 杨辉三角 = resize 成 1 + 填中间;删除重复项 = it = erase(it)

好了本期内容就到这里。下篇我们把手伸进 vector 的肚子里------三个指针、扩容逻辑、memcpy 陷阱,手撕一个自己的 vector。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。

相关推荐
Jasmine_llq1 小时前
《P17461 [GESP202609 八级] 生成树计数》
算法·栈·tarjan 算法·边双连通分量·深度优先搜索 dfs·tarjan 保存当前路径节点·模乘法
Jasmine_llq1 小时前
《P17462 [GESP202609 八级] 末班车》
算法·dijkstra 算法·最大化版本,求最长可行值·建反向图(逆图 / 反转边)·多源 dijkstra 思想·对每个终点单独跑一遍djst·离线预处理 + 在线查询
江江爱编程1 小时前
java零基础保姆级教学01
java·开发语言
沐晓时光1 小时前
C语言入门,深入理解指针(2)
c语言·开发语言
2601_962885721 小时前
如何用 Python 做均线粘合选股?(多头发散前的变盘候选)
开发语言·python
2601_962071572 小时前
类变量和全局变量的隔离性有什么区别?
java·开发语言·jvm
the3clipse2 小时前
H.265熵编码核心:CABAC自适应二进制算术编码详解——如何将语法元素高效压缩为比特流
人工智能·算法·视频编码·h.265·hevc·cabac·cavlc
海绵宝宝转agent2 小时前
LeetCode100 LRU缓存思路讲解
java·开发语言·缓存
CQU_JIAKE2 小时前
10.4【A】
算法