C++ Vector:动态数组的高效使用指南
🔥个人主页:胡萝卜3.0
📖个人专栏: 《C语言》、《数据结构》 、《C++干货分享》、LeetCode&牛客代码强化刷题
⭐️人生格言:不试试怎么知道自己行不行
🎥胡萝卜3.0🌸的简介:
目录
一、初识Vector:C++的动态数组容器
二. vector核心接口:必学的几个高频操作
2.1 定义和初始化(构造函数)
2.2 迭代器:遍历 vector 的 "万能工具"
2.3 空间管理:size、capacity 与扩容策略(reserve)
2.4 增删查改:日常开发高频操作
三. vector 实战:OJ 算法题中的高频用
3.1 只出现一次的数字
3.2 杨辉三角
结尾
一、初识Vector:C++的动态数组容器
Vector 本质上是一个能够动态增长的数组,它在保留普通数组随机访问高效这一核心优势的同时,解决了其固定容量、不够灵活的痛点。通过自动管理内存和自动扩容的机制,它让你可以像使用无限延伸的数组一样,无需关心底层内存分配,就能高效、便捷地存储和访问一系列元素。
vector的核心特性
-
动态数组:底层在连续的内存空间中存储元素。
-
随机访问:通过
[]
运算符或at()
方法,可以在 O(1) 时间复杂度内访问任何元素。 -
动态扩容:当插入新元素导致当前容量不足时,vector 会自动申请一块更大的内存(通常是原大小的1.5或2倍),将原有元素拷贝或移动到新内存,并释放旧内存。
-
尾部操作高效:在末尾插入(
push_back
)或删除(pop_back
)元素,时间复杂度为 O(1)(不考虑扩容开销)。 -
中间/头部操作低效:在中间或头部插入或删除元素,需要移动后续的所有元素以保持连续性,时间复杂度为 O(n)。
二. vector核心接口:必学的几个高频操作
- 我们有了前面学习 string 的基础,再来学习 vector 就轻松很多了,而且也不需要记住所有接口,重点掌握 “构造、迭代器、空间管理、增删查改” 四大类核心接口,即可覆盖 90% 以上场景。
vector是一个很奇怪的容器,它的里面没有实现流插入和流提取,所以我们就需要自己书写一个打印的函数(很简单的),话不多说,直接上代码:
- vector.cpp
void print(const vector<int>& v)
{//下标+[]for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}//迭代器/*vector<int>::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}*/////范围for//支持迭代器的都支持范围for/*for (auto e : v){cout << e << " ";}*/
}
2.1 定义和初始化(构造函数)
(constructor)构造函数声明 | 接口说明 |
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x); (重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last); | 使用迭代器进行初始化构造 |
以及C++11中一个比较好用的一个构造:
ok,接下来我们一一来看:
- vector() 无参构造
//无参构造
vector<int> v;
- vector(size_type n, const value_type& val = value_type())构造并初始化n个val
//创建10个数据,10个数据都是1
vector<int> v1(10, 1);
- vector (const vector& x); 拷贝构造
//拷贝构造
vector<int> v1(10, 1);
vector<int> v2(v1);
- vector (InputIterator first, InputIterator last); 使用迭代器进行初始化构造
这里使用迭代器进行初始化,可以传vector类型,也可以传其他对象的迭代器(前提:类型之间可以进行转换)
1、传vector类型
//使用迭代器空间初始化
vector<int> v1(10, 1);
vector<int> v3(v1.begin(), v1.end());
2、传其他类型的对象(stirng类)
//使用迭代器空间初始化
string s("hello world");
vector<int> v3(s.begin(), s.end());
接下来,我们一起来看看C++11中给我们提供的一个特殊构造:
1、用法
vector<int> v4 = { 1,2,3,4,5,6,7,8,9,10 };
//更严格的写法
vector<int> v4({ 1,2,3,4,5,6,7,8,9,10 });
这时候就有uu想问了,为什么可以这样写?它的底层逻辑是啥?
2、底层逻辑
在C++11中,用了 { } ,编译器就会自动认为是initializer_list,我们可以使用 { } 括任意数量的值去初始化vector对象
底层原理:
内部有两个指针,一个指针指向开头,一个指针指向结尾,可以认为是内部开了一块空间把数组存下来了。
嗯?啥意思?有点不太理解
ok,我们来看下面这张图上的内容:
我们把数组传给initializer_list中的参数il,这个参数其实是创建了一个对象,然后这个对象用指针指向这个数组的开始和结尾,内部底层就相当于写了一个范围for,然后这个范围for就遍历这个initializer_list中的参数il构造对象,然后把它给push_back到vector中
2.2 迭代器:遍历 vector 的 "万能工具"
- 迭代器是访问容器元素的通用接口,vector 的迭代器本质是 “封装的指针”,支持遍历、取值、移动等操作。
iterator的使用 | 接口说明 |
begin + end(重点) | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
- begin + end(重点)
begin()是指向数据开头的迭代器,end()是指向最后一个有效数据的下一个位置
- 代码演示:
void testVector3()
{vector<int> v({ 1,2,3,4,5,6,7,8,9,10 });auto it = v.begin();//指向开头数据的迭代器,也就是指向v[0]while (it != v.end())//end()是指向最后一个数据的下一个位置的迭代器{cout << *it << " ";++it;}}
- rbegin + rend
rbegin 和 rend 正好与begin 和end相反。rbegin是指向最后一个数据的迭代器,end是指向开头数据的前一个位置的迭代器。
- 代码演示:
void testVector3()
{vector<int> v({ 1,2,3,4,5,6,7,8,9,10 });auto it2 = v.rbegin();//指向最后一个数据的迭代器,也就是指向v[9]while (it2 != v.rend())//rend()是指向开头数据的上一个位置的迭代器{cout << *it2 << " ";++it2;}
}
2.3 空间管理:size、capacity 与扩容策略(reserve)
- vector 的 “空间” 分为 size(有效元素个数)和 capacity(最大可容纳元素个数,不含结束标志),理解两者区别是避免扩容开销的关键。
核心接口:
容量空间 | 接口说明 |
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize(重点) | 改变vector的size |
reserve (重点) | 改变vector的capacity |
- size
获取有效数据个数
void testVector3()
{vector<int> v({ 1,2,3,4,5,6,7,8,9,10 });size_t size=v.size();//求出有效数据个数cout << size << endl;
}
- capacity
获取空间大小
void testVector3()
{vector<int> v({ 1,2,3,4,5,6,7,8,9,10 });size_t capacity = v.capacity();cout << capacity << endl;
}
- empty
判断是否为空
void testVector4()
{vector<int> v({ 1,2,3,4,5,6,7,8,9,10 });bool ret = v.empty();cout << ret << endl;if (!ret){cout << "非空" << endl;}else{cout << "空的" << endl;}
}
- resize(重点)
调整 size 到 n(缺省用 0 填充)
1、开空间并插入值
void testVector4()
{vector<int> v;//开10个空间,没有传要插入的值,默认插入0v.resize(10);for (auto e : v){cout << e << " ";}vector<int> v1;//开10个空间,插入1v1.resize(10, 1);for (auto e : v1){cout << e << " ";}
}
2、reszie也可以进行缩容操作,如果n<size(),就保留前n个数据
void testVector()
{ vector<int> v2({ 1,2,3,4,5,6,7,8,9,10 });//5<v2.size() 保留前5个v2.resize(5);for (auto e : v2){cout << e << " ";}
}
resize真正的用途是——开空间并插入值,如果对象中有数据,则在后面继续添加(情况很少)!!!
- reserve (重点)
改变capacity
void testVector5()
{vector<int> v({ 1,2,3,4,6 });for (auto e : v){cout << e << " ";}cout << endl;cout << v.capacity() << endl;cout << endl;//现在我想再插入5个数据,但是空间不够了,需要扩容//将空间扩到10v.reserve(10);v.push_back(5);v.push_back(7);v.push_back(8);v.push_back(9);v.push_back(10);for (auto e : v){cout << e << " ";}cout << endl;cout << v.capacity() << endl;
}
关键:vector 扩容策略
vector 扩容时会分配新内存、迁移旧元素、释放旧内存,这个过程耗时较高。不同编译器扩容倍数不同:
- VS :1.5 倍扩容(如 capacity 从 4→6→9→13...);
- G++ :2 倍扩容(如 capacity 从 4→8→16→32...)。
reserve的真正使用场景:提前开空间,减少扩容次数,提高效率
注意:在vector中,reserve是要多少空间就开多少空间,不会多开空间
2.4 增删查改:日常开发高频操作
- vector 的增删查改接口设计简洁,重点关注 “尾插 / 尾删” 的高效性和 “中间插入 / 删除” 的注意事项。
vector增删查改 | 接口说明 |
push_back(重点) | 尾插 |
pop_back (重点) | 尾删 |
insert | 在position之前插入val |
erase | 删除position位置的数据 |
operator[] (重点) | 像数组一样访问 |
当然还有 emplace 和 emplace_back等接口,后面会一一介绍~~~
- push_back(重点)
在尾部插入数据
void testVector6()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto e : v){cout << e << " ";}
}
- pop_back (重点)
删除尾部数据
void testVector6()
{vector<int> v;//尾插v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto e : v){cout << e << " ";}cout << endl;//尾删v.pop_back();v.pop_back();for (auto e : v){cout << e << " ";}
}
执行结果:
vector中没有提供头插和头删的接口,这是因为头插和头删的时间复杂度都是O(n),不建议使用头插和头删,但是可以使用insert和erase
- insert
在任意位置插入数据,insert用迭代器传位置
void testVector6()
{vector<int> v;//尾插v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto e : v){cout << e << " ";}cout << endl;//头插v.insert(v.begin(), 0);//在中间插入v.insert(v.begin()+2, 10);for (auto e : v){cout << e << " ";}
}
执行结果:
- erase
在任意位置删除数据,erase用迭代器传位置
void testVector6()
{vector<int> v;//尾插v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto e : v){cout << e << " ";}cout << endl;//头删v.erase(v.begin());//删除中间位置数据v.erase(v.begin()+2);for (auto e : v){cout << e << " ";}
}
执行结果:
- operator[] (重点)
vector中有operator[] 这个接口,我们就可以像数组一样使用下标+[ ] 的方式修改和遍历vector对象
void testVector7()
{vector<int> v({ 1,2,3,5,6,7,8,9 });//遍历for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;//修改下标为0的数据v[0] = 0;//下标+[]for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;//遍历+修改for (size_t i = 0; i < v.size(); i++){v[i]++;cout << v[i] << " ";}
}
- emplace
emplace==insert
void testVector8()
{vector<int> v;//尾插v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto e : v){cout << e << " ";}cout << endl;//头插v.emplace(v.begin(), 0);//在中间插入v.emplace(v.begin() + 2, 10);for (auto e : v){cout << e << " ";}
}
- emplace_back
emplace_back==push_back
但是他们两个还是有些区别的,我们一起来看一下~
ok,我们先来看一段代码:
为什么上面这段代码无法运行?这是因为AA中不支持流插入
那我们该怎么解决这个问题呢?
解决方法:
- 让AA支持流插入
- 用->
这时候就有UU想问了,为什么可以这样用?
当迭代器指向的数据是这种符合类型的类,数据是公有的,可以使用 -> ,可以认为底层的指针是数组AA的指针
ok,这时候又有UU想说:嗯?不是要说emplace_back吗?怎么说这个,这是因为emplace_back只有在这种情景下,才能说清和push_back的区别。
差异:
ok ,在现阶段中,我们就认为emplace_back和push_back 是一样的就好了~~~
三. vector 实战:OJ 算法题中的高频用法
- vector 是算法刷题的 “主力容器”,以下 2 道经典 OJ 题,覆盖 vector 的核心使用场景。
3.1 只出现一次的数字
- 题目链接:
https://leetcode.cn/problems/single-number/
- 题目描述
- 算法代码
class Solution {
public:int singleNumber(vector<int>& nums) {int tmp=0;for(auto e:nums){tmp^=e;}return tmp;}
};
3.2 杨辉三角
- 题目链接
https://leetcode.cn/problems/pascals-triangle/
- 题目描述
- 算法代码
class Solution {
public:vector<vector<int>> generate(int numRows) {//创建一个vector<vector<int>>对象vector<vector<int>> vv;//行,匿名对象初始化vv.resize(numRows,vector<int>());//列for(size_t i=0;i<numRows;i++){vv[i].resize(i+1,1);}for(size_t i=2;i<vv.size();i++){for(size_t j=1;j<vv[i].size()-1;++j){vv[i][j]=vv[i-1][j-1]+vv[i-1][j];}}return vv;}
};
- 题目解析
要真正的掌握这道算法题,我们就要对vector的底层有一点的了解(后面会介绍的vector的底层),我们vector其实和我们在数据结构中学的顺序表的是差不多的,既然是这样的话,那我们是不是就可以写出vector的私有成员:
class vector<int>
{
private:int* _a;size_t _size;size_t _capacity;
};
那我们是怎么构造出一个二维数组的呢?
- 先用int 实例化出vector<int>的类
- 再用vector<int>作为模板参数去实例化出vector<vector<int>>的类
用vector<vector<int>>实例化出的对象中的数组中的成员类型都是vector<int>,vector<int>实例化出的对象中的数组是一个成员类型都是int的数组。
通过上面的了解,我们就知道这个二维数组是怎么实现的了~~~
ok,那接下来我们来看看是怎么访问数据的:
上图所演示的又是怎么完成的呢?
我们先来看看这个两个 [ ] 运算符重载是啥样的:
1、
2、
对于二维数组的理解还是比较重要的!!!
结尾
ok,写到这里vector的使用基本上就已经结束了,看到这里的小伙伴给自己一个大大的👍~!!!