C++ 详细讲解vector类
目录
1. 什么是vector?
2. vector的使用
1. 构造函数---初始化
1. 默认构造函数(无参构造)
2. 填充构造函数(指定数量和初始值)
3. 范围构造函数(通过迭代器拷贝其他容器元素)
4. 拷贝构造函数(直接拷贝另一个vector)
注意:
2. vector成员函数的使用
1. 三种遍历方法
2. size
3. capacity
4. max_size
5. reserve
6. resize
7. assign
8. insert
9. find
10. sort
11. erase
3. 二维vector(重点)
1. 核心本质与优势
2. 常见初始化方式
3. 核心操作(访问、添加、删除)
4. 遍历二维 vector
5. 注意事项
4. 总结:
1. 什么是vector?
- vector是表示可变大小数组的序列容器。
- 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
- 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
- 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
- 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好。
2. vector的使用
我们先来看一下C++官方文档中对vector成员函数的一些介绍 : vector文档
1. 构造函数---初始化
图中详细列出了 std::vector 的4种构造方式:
- 默认构造函数:创建空容器,无元素。
- 填充构造函数:创建包含 n 个元素的容器,每个元素都是 val 的副本。
- 范围构造函数:用迭代器范围 [first, last) 内的元素初始化容器。
- 拷贝构造函数:复制另一个 vector x 的所有元素来初始化容器。
1. 默认构造函数(无参构造)
- 作用:创建一个空的vector容器,不包含任何元素,内部数组(动态内存)未分配或仅分配最小默认空间。
- 示例:
std::vector<int> vec; (此时vec.empty()为 true , vec.size() 为0)。
- 核心特点:极简初始化,适合后续通过 push_back() 、 assign() 等方法动态添加元素的场景。
2. 填充构造函数(指定数量和初始值)
- 作用:创建一个包含固定数量(n个)元素的vector,且所有元素都初始化为同一个值( val 的副本,若不指定 val ,内置类型默认初始化,如int为0)。
- 示例:
std::vector<int> vec(5, 10); (创建含5个元素的vector,每个元素都是10)。 std::vector<int> vec(5); (创建含5个元素的vector,每个元素默认初始化为0)。
- 核心特点:一次性初始化“批量相同值”的元素,避免后续逐个赋值,适合已知元素数量和统一初始值的场景(如创建固定长度的初始数组)。
3. 范围构造函数(通过迭代器拷贝其他容器元素)
- 作用:从其他容器(如vector、array、list等)的指定迭代器范围 [first, last) 中,拷贝元素来初始化当前vector(左闭右开,即包含 first 指向的元素,不包含 last 指向的元素)。
- 示例:
std::vector<int> src = {1,2,3,4}; std::vector<int> vec(src.begin(), src.end()); // 拷贝src所有元素,vec变为{1,2,3,4} std::vector<int> vec2(src.begin()+1, src.end()-1); // 拷贝src的[2,3],vec2变为{2,3}
- 核心特点:灵活复用其他容器的“部分或全部元素”,无需手动逐个拷贝,适合需要基于现有容器子集/全集创建新vector的场景。
4. 拷贝构造函数(直接拷贝另一个vector)
- 作用:创建一个完全复制另一个vector( x ) 的新vector,新vector的元素、大小、容量(通常)与原vector完全一致。
- 示例:
std::vector<int> src = {1,2,3}; std::vector<int> vec(src); // 拷贝src,vec与src完全相同 std::vector<int> vec2 = src; // 等价于上一行,拷贝构造的另一种写法
- 核心特点:专门用于“复制已有vector”,是C++默认的“值拷贝”语义体现,适合需要创建原vector副本(独立于原vector,修改新vector不影响原vector)的场景。
注意:
- 可以看到第三个迭代器构造函数是一个模板函数,所以不一定只用vector的迭代器,也可以用其他容器迭代器初始化,只要数据类型匹配(*iterator对象的类型跟vector中存的数据类型是一致的):
- 迭代器进行初始化模板函数实际是这样实现的:
// template <class InputIterator> 表明这是模板函数,InputIterator是迭代器类型参数
template <class InputIterator>
vector(InputIterator first, InputIterator last) {// 函数体内通过迭代器的通用操作(++、* 解引用)遍历序列,无需关心具体迭代器类型while (first != last) {push_back(*first); // *first 取迭代器指向的元素++first; // 迭代器向后移动}
}
- 我们定义下面两个对象有没有差别?
string s("111111");
vector<char> vc(6,'1'); //调用构造函数
- 答 : 不能,vector里面给char,虽然它们底层都是数组中存char类型数据,但是还是不一样的,s对象中指向的空间结尾有\0,string的很多操作是独有的,比如+=字符串等等
2. vector成员函数的使用
1. 三种遍历方法
上面知道了vector类对象如何初始化,那么我们想要遍历该对象该怎么遍历呢?
首先使用push_back尾插进去数据,遍历方法:
- 下标+[]
- 迭代器遍历
- 范围for遍历
#include<iostream>
#include<vector>
using namespace std;
void test_vector()
{vector<int> v;//使用push_back尾插数据v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//遍历vector//1、下标+[]for(size_t i =0;i<v.size(),i++){v[i]-=1;cout<<v[i]<<" ";}cout<<endl;//2、迭代器vector<int>::iterator it = v.begin();while(it!=v.end()){*it += 1;cout<<*it<<" ";++it;}cout<<endl;//范围forfor(auto e:v){cout<< e <<" ";}cout<<endl;
}
int main()
{test_vector();return 0;
}
我们还可以利用反向迭代器进行反向遍历:
void test_vector()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//反向迭代器进行遍历vector<int>::reverse_iterator rit = v.rbegin();while(rit!=v.rend()){cout<<*rit<<" ";++rit;}cout<<endl;
}
这里的rit不是原生指针,而是被封装的类对象,重载operator++才能实现++rit时,倒着走 , 而非原生质真的直接操作。
2. size
- size 函数是用于获取容器中当前元素个数的核心接口
- 返回 vector 中实际存储的元素数量,类型为 size_t (无符号整数)。
vector<int> v = {1,2,3};
size_t count = v.size(); // count = 3
3. capacity
capacity 是 std::vector 的一个重要属性,它表示当前分配的内存能够容纳的最大元素个数(即不重新分配内存时,最多能存多少元素)。而 size 是当前实际存储的元素个数。
vector<int> v;
v.push_back(1);
v.push_back(2);
// 此时 size 是 2,capacity 可能是 2、4 或其他(由编译器和标准库实现决定,通常会预分配多于当前元素数的内存以减少扩容次数)
- 当 size 超过 capacity 时, vector 会自动扩容(重新分配更大的内存块,复制原有元素,释放旧内存),这也是 vector 动态增长的底层机制。可以通过 vector 的 capacity() 函数来获取这个值。
4. max_size
返回vector可以容纳的最大元素数。实际中并没有什么意义
void test_vector()
{vector<int> v;cout<<v.max_size()<<endl;//没什么意义v.reserve(10);//开空间,改变容量
}
5. reserve
功能 : 主动为 vector 预分配内存,确保其 capacity (容量)至少达到指定大小,不改变 size (实际元素个数)。避免多次自动扩容(扩容会复制元素、释放旧内存,耗时),适合已知元素大致数量时提升效率。若 n ≤当前 capacity ,函数不做任何操作;只增容,不缩容(缩容需用 shrink_to_fit() )。
vector<int> vec;
vec.reserve(100); // 预分配能存100个int的内存,此时size=0,capacity≥100
for (int i=0; i<100; ++i)
{vec.push_back(i); // 因已预分配,这100次插入不会触发自动扩容
}
6. resize
改变这个vector对象的长度为n,如果n小于当前vector的长度,则将当前值缩短到第n个数据,删除第n个以外的数据。如果n大于当前vector对象长度,延长该vector对象长度,并在最后插入指定内容直到达到的延长后的长度n。如果指定值, 用该值来初始化,否则,他们初始化为匿名对象。
vector<int> v = {1,2,3};// 场景1:n > size → 新增元素,值为0(int的默认值)
v.resize(5); // 现在v的size是5,元素为[1,2,3,0,0]// 场景2:n > size 且指定默认值
v.resize(7, 99); // 元素变为[1,2,3,0,0,99,99]// 场景3:n < size → 销毁末尾元素
v.resize(2); // 元素变为[1,2],capacity仍保持扩容后的大小(如7)
7. assign
分配新的内容给vector,代替它当前的内容,并且修改它的大小。可以看到assign函数的参数可以是迭代器,也可以是val个数和val
vector<int> v = {1,2,3};// 示例1:范围赋值(从其他容器/数组批量赋值)
list<int> lst = {4,5,6};
v.assign(lst.begin(), lst.end()); // v变为[4,5,6]// 示例2:重复赋值(填充n个相同元素)
v.assign(3, 99); // v变为[99,99,99]
8. insert
用于在指定位置插入元素的核心接口,支持单个元素、多个元素或范围元素的插入,在容器的指定位置插入元素,插入后容器 size 增加,若空间不足会触发扩容。
调用格式(三种重载):
插入单个元素:iterator insert(iterator pos, const T& val); (pos是插入位置的迭代器,val是要插入的元素)。
插入多个相同元素:
iterator insert(iterator pos, size_t n, const T& val); (n是插入个数)
插入范围元素:
iterator insert(iterator pos, InputIterator first, InputIterator last);( first/last是迭代器范围)
插入位置的后续元素会向后移动(若空间不足,先扩容再移动,移动过程有性能开销)。
返回值是指向第一个插入元素的迭代器,可用于后续操作。
vector<int> v = {1,3,4};// 示例1:插入单个元素
auto it = v.insert(v.begin() + 1, 2); // 在索引1的位置插入2,v变为[1,2,3,4]// 示例2:插入多个相同元素
v.insert(v.end(), 2, 5); // 在末尾插入2个5,v变为[1,2,3,4,5,5]// 示例3:插入范围元素
list<int> lst = {6,7};
v.insert(v.begin(), lst.begin(), lst.end()); // 在开头插入lst的元素,v变为[6,7,1,2,3,4,5,5]
那么我们假设想在3的前面插入呢?我们想一想我们肯定先需要找到3这个元素,才能在它前面插入元素,而我们发现vector当中没有find函数,但是在算法里面有一个find函数模板以提供使用:
9. find
find函数参数是迭代器区间以及需要找到的val值,返回的是这段区间第一次发现的元素的迭代器,如果没有发现则返回的是last,我们想要在3之前插入元素数据20:
void test_vector5()
{int a[] = { 1,3,4 };vector<int> v(a, a + 3);vector<int>::iterator pos = find(v.begin(),v.end(),3);if(pos!= v.end()){v.insert(pos,20);}for(auto e:v){cout<< e <<" ";}cout<<endl;
}
输出:
1 20 3 4
10. sort
在算法模块还有一个函数便于我们使用:sort函数
核心参数传递的就是迭代器,且明确要求是随机访问迭代器
void test_vector6()
{int a[] = { 1,2,3,4,5 };vector<int> v(a, a + 5);//默认排升序sort(v.begin(),v.end());
}
我们还可以用sort对数组进行排序:
void test_vector6()
{//指向数组的空间的指针是天然的迭代器int a1[]={30,1,13,23,42};sort(a1,a1+5);//也可以对数组排序for(auto e:a1){cout<< e <<" ";}cout<<endl;
}
指向数组的空间的指针是天然的迭代器,故也是可以对数组进行排序的
11. erase
erase 是用于从容器中删除指定元素或元素范围的核心接口,删除后容器 size 减小,剩余元素会自动前移,但不会自动缩减内存容量( capacity 不变)。
erase 有两种主要重载形式,均通过迭代器指定删除范围:
1. 删除单个元素iterator erase(iterator pos); pos :指向要删除元素的有效迭代器(不能是 end() )。
返回值:指向被删除元素下一个元素的迭代器(用于避免迭代器失效)。
2. 删除元素范围iterator erase(iterator first, iterator last); first / last :指定删除范围[first, last)(左闭右开,包含first指向的元素,不包含last指向的元素)。
返回值:指向范围 [first, last) 之后第一个元素的迭代器。
#include <vector>
#include <iostream>int main() {std::vector<int> v = {1, 2, 3, 4, 5};// 示例1:删除单个元素(删除值为3的元素)auto it = v.begin() + 2; // 指向元素3it = v.erase(it); // 删除后,it指向元素4,v变为 [1,2,4,5]// 示例2:删除元素范围(删除从it开始到末尾的元素)v.erase(it, v.end()); // 删除4、5,v变为 [1,2]// 示例3:释放多余内存(size=2,capacity可能大于2)v.shrink_to_fit(); // 可选,将capacity缩减至与size一致return 0;
}
3. 二维vector(重点)
C++ 中的二维vector :(vector<vector<T>>)
二维 vector 本质是“元素类型为 vector 的 vector ”,可理解为“动态二维数组”,其每行长度可独立变化(非严格矩形,也叫“锯齿数组”),核心特性和用法如下:
1. 核心本质与优势
- 本质:外层 vector 存储的每个元素,都是一个独立的内层 vector (即“一行”数据)。
- 优势:
- 动态大小:无需预先指定行数和列数,可随时添加/删除行、扩展每行的列数。
- 灵活性:每行长度可不同(如第一行3个元素,第二行5个元素),适合不规则数据。
- 内存安全:自动管理内存,无需手动释放,避免数组越界和内存泄漏。
2. 常见初始化方式
(1) 直接初始化(指定行数和每行列数)
适用于创建“矩形二维数组”(每行长度相同):
#include <vector>
using namespace std;// 方式1:初始化3行4列,所有元素默认值(int默认0,string默认空串)
vector<vector<int>> vec1(3, vector<int>(4)); // 方式2:初始化2行3列,所有元素指定为10
vector<vector<int>> vec2(2, vector<int>(3, 10));
// 结果:vec2 = [[10,10,10], [10,10,10]]
(2) 列表初始化(自定义每行元素)
适用于不规则或已知具体数据的场景:
// 方式1:初始化3行,每行长度分别为2、3、1
vector<vector<int>> vec = {{1, 2}, // 第0行{3, 4, 5}, // 第1行{6} // 第2行
};// 方式2:先创建空外层vector,再逐个添加行
vector<vector<int>> vec2;
vec2.push_back({10, 20}); // 添加第0行
vec2.push_back({30}); // 添加第1行
// 结果:vec2 = [[10,20], [30]]
3. 核心操作(访问、添加、删除)
(1) 访问元素
通过“外层索引[行号] + 内层索引[列号]”访问,与普通二维数组语法一致:
vector<vector<int>> vec = {{1,2}, {3,4,5}};
int a = vec[0][1]; // 访问第0行第1列,a = 2
int b = vec[1][2]; // 访问第1行第2列,b = 5// 推荐:用 at() 访问(会做越界检查,更安全)
int c = vec.at(0).at(1); // 效果同上,越界时抛异常
(2) 添加元素
- 添加一行:用 push_back() 直接添加一个内层 vector 作为新行。
- 添加一列(扩展某行):通过外层 vector 的索引找到目标行,再用 push_back() 给该行添加元素。
vector<vector<int>> vec = {{1,2}, {3,4}};// 1. 添加新行
vec.push_back({5,6,7}); // 结果:[[1,2], [3,4], [5,6,7]]// 2. 给第0行添加一个元素(扩展列)
vec[0].push_back(8); // 结果:[[1,2,8], [3,4], [5,6,7]]
(3)删除元素
- 删除最后一行:外层 vector 调用 pop_back() 。
- 删除某一行:外层 vector 调用 erase() ,通过迭代器指定行。
- 删除某行的某个元素:找到目标行,调用该行(内层 vector )的 erase() 。
vector<vector<int>> vec = {{1,2,8}, {3,4}, {5,6,7}};// 1. 删除最后一行
vec.pop_back(); // 结果:[[1,2,8], [3,4]]// 2. 删除第0行(通过迭代器)
vec.erase(vec.begin()); // 结果:[[3,4]]// 3. 给第0行删除第1个元素(列)
vec[0].erase(vec[0].begin() + 1); // 结果:[[3]]
4. 遍历二维 vector
(1) 普通 for 循环(依赖索引)
vector<vector<int>> vec = {{1,2}, {3,4,5}};
// 外层循环遍历行,内层循环遍历每行的列
for (int i = 0; i < vec.size(); ++i)
{ // vec.size() 是行数for (int j = 0; j < vec[i].size(); ++j) { // vec[i].size() 是第i行的列数cout << vec[i][j] << " ";}cout << endl;
}
(2) 范围 for 循环(更简洁)
for (auto& row : vec)
{ // 遍历每一行(用引用避免拷贝)for (auto& elem : row) { // 遍历当前行的每个元素cout << elem << " ";}cout << endl;
}
5. 注意事项
- 内存连续性:与普通二维数组(内存连续)不同,二维 vector 的每行(内层 vector )是独立的内存块,整体内存不连续,访问效率略低于普通二维数组。
- 行长度一致性:若需严格的“矩形二维数组”,需手动保证每行 size 相同(可封装函数检查或初始化时统一设置)。
- 性能优化:频繁添加行时,可先给外层 vector 用 reserve(n) 预分配行数;频繁扩展某行时,给对应内层 vector 预分配列数,减少扩容开销。
典型使用场景:
- 存储不规则数据(如每行元素个数不同的表格、稀疏矩阵)。
- 动态调整大小的场景(如刷题时不确定输入数据的行数和列数)。
- 作为函数参数传递(需注意传递方式,推荐用引用 const vector<vector<T>>& 避免拷贝)。
4. 总结:
本文介绍了C++中vector容器的基本概念和使用方法。vector是可变大小的序列容器,采用连续存储空间,支持高效随机访问和动态增长。文章详细讲解了vector的四种构造方式、常用成员函数(如size、capacity、resize、assign等)、遍历方法(下标、迭代器、范围for)以及二维vector的使用。重点内容包括:vector的动态扩容机制、元素访问和修改操作、二维vector的初始化和遍历(支持不规则数据结构)。vector相比数组和链表在随机访问和尾部操作上更高效,但在中间插入删除时性能较低,适合需要频繁访问和动态调整大小的场景。
感谢大家的观看!