STL 容器:vector 动态数组的全面解析
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种即可。
#include<iostream>#include<vector>usingnamespace std;intmain(){// 方式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]<<" ";}return0;}2. vector 核心成员函数(必学)
vector 的成员函数众多,但常用的只有10个左右,我们按“访问、插入、删除、容量、其他”分类讲解,结合示例说明用法,避免记混。
(1)访问元素(3种方式)
operator[]:下标访问,与普通数组一致,无越界检查(高效,但不安全);at():下标访问,有越界检查,越界会抛出 out_of_range 异常(安全,效率略低);front()/back():访问第一个/最后一个元素,时间复杂度 O(1)(高频使用)。
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)(需移动后续元素,效率较低)。
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)。
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(用于优化扩容效率)。
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循环”和“迭代器遍历”最常用,适配不同需求。
#include<iostream>#include<vector>usingnamespace std;intmain(){ 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;return0;}三、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个元素时无需触发任何扩容,大幅提升性能。
#include<iostream>#include<vector>usingnamespace std;intmain(){ 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(提前分配的容量)return0;}注意:reserve() 只能增大容量,不能减小容量;如果传入的参数小于当前容量,reserve() 不会做任何操作。
4. 扩容导致的迭代器失效问题
这是vector使用中最常见的“坑”——当vector触发扩容后,原内存被释放,指向原内存的迭代器、指针、引用都会变成“野指针”,继续使用会导致程序崩溃或未定义行为。
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 存储自定义结构体
存储自定义结构体时,需确保结构体有默认构造函数(如果手动定义了带参构造,需显式定义默认构造),否则初始化时会报错。
#include<iostream>#include<vector>#include<string>usingnamespace std;// 自定义结构体:学生信息structStudent{ 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){}};intmain(){// 初始化自定义结构体vector vector<Student> students; students.push_back(Student("张三",18,90)); students.push_back(Student("李四",19,85)); students.push_back(Student("王五",18,95));// 遍历输出for(constauto& stu : students){// 用const引用,避免拷贝,提高效率 cout <<"姓名:"<< stu.name <<",年龄:"<< stu.age <<",成绩:"<< stu.score << endl;}return0;}2. vector 排序(配合 sort 算法)
sort 是STL最常用的排序算法,vector支持随机访问迭代器,因此可以直接使用sort对vector进行排序。默认是升序排序,也可以自定义排序规则(降序、按结构体字段排序等)。
#include<iostream>#include<vector>#include<algorithm>// sort算法头文件usingnamespace std;// 自定义排序规则:降序排序(函数形式)boolcmpDesc(int a,int b){return a > b;// a > b 时,a排在b前面,即降序}// 自定义结构体structStudent{ string name;int score;// 按成绩降序排序(成员函数形式,可选)booloperator<(const Student& other)const{return score > other.score;}};intmain(){// 1. 内置类型排序 vector<int> v1 ={3,1,4,1,5,9};sort(v1.begin(), v1.end());// 默认升序:1 1 3 4 5 9sort(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(constauto& stu : v2){ cout << stu.name <<":"<< stu.score << endl;}return0;}3. vector 去重(配合 unique + erase 算法)
vector 本身没有去重接口,但可以通过 STL 的 unique 算法(去重)+ erase 函数(删除重复元素)实现去重。注意:unique 算法只能去除“相邻的重复元素”,因此去重前必须先排序。
#include<iostream>#include<vector>#include<algorithm>usingnamespace std;intmain(){ 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 9for(int val : v){ cout << val <<" ";}return0;}五、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),避免手动管理内存。
// 错误示例:内存泄漏 vector<int*> v; v.push_back(newint(1)); v.push_back(newint(2)); v.clear();// 仅清空元素,未释放new分配的内存,导致内存泄漏// 正确示例:手动释放内存 vector<int*> v; v.push_back(newint(1)); v.push_back(newint(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“利器”。
最后,附上本文所有核心示例代码汇总,方便大家复制测试、快速上手:
// 本文核心示例代码汇总#include<iostream>#include<vector>#include<algorithm>#include<string>usingnamespace std;// 1. 初始化与基础操作voidtestInitAndBase(){ 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. 容量操作与扩容优化voidtestCapacity(){ 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. 自定义结构体与排序structStudent{ string name;int score;Student(string n,int s):name(n),score(s){}};voidtestSort(){ vector<Student> v ={{"张三",90},{"李四",85},{"王五",95}};sort(v.begin(), v.end(),[](const Student& a,const Student& b){return a.score > b.score;});for(constauto& stu : v){ cout << stu.name <<":"<< stu.score << endl;}}// 4. 去重操作voidtestUnique(){ 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,避免内存泄漏voidtestPointer(){ vector<int*> v; v.push_back(newint(1)); v.push_back(newint(2));for(int* p : v){delete p; p =nullptr;} v.clear(); cout <<"内存释放完成"<< endl;}intmain(){testInitAndBase();testCapacity();testSort();testUnique();testPointer();return0;}