STL 容器:vector 动态数组的全面解析
在C++ STL的序列式容器中,vector无疑是最常用、最灵活、最贴合日常开发需求的容器之一。无论是笔试面试中的算法题,还是实际项目中的数据存储,vector都能凭借其"动态扩容""随机访问"的核心优势,成为开发者的首选。
很多开发者对vector的认知停留在"可以自动变长的数组",但其实它的底层实现、扩容机制、接口用法以及性能优化,都有很多值得深究的细节。今天这篇博客,我们就来全面拆解vector------从基础用法到底层原理,从实操示例到避坑指南,让你真正吃透这个STL"高频容器",做到灵活运用、规避隐患。
一、vector 是什么?核心定位与优势
vector 是 STL 标准模板库中的序列式容器,底层基于动态数组实现,本质是封装了动态分配内存的模板类。它可以存储任意数据类型(内置类型int、float,自定义结构体、类等),支持动态扩容(无需手动管理内存大小),同时提供了便捷的接口,实现数据的增、删、改、查等操作。
vector 的核心优势(为什么首选它?)
-
随机访问高效:支持像普通数组一样通过下标 [ ] 直接访问元素,时间复杂度为 O(1),这是vector区别于list(双向链表)的核心优势,适合频繁读取元素的场景。
-
动态扩容省心:无需手动计算和分配内存,当存储的数据超过当前容量时,vector会自动扩容,开发者只需专注于数据操作,无需关心底层内存管理。
-
接口丰富易用:STL为vector提供了完善的成员函数(如插入、删除、排序、清空等),接口命名规范、用法简洁,上手成本低。
-
内存连续:底层数组的内存是连续的,缓存命中率高,相较于非连续内存的容器(如list),在遍历、访问时性能更优。
vector 的适用场景
结合其优势,vector 适合以下场景,也是日常开发中最常遇到的场景:
-
需要频繁通过下标访问元素(如存储用户列表、成绩排名、日志数据);
-
数据量不确定,需要动态增减(如接收用户输入、读取文件内容);
-
频繁在尾部插入/删除元素(尾部操作效率极高,时间复杂度 O(1));
-
需要与算法协同使用(如sort排序、find查找,vector支持随机访问迭代器,适配所有STL算法)。
二、vector 基础用法:从初始化到常用操作
在使用vector之前,需包含对应的头文件#include <vector>,并建议使用 std 命名空间(或显式调用 std::vector)。下面从初始化开始,逐一讲解vector的核心基础操作,搭配示例代码,方便直接复制测试。
1. vector 的初始化(5种常用方式)
vector 的初始化方式灵活,可根据实际需求选择,不同方式的效率和适用场景略有差异,重点掌握前3种即可。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 方式1:默认初始化,空vector,容量为0,后续添加元素时自动扩容
vector<int> v1;
// 方式2:初始化n个元素,每个元素默认值为0(内置类型),自定义类型需有默认构造
vector<int> v2(5); // v2: [0, 0, 0, 0, 0]
// 方式3:初始化n个元素,每个元素赋值为指定值(推荐,高效)
vector<int> v3(5, 3); // v3: [3, 3, 3, 3, 3]
// 方式4:通过已有数组/vector初始化(拷贝构造)
int arr[] = {1, 2, 3, 4, 5};
vector<int> v4(arr, arr + 5); // v4: [1, 2, 3, 4, 5](左闭右开区间)
vector<int> v5(v4); // 拷贝v4,v5与v4元素完全一致
// 方式5:通过初始化列表初始化(C++11及以上支持,最简洁)
vector<int> v6 = {1, 2, 3, 4, 5};
vector<int> v7{10, 20, 30}; // 省略等号,同样有效
// 打印测试(后续会讲遍历方式,此处暂用简单循环)
for (int i = 0; i < v7.size(); i++) {
cout << v7[i] << " ";
}
return 0;
}
2. vector 核心成员函数(必学)
vector 的成员函数众多,但常用的只有10个左右,我们按"访问、插入、删除、容量、其他"分类讲解,结合示例说明用法,避免记混。
(1)访问元素(3种方式)
-
operator[]:下标访问,与普通数组一致,无越界检查(高效,但不安全); -
at():下标访问,有越界检查,越界会抛出 out_of_range 异常(安全,效率略低); -
front()/back():访问第一个/最后一个元素,时间复杂度 O(1)(高频使用)。
cpp
vector<int> v = {1, 2, 3, 4, 5};
cout << v[0] << endl; // 输出1(无越界检查)
cout << v.at(2) << endl; // 输出3(有越界检查)
cout << v.front() << endl; // 输出1(第一个元素)
cout << v.back() << endl; // 输出5(最后一个元素)
// 注意:v[10] 不会报错,但会访问非法内存(未定义行为);v.at(10) 会直接抛出异常
(2)插入元素(2种高频方式)
-
push_back():在vector尾部插入一个元素,时间复杂度 O(1)(无扩容时),最常用; -
insert():在指定位置插入元素/元素组,时间复杂度 O(n)(需移动后续元素,效率较低)。
cpp
vector<int> v = {1, 2, 3};
// 1. push_back:尾部插入
v.push_back(4); // v: [1, 2, 3, 4]
v.push_back(5); // v: [1, 2, 3, 4, 5]
// 2. insert:指定位置插入(需配合迭代器)
// 插入单个元素:在第2个位置(下标1)插入10
v.insert(v.begin() + 1, 10); // v: [1, 10, 2, 3, 4, 5]
// 插入多个相同元素:在第3个位置(下标2)插入2个20
v.insert(v.begin() + 2, 2, 20); // v: [1, 10, 20, 20, 2, 3, 4, 5]
// 插入另一个vector的元素:在末尾插入v2的所有元素
vector<int> v2 = {6, 7, 8};
v.insert(v.end(), v2.begin(), v2.end()); // v: [1,10,20,20,2,3,4,5,6,7,8]
(3)删除元素(3种高频方式)
-
pop_back():删除尾部最后一个元素,时间复杂度 O(1),最常用; -
erase():删除指定位置/指定区间的元素,时间复杂度 O(n)(需移动后续元素); -
clear():清空vector中所有元素,但不释放内存(容量不变,size变为0)。
cpp
vector<int> v = {1, 10, 20, 20, 2, 3, 4, 5};
// 1. pop_back:删除尾部元素
v.pop_back(); // v: [1, 10, 20, 20, 2, 3, 4]
// 2. erase:删除指定位置元素(下标3的元素20)
v.erase(v.begin() + 3); // v: [1, 10, 20, 2, 3, 4]
// erase:删除指定区间元素(从下标1到下标2,左闭右开)
v.erase(v.begin() + 1, v.begin() + 3); // v: [1, 2, 3, 4]
// 3. clear:清空所有元素
v.clear();
cout << v.size() << endl; // 输出0(元素个数为0)
cout << v.capacity() << endl; // 输出4(容量不变,仍为之前的大小)
(4)容量相关(4个核心函数)
vector 的"容量(capacity)"和"大小(size)"是极易混淆的两个概念,必须重点区分:
-
size() :返回vector中当前元素的个数(实际存储的数据量);
-
capacity() :返回vector底层数组能容纳的最大元素个数(已分配的内存大小);
-
empty():判断vector是否为空(size() == 0),返回bool值(高效,推荐使用);
-
resize():调整vector的size(元素个数),若size增大,新增元素为默认值/指定值;若size减小,删除末尾多余元素(不改变capacity);
-
reserve() :提前分配内存,调整vector的capacity(容量),不改变size(用于优化扩容效率)。
cpp
vector<int> v;
cout << v.size() << " " << v.capacity() << endl; // 0 0(空vector)
v.push_back(1);
cout << v.size() << " " << v.capacity() << endl; // 1 1(容量自动扩容到1)
v.push_back(2);
cout << v.size() << " " << v.capacity() << endl; // 2 2(容量扩容到2)
v.push_back(3);
cout << v.size() << " " << v.capacity() << endl; // 3 4(容量扩容到4,通常是2倍扩容)
// resize:调整size为5,新增元素默认值0
v.resize(5);
cout << v.size() << " " << v.capacity() << endl; // 5 4?不,容量会自动扩容到不小于size,此处输出5 8
// resize:调整size为3,删除末尾2个元素
v.resize(3);
cout << v.size() << " " << v.capacity() << endl; // 3 8(容量不变)
// reserve:提前分配容量为10,不改变size
v.reserve(10);
cout << v.size() << " " << v.capacity() << endl; // 3 10(容量变为10,size仍为3)
(5)其他常用操作
-
swap():交换两个vector的元素、size和capacity(高效,时间复杂度 O(1)); -
data():返回vector底层数组的首地址(可用于与C语言数组交互)。
3. vector 的遍历方式(4种常用方式)
vector 支持多种遍历方式,可根据场景选择,其中"范围for循环"和"迭代器遍历"最常用,适配不同需求。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
// 方式1:普通for循环(下标访问,最直观,适合需要下标场景)
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
// 方式2:范围for循环(C++11及以上,简洁,无需关心下标,仅遍历)
for (int val : v) {
cout << val << " "; // val是v中元素的拷贝,修改val不影响原vector
}
cout << endl;
// 范围for循环(修改原元素,需用引用)
for (int& val : v) {
val *= 2; // 修改原vector中的元素
}
// 方式3:迭代器遍历(最通用,适配所有STL算法,推荐)
// 普通迭代器(可读可写)
vector<int>::iterator it;
for (it = v.begin(); it != v.end(); ++it) {
cout << *it << " "; // *it 访问迭代器指向的元素
}
cout << endl;
// 常量迭代器(只读,不允许修改元素)
vector<int>::const_iterator cit;
for (cit = v.cbegin(); cit != v.cend(); ++cit) {
cout << *cit << " ";
}
cout << endl;
// 方式4:反向迭代器(从尾部到头部遍历)
vector<int>::reverse_iterator rit;
for (rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " "; // 输出:10 8 6 4 2
}
cout << endl;
return 0;
}
三、vector 底层原理:扩容机制(核心重点)
很多开发者使用vector时,只知道它能自动扩容,但不清楚扩容的底层逻辑,导致在频繁插入元素时,出现性能瓶颈或迭代器失效问题。这一部分,我们就来拆解vector的扩容机制,搞懂"为什么扩容""怎么扩容""扩容有什么隐患"。
1. 扩容的本质:重新分配内存 + 拷贝元素
vector 底层是连续的动态数组,一旦数组被分配,内存地址就固定了。当我们通过 push_back() 插入元素,且当前 size == capacity 时,vector 就会触发"扩容",扩容的核心步骤的是:
-
分配一块更大的新内存(通常是原容量的2倍,不同编译器略有差异,如VS是1.5倍,GCC是2倍);
-
将原内存中的所有元素,拷贝到新内存中;
-
释放原内存空间;
-
将vector的指针指向新内存,并更新capacity(容量)。
2. 扩容的性能损耗
从扩容的步骤可以看出,扩容是一个"耗时操作"------涉及内存分配、元素拷贝、原内存释放,这些操作的时间复杂度是 O(n)(n为当前元素个数)。如果频繁插入元素,会触发多次扩容,导致性能下降。
举个例子:如果我们要插入1000个元素,vector初始容量为0,每次扩容2倍,那么会触发的扩容次数为:0→1→2→4→8→16→32→64→128→256→512→1024,共11次扩容,每次扩容都要拷贝当前所有元素,累计拷贝次数较多。
3. 扩容优化:提前 reserve 分配内存
针对扩容的性能损耗,STL 提供了 reserve() 函数,允许我们提前分配足够的容量,避免频繁扩容。比如上面的例子,我们提前 reserve(1000),就可以只分配一次内存,插入1000个元素时无需触发任何扩容,大幅提升性能。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1;
vector<int> v2;
// v2提前分配容量1000,避免扩容
v2.reserve(1000);
// 插入1000个元素,对比两者性能(肉眼可见v2更快)
for (int i = 0; i < 1000; i++) {
v1.push_back(i);
v2.push_back(i);
}
cout << v1.capacity() << endl; // 1024(多次扩容后的容量)
cout << v2.capacity() << endl; // 1000(提前分配的容量)
return 0;
}
注意:reserve() 只能增大容量,不能减小容量;如果传入的参数小于当前容量,reserve() 不会做任何操作。
4. 扩容导致的迭代器失效问题
这是vector使用中最常见的"坑"------当vector触发扩容后,原内存被释放,指向原内存的迭代器、指针、引用都会变成"野指针",继续使用会导致程序崩溃或未定义行为。
cpp
vector<int> v = {1, 2, 3};
vector<int>::iterator it = v.begin(); // it指向原内存的第一个元素(地址假设为0x123)
// 插入元素,触发扩容(原容量3,插入第4个元素,扩容到6)
v.push_back(4);
v.push_back(5);
v.push_back(6);
v.push_back(7); // 触发扩容,原内存释放,it变成野指针
// 继续使用it,未定义行为(可能崩溃、输出乱码)
cout << *it << endl;
// 解决方法:扩容后,重新获取迭代器
it = v.begin();
cout << *it << endl; // 正常输出1
四、vector 进阶用法:自定义类型、排序与去重
vector 不仅支持内置类型,还支持自定义结构体、类,同时可以与STL算法协同使用,实现排序、去重等高级操作,这也是vector高频使用的场景之一。
1. vector 存储自定义结构体
存储自定义结构体时,需确保结构体有默认构造函数(如果手动定义了带参构造,需显式定义默认构造),否则初始化时会报错。
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 自定义结构体:学生信息
struct Student {
string name;
int age;
int score;
// 默认构造函数(必须有,否则vector<Student> v(5)会报错)
Student() : name(""), age(0), score(0) {}
// 带参构造函数(方便初始化)
Student(string n, int a, int s) : name(n), age(a), score(s) {}
};
int main() {
// 初始化自定义结构体vector
vector<Student> students;
students.push_back(Student("张三", 18, 90));
students.push_back(Student("李四", 19, 85));
students.push_back(Student("王五", 18, 95));
// 遍历输出
for (const auto& stu : students) { // 用const引用,避免拷贝,提高效率
cout << "姓名:" << stu.name << ",年龄:" << stu.age << ",成绩:" << stu.score << endl;
}
return 0;
}
2. vector 排序(配合 sort 算法)
sort 是STL最常用的排序算法,vector支持随机访问迭代器,因此可以直接使用sort对vector进行排序。默认是升序排序,也可以自定义排序规则(降序、按结构体字段排序等)。
cpp
#include <iostream>
#include <vector>
#include <algorithm> // sort算法头文件
using namespace std;
// 自定义排序规则:降序排序(函数形式)
bool cmpDesc(int a, int b) {
return a > b; // a > b 时,a排在b前面,即降序
}
// 自定义结构体
struct Student {
string name;
int score;
// 按成绩降序排序(成员函数形式,可选)
bool operator<(const Student& other) const {
return score > other.score;
}
};
int main() {
// 1. 内置类型排序
vector<int> v1 = {3, 1, 4, 1, 5, 9};
sort(v1.begin(), v1.end()); // 默认升序:1 1 3 4 5 9
sort(v1.begin(), v1.end(), cmpDesc); // 自定义降序:9 5 4 3 1 1
// 2. 自定义结构体排序(两种方式)
vector<Student> v2;
v2.push_back({"张三", 90});
v2.push_back({"李四", 85});
v2.push_back({"王五", 95});
// 方式1:使用成员函数 operator<(已定义按成绩降序)
sort(v2.begin(), v2.end());
// 方式2:使用lambda表达式(更灵活,无需定义额外函数)
sort(v2.begin(), v2.end(), [](const Student& a, const Student& b) {
return a.score > b.score; // 按成绩降序
});
// 遍历输出
for (const auto& stu : v2) {
cout << stu.name << ":" << stu.score << endl;
}
return 0;
}
3. vector 去重(配合 unique + erase 算法)
vector 本身没有去重接口,但可以通过 STL 的 unique 算法(去重)+ erase 函数(删除重复元素)实现去重。注意:unique 算法只能去除"相邻的重复元素",因此去重前必须先排序。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {3, 1, 4, 1, 5, 9, 5, 3};
// 步骤1:先排序,让重复元素相邻
sort(v.begin(), v.end()); // 排序后:1 1 3 3 4 5 5 9
// 步骤2:unique去重,返回指向第一个重复元素的迭代器
auto it = unique(v.begin(), v.end()); // 去重后:1 3 4 5 9 ? ? ?(后面的元素未删除)
// 步骤3:erase删除重复元素(从it到末尾)
v.erase(it, v.end());
// 输出去重后的结果:1 3 4 5 9
for (int val : v) {
cout << val << " ";
}
return 0;
}
五、vector 使用避坑指南(高频错误总结)
结合前面的讲解,总结vector使用中最容易出现的5个错误,以及对应的解决方案,帮你规避隐患,写出更安全、高效的代码。
1. 越界访问(最常见错误)
错误:使用 v[i] 访问下标超出 size()-1 的元素(如 v.size() 为5,访问 v[5]),无越界检查,导致程序崩溃或未定义行为。
解决方案:
-
访问前判断下标是否合法(i < v.size());
-
使用 at() 访问(有越界检查,抛出异常,便于调试);
-
避免在循环中使用固定下标(如 for (int i = 0; i < 10; i++),忽略 v.size() 可能小于10)。
2. 迭代器失效(最隐蔽错误)
错误:扩容后继续使用原迭代器、指针、引用;或在 erase() 后继续使用被删除位置的迭代器。
解决方案:
-
扩容后,重新获取迭代器(如 it = v.begin());
-
erase() 会返回指向被删除元素下一个位置的迭代器,可利用返回值更新迭代器(如 it = v.erase(it));
-
提前 reserve() 分配足够容量,避免频繁扩容。
3. 混淆 size() 和 capacity()
错误:认为 v.capacity() == v.size(),或用 capacity() 判断元素个数(如 for (int i = 0; i < v.capacity(); i++))。
解决方案:
-
size():当前元素个数,用于遍历、判断元素是否存在;
-
capacity():底层容量,仅用于扩容优化(reserve()),不用于元素访问。
4. 频繁在中间插入/删除元素
错误:在vector中间频繁插入/删除元素(如 v.insert(v.begin() + 1, val)),导致大量元素移动,性能低下。
解决方案:
-
如果需要频繁在中间插入/删除,优先使用 list(双向链表,任意位置插入删除效率 O(1));
-
若必须使用vector,可尽量将插入/删除操作集中在尾部(push_back/pop_back)。
5. 未释放内存(内存泄漏隐患)
错误:vector 存储指针类型(如 vector<int*>)时,只 clear() 清空元素,未释放指针指向的内存,导致内存泄漏。
解决方案:
-
清空vector前,遍历所有元素,手动释放指针指向的内存;
-
优先使用智能指针(如 shared_ptr、unique_ptr),避免手动管理内存。
cpp
// 错误示例:内存泄漏
vector<int*> v;
v.push_back(new int(1));
v.push_back(new int(2));
v.clear(); // 仅清空元素,未释放new分配的内存,导致内存泄漏
// 正确示例:手动释放内存
vector<int*> v;
v.push_back(new int(1));
v.push_back(new int(2));
// 遍历释放内存
for (int* p : v) {
delete p; // 释放指针指向的内存
p = nullptr; // 避免野指针
}
v.clear(); // 再清空元素
六、总结:vector 的核心价值与使用建议
vector 作为 STL 中最常用的容器,其核心价值在于"平衡了效率与便捷性"------既有普通数组的随机访问高效性,又有动态数组的灵活性,同时封装了内存管理,让开发者无需关注底层细节。
最后,给大家几点使用建议,帮助你更好地运用vector:
-
优先使用 vector,除非有明确的场景需求(如频繁中间插入删除用 list,高频查找用 unordered_map);
-
插入大量元素时,提前用 reserve() 分配容量,优化性能;
-
访问元素优先用 [ ](高效),调试时可用 at()(安全);
-
遍历元素优先用范围for循环(简洁),需要迭代器时用普通迭代器(通用);
-
规避迭代器失效、越界访问、内存泄漏三大坑,写出安全、高效的代码。
vector 的用法看似简单,但想要真正用好、用活,需要深入理解其底层扩容机制和注意事项。希望本文的全面解析,能帮助你彻底吃透vector,在笔试面试和实际开发中,都能灵活运用这个STL"利器"。
最后,附上本文所有核心示例代码汇总,方便大家复制测试、快速上手:
cpp
// 本文核心示例代码汇总
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
// 1. 初始化与基础操作
void testInitAndBase() {
vector<int> v1;
vector<int> v2(5, 3);
vector<int> v3 = {1, 2, 3, 4, 5};
v3.push_back(6);
v3.insert(v3.begin() + 2, 10);
v3.pop_back();
v3.erase(v3.begin() + 1);
for (int val : v3) {
cout << val << " ";
}
cout << endl;
}
// 2. 容量操作与扩容优化
void testCapacity() {
vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
}
// 3. 自定义结构体与排序
struct Student {
string name;
int score;
Student(string n, int s) : name(n), score(s) {}
};
void testSort() {
vector<Student> v = {{"张三", 90}, {"李四", 85}, {"王五", 95}};
sort(v.begin(), v.end(), [](const Student& a, const Student& b) {
return a.score > b.score;
});
for (const auto& stu : v) {
cout << stu.name << ":" << stu.score << endl;
}
}
// 4. 去重操作
void testUnique() {
vector<int> v = {3, 1, 4, 1, 5, 9, 5, 3};
sort(v.begin(), v.end());
auto it = unique(v.begin(), v.end());
v.erase(it, v.end());
for (int val : v) {
cout << val << " ";
}
cout << endl;
}
// 5. 指针类型vector,避免内存泄漏
void testPointer() {
vector<int*> v;
v.push_back(new int(1));
v.push_back(new int(2));
for (int* p : v) {
delete p;
p = nullptr;
}
v.clear();
cout << "内存释放完成" << endl;
}
int main() {
testInitAndBase();
testCapacity();
testSort();
testUnique();
testPointer();
return 0;
}