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 就会触发“扩容”,扩容的核心步骤的是:

  1. 分配一块更大的新内存(通常是原容量的2倍,不同编译器略有差异,如VS是1.5倍,GCC是2倍);
  2. 将原内存中的所有元素,拷贝到新内存中;
  3. 释放原内存空间;
  4. 将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:

  1. 优先使用 vector,除非有明确的场景需求(如频繁中间插入删除用 list,高频查找用 unordered_map);
  2. 插入大量元素时,提前用 reserve() 分配容量,优化性能;
  3. 访问元素优先用 [ ](高效),调试时可用 at()(安全);
  4. 遍历元素优先用范围for循环(简洁),需要迭代器时用普通迭代器(通用);
  5. 规避迭代器失效、越界访问、内存泄漏三大坑,写出安全、高效的代码。

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;}

Read more

C++ 入门必看:引用怎么用?inline 和 nullptr 是什么?

C++ 入门必看:引用怎么用?inline 和 nullptr 是什么?

目录 * 一、引用 * 1.1 引用的概念和定义 * 1.2 引用的特性 * 1.3 引用的使用 * 1.3.1 引用传参的使用 * 1.3.2 传引用返回的错误使用 * 1.3.3 传引用返回的正确使用 * 1.4 const引用 * 1.5 指针和引用的关系 * 二、inline * 三、nullptr * 总结 🎬 云泽Q:个人主页 🔥 专栏传送入口: 《C语言》《数据结构》《C++》《Linux》 ⛺️遇见安然遇见你,不负代码不负卿~ 在这篇文章开始之前,我想给大家推荐一个非常牛的人工智能学习网站。在近几年,大家也知道人工智能和 AI 技术的发展也是非常迅速,

By Ne0inhk
Python 安装教程【使用 Python install manager】

Python 安装教程【使用 Python install manager】

下载 官网如下 https://www.python.org/downloads/ 如果选择传统的【exe】格式,安装时会有如下界面 NOTE: This installer is being retired and will no longerbe available after Python 3.15 这句话的翻译是 注意:此安装程序即将停用,在 Python 3.15 版本发布后将不再提供 所以推荐选择【msix】格式的安装包,这是现代打包格式 安装 双击下载的【msix】文件 1 当准备就绪时启动: 勾选后,点击“安装 Python” ->

By Ne0inhk
【与C++的邂逅】--- string容器使用

【与C++的邂逅】--- string容器使用

Welcome to 9ilk's Code World         (๑•́ ₃ •̀๑) 个人主页:        9ilk (๑•́ ₃ •̀๑) 文章专栏:     与C++的邂逅    本篇博客我们将来了解string容器本身以及接口的使用。 string是串,本质是一个字符数组,可以对其进行增删查改。 🏠 string构造 //常用 string s1; string s2("hello world"); string s3(s2); //不常用 string s4(s2,3,5); // string (const string& str, size_t pos, size_t len = npos) string s5(s2,

By Ne0inhk