🔥铅笔小新z:个人主页
🎬博客专栏:C++学习
💫滴水不绝,可穿石;步履不休,能至渊。
引言
在C++标准模板库(STL)中,vector是最重要、最常用的容器之一。它提供了动态数组的功能,能够自动管理内存,同时保持了数组的随机访问特性。本文将深入探讨vector的各个方面,从基本使用到高级特性,再到底层实现原理。
1. vector的基本介绍
1.1 vector是什么?
vector是一个序列容器,表示可以改变大小的数组。与普通数组不同,vector能够动态增长和收缩,自动处理内存管理。它支持快速随机访问,在尾部插入和删除元素效率高,但在中间或头部插入删除效率相对较低。
1.2 学习vector的三个境界
- 能用:掌握基本接口,能够在实际编程中使用vector
- 明理:理解vector的工作原理和内部机制
- 能扩展:能够根据需要自定义或扩展vector的功能
1.3vector 相关接口
2. vector的基本使用
2.1 vector的构造函数
// 1. 默认构造函数 - 创建空vectorvector<int>v1;// 2. 构造并初始化n个valvector<int>v2(5,10);// 5个元素,每个都是10// 3. 拷贝构造vector<int>v3(v2);// 4. 使用迭代器范围构造intarr[]={1,2,3,4,5};vector<int>v4(arr,arr+5);2.1.1 默认构造函数
vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}2.1.2 构造并初始化n个val
vector(size_t n,constT&val=T()){reserve(n);for(size_t i=0;i<n;i++){push_back(val);}}2.1.3 拷贝构造
vector(constvector<T>&v){reserve(v.size());for(auto&e:v){push_back(e);}}2.2 vector的迭代器
迭代器提供了访问容器元素的统一方式:
vector<int>v={1,2,3,4,5};// 正向迭代器for(autoit=v.begin();it!=v.end();++it){cout<<*it<<" ";}// 反向迭代器for(autorit=v.rbegin();rit!=v.rend();++rit){cout<<*rit<<" ";}// 范围for循环(C++11)for(auto&elem:v){cout<<elem<<" ";}2.3 vector的空间管理
vector有两个重要的容量概念:
- size: 当前容器中实际元素的数量
- capacity: 容器在不重新分配内存的情况下可以容纳的元素数量
vector<int>v;cout<<"size: "<<v.size()<<endl;// 0cout<<"capacity: "<<v.capacity()<<endl;// 0cout<<"empty: "<<v.empty()<<endl;// truev.reserve(100);// 预分配100个元素的空间cout<<"capacity after reserve: "<<v.capacity()<<endl;// 100v.resize(50);// 改变size为50,多出的位置用0填充cout<<"size after resize: "<<v.size()<<endl;// 502.4 vector的容量增长策略
vector的容量增长策略在不同实现中有所不同:
voidTestVectorExpand(){vector<int>v;size_t sz=v.capacity();cout<<"capacity growth:\n";for(inti=0;i<100;++i){v.push_back(i);if(sz!=v.capacity()){sz=v.capacity();cout<<"capacity changed: "<<sz<<endl;}}}不同编译器的实现差异:
- VS系列编译器:按大约1.5倍增长
- g++编译器:按2倍增长
这种差异是因为不同版本的STL实现不同,不要固化认为vector总是2倍增长。
2.5 vector的增删查改操作
vector<int>v={1,2,3};// 1. 添加元素v.push_back(4);// 尾部插入v.insert(v.begin()+1,5);// 在指定位置插入// 2. 删除元素v.pop_back();// 尾部删除v.erase(v.begin());// 删除指定位置元素v.clear();// 清空所有元素// 3. 访问元素cout<<v[0]<<endl;// 使用operator[],不检查边界cout<<v.at(0)<<endl;// 使用at(),会检查边界// 4. 查找元素(需要包含<algorithm>)autoit=find(v.begin(),v.end(),3);if(it!=v.end()){cout<<"Found: "<<*it<<endl;}// 5. 交换两个vectorvector<int>v2={10,20,30};v.swap(v2);// 交换v和v2的内容3. vector迭代器失效问题(重点)
迭代器失效是使用vector时需要特别注意的问题。迭代器失效意味着迭代器指向的内存空间已经无效,继续使用会导致未定义行为(通常程序崩溃)。
3.1 vector的底层存储结构
vector在内存中是一段连续的存储空间:
template<classT>classvector{private:T*_start;// 指向数据起始位置T*_finish;// 指向最后一个元素的下一个位置T*_end_of_storage;// 指向存储空间末尾// ...};当你获取一个迭代器时,它本质上是一个指针(或指针的封装):
vector<int>v={1,2,3,4,5};autoit=v.begin();// it本质上是一个 int*,指向第一个元素内存布局如下:
地址:0x1000 0x1004 0x1008 0x100C 0x1010 0x1014... 数据: 1 2 3 4 5 ... ↑ it (指向0x1000)3.2 导致迭代器失效的操作
情况1:空间重新分配
vector<int>v={1,2,3,4,5};autoit=v.begin();// 以下操作都可能导致迭代器失效v.resize(100);// 扩容v.reserve(100);// 扩容v.push_back(6);// 可能导致扩容v.insert(v.begin(),0);// 可能导致扩容v.assign(100,8);// 重新赋值,可能改变容量// 错误:it可能已经失效while(it!=v.end()){// 可能访问已释放的内存cout<<*it<<" ";++it;}扩容过程:
- 在堆上申请新的、更大的连续内存空间
- 将旧内存中的数据拷贝到新内存
- 释放旧内存空间
- 更新vector的内部指针到新内存
关键问题:迭代器it仍然指向旧的、已被释放的内存地址:
旧内存(已被释放):0x1000 0x1004 0x1008... ↑ it (仍然指向这里,但内存已无效) 新内存(当前有效):0x2000 0x2004 0x2008... 1 2 3 ...解决方案:在可能导致扩容的操作后,重新获取迭代器。
情况2:元素删除
vector<int>v={1,2,3,4,5};autoit=v.begin()+2;// it指向元素3v.erase(v.begin()+1);// 删除元素2删除过程:
删除前: [1, 2, 3, 4, 5] ↑ it指向3 删除元素2: 1. 删除位置1的元素: [1, _, 3, 4, 5] 2. 向前移动元素3-5: [1, 3, 4, 5] 删除后: [1, 3, 4, 5] ↑ it指向4(不是原来的3了)解决方案:使用erase的返回值更新迭代器。
// 正确删除vector中所有偶数的示例vector<int>v={1,2,3,4};autoit=v.begin();while(it!=v.end()){if(*it%2==0){it=v.erase(it);// erase返回下一个有效迭代器}else{++it;}}情况3:插入元素导致内存移动
即使不扩容,插入操作也可能导致迭代器失效:
vector<int>v={1,2,3,4,5};autoit=v.begin()+2;// it指向元素3v.insert(v.begin()+1,99);// 在位置1插入99插入操作需要移动元素:
插入前: [1, 2, 3, 4, 5] ↑ it指向3 插入过程: 1. 向后移动元素2-5: [1, _, 2, 3, 4, 5] 2. 在位置1插入99: [1, 99, 2, 3, 4, 5] 插入后: [1, 99, 2, 3, 4, 5] ↑ it仍然指向原来的内存位置,但现在是元素2虽然it指向的内存地址仍然有效,但元素已经改变了位置,原来的迭代器不再指向期望的元素。
情况4:编译器的差异处理
不同编译器对迭代器失效的处理严格程度不同:
- VS编译器:检测严格,失效后立即报错
- g++编译器:检测较宽松,可能继续运行但结果错误
// 在g++下可能不崩溃,但结果错误vector<int>v={1,2,3,4,5};autoit=v.begin();v.reserve(100);// 扩容,迭代器失效// 可能不崩溃,但输出错误while(it!=v.end()){cout<<*it<<" ";// 未定义行为++it;}3.3 string的迭代器失效问题
与vector类似,string也有迭代器失效问题:
string s="hello";autoit=s.begin();s.resize(20,'!');// 可能导致扩容,迭代器失效// 错误:使用可能已失效的迭代器while(it!=s.end()){cout<<*it;// 可能崩溃++it;}3.4 具体操作分析
(1)resize(n, val)可能扩容
vector<int>v={1,2,3,4,5};// 容量5autoit=v.begin();v.resize(100,0);// 需要扩容到至少100// 旧内存不够 → 申请新内存 → 拷贝数据 → 释放旧内存// it失效!(2)reserve(n)可能扩容
vector<int>v={1,2,3,4,5};// 容量5autoit=v.begin();v.reserve(100);// 容量从5扩大到100// 必须重新分配内存 → it失效!(3)push_back(val)可能触发扩容
vector<int>v={1,2,3,4,5};// 容量5,已满autoit=v.begin();v.push_back(6);// 需要扩容// 容量不足 → 重新分配内存 → it失效!(4)insert(pos, val)可能触发扩容或移动
vector<int>v={1,2,3,4,5};// 容量5autoit=v.begin()+2;v.insert(v.begin(),0);// 在开头插入// 两种情况:// 1. 如果容量不足:扩容 → it完全失效(指向释放的内存)// 2. 如果容量足够:元素向后移动 → it指向的元素改变(5)assign(n, val)完全重新分配
vector<int>v={1,2,3,4,5};autoit=v.begin();v.assign(100,8);// 替换所有内容// 清空现有元素 → 可能需要重新分配内存 → it失效!3.5 为什么访问失效迭代器会导致问题?
对于已释放的内存(情况1)
vector<int>v={1,2,3};autoit=v.begin();v.reserve(100);// 重新分配内存,释放旧内存cout<<*it;// 访问已释放的内存!可能的后果:
- 程序崩溃:访问无效内存地址(段错误)
- 读取垃圾值:内存已被其他数据覆盖
- 未定义行为:任何事情都可能发生
对于元素位置改变(情况2和3)
vector<int>v={1,2,3};autoit=v.begin()+1;// 指向2v.insert(v.begin(),0);// 插入元素cout<<*it;// 输出2?不,可能是其他值!结果是逻辑错误:迭代器不指向期望的元素。
3.6 正确的做法
方案1:重新获取迭代器
vector<int>v={1,2,3,4,5};autoit=v.begin();// 执行可能使迭代器失效的操作v.resize(100);// 重新获取迭代器it=v.begin();// 重要:重新赋值// 现在可以安全使用while(it!=v.end()){cout<<*it<<" ";++it;}方案2:使用索引替代迭代器
vector<int>v={1,2,3,4,5};size_t index=0;// 使用索引而不是迭代器v.resize(100);// 扩容// 索引不受影响(只要不超过size)if(index<v.size()){cout<<v[index];// 安全}方案3:使用返回值更新迭代器
vector<int>v={1,2,3,4,5};autoit=v.begin();// insert返回新插入元素的位置it=v.insert(it,0);// 在开头插入0,it更新为指向新元素// erase返回被删除元素的下一个位置it=v.erase(it);// 删除it指向的元素,it更新为下一个元素3.7 总结:迭代器失效的核心原因
| 操作 | 失效原因 | 影响范围 |
|---|---|---|
| resize/reserve | 内存重新分配 | 所有迭代器、指针、引用失效 |
| push_back | 可能触发内存重新分配 | 如果扩容,所有迭代器失效 |
| insert | 1. 可能扩容 2. 元素移动 | 1. 所有迭代器失效 2. 部分迭代器指向错误元素 |
| erase | 元素向前移动 | 被删除及之后位置的迭代器失效 |
| assign/clear | 完全重新分配或清空 | 所有迭代器失效 |
根本原因:vector保证元素在内存中连续存储。为了维持这种连续性,当需要更多空间或插入删除元素时,必须移动元素或重新分配内存,这会导致原有的地址引用失效。
重要原则:在修改vector容量的操作后,永远假设所有现有的迭代器、指针和引用都已失效,除非操作文档明确说明它们保持有效。
4. vector在算法题中的应用
4.1只出现一次的数字
classSolution{public:intsingleNumber(vector<int>&nums){intresult=0;for(intnum:nums){result^=num;// 利用异或性质}returnresult;}};4.2杨辉三角
classSolution{public:vector<vector<int>>generate(intnumRows){vector<vector<int>>triangle(numRows);for(inti=0;i<numRows;++i){triangle[i].resize(i+1,1);// 每行有i+1个元素,初始化为1// 计算中间元素for(intj=1;j<i;++j){triangle[i][j]=triangle[i-1][j-1]+triangle[i-1][j];}}returntriangle;}};4.3删除排序数组中的重复项
classSolution{public:intremoveDuplicates(vector<int>&nums){if(nums.empty())return0;intk=0;for(inti=1;i<nums.size();++i){if(nums[i]!=nums[k]){nums[++k]=nums[i];}}returnk+1;}};5. vector的模拟实现
5.1 vector的基本框架
namespacemy{template<classT>classvector{private:T*_start;// 指向数据起始位置T*_finish;// 指向最后一个元素的下一个位置T*_end_of_storage;// 指向存储空间末尾public:// 构造函数vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}// 析构函数~vector(){delete[]_start;_start=_finish=_end_of_storage=nullptr;}// 容量相关size_tsize()const{return_finish-_start;}size_tcapacity()const{return_end_of_storage-_start;}boolempty()const{return_start==_finish;}// 元素访问T&operator[](size_t pos){return_start[pos];}constT&operator[](size_t pos)const{return_start[pos];}// 迭代器T*begin(){return_start;}T*end(){return_finish;}constT*begin()const{return_start;}constT*end()const{return_finish;}// 预留空间voidreserve(size_t n){if(n>capacity()){T*new_start=newT[n];size_t sz=size();// 深拷贝元素for(size_t i=0;i<sz;++i){new_start[i]=_start[i];}delete[]_start;_start=new_start;_finish=_start+sz;_end_of_storage=_start+n;}}// 添加元素voidpush_back(constT&val){if(_finish==_end_of_storage){reserve(capacity()==0?4:capacity()*2);}*_finish=val;++_finish;}// 交换数据voidswap(vector<T>&v){std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_end_of_storage,v._end_of_storage);}// 赋值运算符重载vector<T>&operator=(vector<T>v){swap(v);return*this;}// 插入数据iteratorinsert(iterator pos,constT&x){assert(pos>=_start);assert(pos<=_finish);// 扩容if(_finish==_end_of_storage){size_t len=pos-_start;reserve(capacity()==0?4:capacity()*2);pos=_start+len;}iterator end=_finish-1;while(end>=pos){*(end+1)=*end;--end;}*pos=x;++_finish;returnpos;}// 删除数据iterarorerase(iterator pos){assert(pos>=_start);assert(pos<_finish);iterator it=pos+1;while(it!=end()){*(it-1)=*it;++it;}--_finish;returnpos;}};}5.2 使用memcpy的问题
在实现vector的扩容时,不能简单地使用memcpy进行内存拷贝:
// 错误示例:使用memcpy拷贝自定义类型voidreserve(size_t n){if(n>capacity()){T*new_start=newT[n];// 错误:对于管理资源的自定义类型,memcpy是浅拷贝memcpy(new_start,_start,size()*sizeof(T));delete[]_start;// 释放旧空间,可能导致资源重复释放_start=new_start;// ... 其他更新}}问题分析:
memcpy是二进制内存拷贝,执行的是浅拷贝- 对于管理资源的自定义类型(如
string),浅拷贝会导致多个对象共享同一资源 - 释放旧空间时,资源被释放,新空间中的对象指向已释放的资源
正确做法:使用深拷贝
// 正确做法:使用深拷贝voidreserve(size_t n){if(n>capacity()){T*new_start=newT[n];size_t sz=size();// 深拷贝:调用元素的拷贝构造函数或赋值运算符for(size_t i=0;i<sz;++i){new_start[i]=_start[i];// 调用T的赋值运算符或拷贝构造函数}// 析构旧元素for(size_t i=0;i<sz;++i){_start[i].~T();// 显式调用析构函数}delete[]_start;_start=new_start;_finish=_start+sz;_end_of_storage=_start+n;}}6. 动态二维数组的实现
vector可以方便地实现动态二维数组:
// 创建n行的二维数组vector<vector<int>>createMatrix(intn){vector<vector<int>>matrix(n);for(inti=0;i<n;++i){matrix[i].resize(i+1,1);// 第i行有i+1个元素}returnmatrix;}// 访问二维数组voidprintMatrix(constvector<vector<int>>&matrix){for(inti=0;i<matrix.size();++i){for(intj=0;j<matrix[i].size();++j){cout<<matrix[i][j]<<" ";}cout<<endl;}}7. 使用vector的最佳实践
预分配空间:如果知道大概需要多少元素,使用
reserve()预分配空间,避免频繁扩容。谨慎使用迭代器:在可能修改容量的操作后,不要使用旧的迭代器。
选择合适的访问方式:
- 随机访问:使用
operator[]或at() - 遍历:使用范围for循环或迭代器
- 性能敏感:考虑使用指针访问
- 随机访问:使用
注意元素的拷贝成本:存储大对象时,考虑存储指针或使用移动语义。
善用swap:使用
swap()快速清空vector或交换两个vector的内容。
// 快速清空vector(释放内存)vector<int>v(1000000);vector<int>().swap(v);// 清空v并释放内存// C++11更简洁的方式v.clear();v.shrink_to_fit();// 请求释放未使用的内存总结
vector是C++中最重要、最常用的容器之一。掌握vector不仅需要了解其基本用法,还需要深入理解其内部工作原理,特别是迭代器失效、内存管理和性能特征。通过合理使用vector,可以编写出高效、安全的C++代码。
在实际开发中,应根据具体需求选择合适的容器。vector适合需要频繁随机访问、尾部插入删除的场景。如果需要频繁在中间插入删除,可能需要考虑list或deque;如果需要快速查找,可能需要考虑set或map。
通过深入理解vector,我们不仅能够更好地使用这个容器,还能够学习到C++内存管理、模板编程、异常安全等高级主题,为成为更优秀的C++程序员打下坚实基础。
希望这篇长文对你有帮助!如果觉得不错,欢迎点赞、收藏、分享~
作者:铅笔小新z
日期:2025 年 12 月 16 日
目标:一篇讲透 C++ vector,从使用到底层实现。