C++ vector 容器使用、迭代器失效与模拟实现详解
C++ vector 动态数组常用接口及空间管理机制解析,重点阐述迭代器失效场景与原因,提供标准库模拟实现代码及 OJ 实战案例,帮助开发者深入理解底层内存模型。

C++ vector 动态数组常用接口及空间管理机制解析,重点阐述迭代器失效场景与原因,提供标准库模拟实现代码及 OJ 实战案例,帮助开发者深入理解底层内存模型。

使用 STL 的三个境界:能用,明理,能扩展。下面学习 vector,我们也是按照这个方法去学习。
vector 学习时一定要学会查看文档:vector 的文档介绍,vector 在实际中非常的重要,在实际中我们熟悉常见的接口就可以,下面列出了哪些接口是要重点掌握的。
| 构造函数声明 | 接口说明 |
|---|---|
vector() (重点) | 无参构造 |
vector (size_type n, const value_type& val = value_type()) | 构造并初始化 n 个 val |
vector (const vector& x); (重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last); | 使用迭代器进行初始化构造 |
这些构造函数在下面代码实现中均会呈现
| iterator 的使用 | 接口说明 |
|---|---|
begin + end (重点) | 获取第一个数据位置的 iterator/const_iterator,获取最后一个数据的下一个位置的 iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的 reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
当然迭代器不是简单的指针,它的实现包装还是挺复杂的,但是在存储数据的一片连续空间中我们可以用指针先简单模拟着
| 容量空间 | 接口说明 |
|---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize (重点) | 改变 vector 的 size |
reserve (重点) | 改变 vector 的 capacity |
capacity 的代码在 vs 和 g++ 下分别运行会发现,vs 下 capacity 是按 1.5 倍增长的,g++ 是按 2 倍增长的。这个问题经常会考察,不要固化的认为,vector 增容都是 2 倍,具体增长多少是根据具体的需求定义的。vs 是 PJ 版本 STL,g++ 是 SGI 版本 STL。reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。resize 在开空间的同时还会进行初始化,影响 size。
// 测试 vector 的默认扩容机制
void TestVectorExpand(){
size_t sz;
std::vector<int> v;
sz = v.capacity();
std::cout << "making v grow:\n";
for(int i=0; i<100;++i){
v.push_back(i);
if(sz != v.capacity()){
sz = v.capacity();
std::cout << "capacity changed: " << sz << '\n';
}
}
}
vs:运行结果:vs 下使用的 STL 基本是按照 1.5 倍方式扩容 making foo grow : capacity changed :1 ... capacity changed :141
g++ 运行结果:linux 下使用的 STL 基本是按照 2 倍方式扩容 making foo grow : capacity changed :1 ... capacity changed :128
// 如果已经确定 vector 中要存储元素大概个数,可以提前将空间设置足够
// 就可以避免边插入边扩容导致效率低下的问题了
void TestVectorExpandOP(){
std::vector<int> v;
size_t sz = v.capacity();
v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
std::cout << "making bar grow:\n";
for(int i=0; i<100;++i){
v.push_back(i);
if(sz != v.capacity()){
sz = v.capacity();
std::cout << "capacity changed: " << sz << '\n';
}
}
}
| vector 增删查改 | 接口说明 |
|---|---|
push_back (重点) | 尾插 |
pop_back (重点) | 尾删 |
find | 查找。(注意这个是算法模块实现,不是 vector 的成员接口) |
insert | 在 position 之前插入 val |
erase | 删除 position 位置的数据 |
swap | 交换两个 vector 的数据空间 |
operator[] (重点) | 像数组一样访问 |
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector 的迭代器就是原生态指针 T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃 (即如果继续使用已经失效的迭代器,程序可能会崩溃)。
对于 vector 可能会导致其迭代器失效的操作有:会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back 等。
- 可以理解为野指针导致的迭代器失效
#include<iostream>
using namespace std;
#include<vector>
int main(){
std::vector<int> v{1,2,3,4,5,6};
auto it = v.begin();
// 给 vector 重新赋值,可能会引起底层容量改变
v.assign(100,8);
/* 出错原因:以上操作,都有可能会导致 vector 扩容,也就是说 vector 底层原理旧空间被释放掉,而在打印时,it 还使用的是释放之间的旧空间,在对 it 迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
解决方式:在以上操作完成之后,如果想要继续通过迭代器操作 vector 中的元素,只需给 it 重新赋值即可。 */
while(it != v.end()){
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
return 0;
}
指定位置元素的删除操作–erase
代码解释:
#include<iostream>
using namespace std;
#include<vector>
int main(){
int a[]={1,2,3,4};
std::vector<int>v(a, a +sizeof(a)/sizeof(int));
// 使用 find 查找 3 所在位置的 iterator
std::vector<int>::iterator pos = find(v.begin(), v.end(),3);
// 删除 pos 位置的数据,导致 pos 迭代器失效。
v.erase(pos);
std::cout << *pos << endl; // 此处会导致非法访问
return 0;
}
erase 删除 pos 位置元素后,pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么 pos 就失效了。因此删除 vector 中任意位置上元素时,vs 就认为该位置迭代器失效了。但是 Linux 下不会挂掉。因此可以看出不同平台的编译器的设计在文档不做要求的情况下设计可能是不同的。
以下代码的功能是删除 vector 中所有的偶数,请问那个代码是正确的,为什么?
#include<iostream>
using namespace std;
#include<vector>
int main(){
std::vector<int> v{1,2,3,4};
auto it = v.begin();
while(it != v.end()){
if(*it %2==0) v.erase(it);
++it;
}
return 0;
}
int main(){
std::vector<int> v{1,2,3,4};
auto it = v.begin();
while(it != v.end()){
if(*it %2==0) it = v.erase(it); // erase 返回的删除元素的下一个元素的迭代器
else ++it;
}
return 0;
}
第二个正确
注意:Linux 下,g++ 编译器对迭代器失效的检测并不是非常严格,处理也没有 vs 下极端。
// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main(){
std::vector<int> v{1,2,3,4,5};
for(size_t i=0; i<v.size();++i) std::cout << v[i]<<" ";
std::cout << std::endl;
auto it = v.begin();
std::cout << "扩容之前,vector 的容量为:" << v.capacity() << std::endl;
// 通过 reserve 将底层空间设置为 100,目的是为了让 vector 的迭代器失效
v.reserve(100);
std::cout << "扩容之后,vector 的容量为:" << v.capacity() << std::endl;
// 经过上述 reserve 之后,it 迭代器肯定会失效,在 vs 下程序就直接崩溃了,但是 linux 下不会
// 虽然可能运行,但是输出的结果是不对的
while(it != v.end()){
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
return 0;
}
程序输出:12345 扩容之前,vector 的容量为:5 扩容之后,vector 的容量为 :1000234540912345
这个也就是迭代器失效的第一种案列 即野指针导致的迭代器失效
// 2. erase 删除任意位置代码后,linux 下迭代器并没有失效
// 因为空间还是原来的空间,后序元素往前搬移了,it 的位置还是有效的
#include<vector>
#include<algorithm>
int main(){
std::vector<int> v{1,2,3,4,5};
std::vector<int>::iterator it = find(v.begin(), v.end(),3);
v.erase(it);
std::cout << *it << std::endl;
while(it != v.end()){
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
return 0;
}
程序可以正常运行,并打印:445
// 3: erase 删除的迭代器如果是最后一个元素,删除之后 it 已经超过 end
// 此时迭代器是无效的,++it 导致程序崩溃
int main(){
std::vector<int> v{1,2,3,4,5};
auto it = v.begin();
while(it != v.end()){
if(*it %2==0) v.erase(it);
++it;
}
for(auto e : v) std::cout << e << " ";
std::cout << std::endl;
return 0;
}
从上述三个例子中可以看到:SGI STL 中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果 it 不在 begin 和 end 范围内,肯定会崩溃的。
怎么说呢?在设计个人觉得还是 vs 的挂掉比较好,因为 vs 直接规避了删除最后一个元素导致的越界问题。
// 4. 与 vector 类似,string 在插入 + 扩容操作 +erase 之后,迭代器也会失效
#include<string>
void TestString(){
std::string s("hello");
auto it = s.begin();
// 放开之后代码会崩溃,因为 resize 到 20 会 string 会进行扩容
// 扩容之后,it 指向之前旧空间已经被释放了,该迭代器就失效了
// 后序打印时,再访问 it 指向的空间程序就会崩溃
//s.resize(20, '!');
while(it != s.end()){
std::cout << *it;
++it;
}
std::cout << std::endl;
it = s.begin();
while(it != s.end()){
it = s.erase(it);
// 按照下面方式写,运行时程序会崩溃,因为 erase(it) 之后
// it 位置的迭代器就失效了
// s.erase(it);
// ++it;
}
}
所以为了使得咱们的代码可以在各个平台都能跑,在使用前,对迭代器重新赋值即可。
class Solution{
public:
int singleNumber(std::vector<int>& nums){
int value = 0;
for(auto e : nums){
value ^= e;
}
return value;
}
};
补充位运算小知识:0^任何数字 都是这个数字本身;相同的两个数按位^ 等于 0
// 涉及 resize / operator[]
// 核心思想:找出杨辉三角的规律,发现每一行头尾都是 1,中间第 [j] 个数等于上一行 [j-1]+[j]
class Solution{
public:
std::vector<std::vector<int>> generate(int numRows){
std::vector<std::vector<int>>vv(numRows);
for(int i=0; i<numRows;++i){
vv[i].resize(i+1,1);
}
for(int i=2; i<numRows;++i){
for(int j=1; j<i;++j){
vv[i][j]= vv[i-1][j]+ vv[i-1][j-1];
}
}
return vv;
}
};
总结:通过上面的练习我们发现 vector 常用的接口更多是插入和遍历。遍历更喜欢用数组 operator[i] 的形式访问,因为这样便捷。希望大家自己实现一遍上面讲解的 OJ 练习,然后请自行完成下面题目的 OJ 练习。以此增强学习 vector 的使用。
【代码实现】:
#pragma once
//vector 的底层模拟实现
#include<iostream>
#include<assert.h>
namespace twg {
template<classT>
class vector{
typedef T* iterator;
typedefconst T* const_iterator;
public:
//默认构造
vector(){}
//拷贝构造
vector(const vector<T>& s){
T* tmp =new T[s.capacity()];
//拷贝数据
for(int i=0; i<s.size(); i++){
tmp[i]= s[i];
}
_start = tmp; _finish = tmp + s.size(); _end_of_storage = tmp + s.capacity();
}
//迭代器区间拷贝构造
template<classInputIterator>
vector(InputIterator first, InputIterator last):_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){
while(first != last){
push_back(*first);
++first;
}
}
//析构
~vector(){
//释放资源
if(_start !=nullptr){
delete[] _start; _start = _finish = _end_of_storage =nullptr;
}
}
//赋值重载
vector<T>&operator=( vector<T> s){
swap(s);
return*this;
}
//其他常用接口
//访问符重载
T&operator[](size_t n){
assert(n<size());
assert(n >=0);
return*(_start + n);
}
const T&operator[](size_t n)const{
assert(n <size());
assert(n >=0);
return _start[n];
}
voidswap(vector<T>& s){
std::swap(_start, s._start);
std::swap(_finish, s._finish);
std::swap(_end_of_storage, s._end_of_storage);
}
//reserve
voidreserve(size_t n){
if(n <=capacity())return;
//不同扩容
else{
int oldsize =size();
T* tmp =new T[n];
//拷贝数据
for(int i=0; i<oldsize; i++){
tmp[i]=*(_start + i);
}
delete[] _start;//释放资源
_start = tmp; _finish = tmp + oldsize; _end_of_storage = tmp + n;;
}
}
voidpush_back(const T& x){
//检查空间是否够用
if(_finish == _end_of_storage){
//扩容
size_t newsize = _finish ==nullptr?4:capacity()*2;
reserve(newsize);
}
//尾插元素
*_finish = x;
++_finish;
}
size_t size()const{return _finish - _start;}
size_t capacity()const{return _end_of_storage - _start;}
const iterator begin()const{return _start;}
const iterator end()const{return _finish;}
voidprint(){
auto it =begin();
while(it !=end()){
*it;++it;
}
std::cout << std::endl;
for(int i=0; i<size(); i++){
std::cout <<*(_start + i)<<" ";
}
std::cout << std::endl;
}
voidresize(size_t n,T x){
if(n <=size()){
_finish = _start + n;
}elseif(n >size()&& n <capacity()){
int num = n -size();
while(num--){
push_back(x);
}
}else{
reserve(n);
int num = n -size();
while(num--){
push_back(x);
}
}
}
voidinsert(iterator pos,const T& x)// 使用 const 引用
{
// 检查 pos 有效性
assert(pos >= _start && pos <= _finish);
if(_finish == _end_of_storage){
// 扩容前计算 pos 的相对位置
size_t pos_index = pos - _start;
reserve(capacity()==0?4:capacity()*2);
pos = _start + pos_index;// 更新 pos 位置
}
iterator end = _finish -1;
while(end >= pos){
*(end +1)=*end;
--end;
}
*pos = x;// 在 pos 位置插入
++_finish;
}
voiderase(const iterator& pos){
auto it = pos+1;
while(it != _finish){
*(it -1)=*it;
++it;
}
_finish--;
}
voidpop_back(){
assert(_finish > _start);
--_finish;
}
private:
//相较 string
//其数据是三个迭代器
iterator _start =nullptr;
iterator _finish =nullptr;
iterator _end_of_storage =nullptr;
};
}
vector 的常用接口的模拟实现
假设模拟实现的 vector 中的 reserve 接口中,使用 memcpy 进行的拷贝,以下代码会发生什么问题?
int main(){
twg::vector<bite::string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}
问题分析:memcpy 是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
如果拷贝的是自定义类型的元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为 memcpy 的拷贝实际是浅拷贝。
结论:如果对象中涉及到资源管理时,千万不能使用 memcpy 进行对象之间的拷贝,因为 memcpy 是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
// 以杨慧三角的前 n 行为例:假设 n 为 5
void test2vector(size_t n){
// 使用 vector 定义二维数组 vv,vv 中的每个元素都是 vector<int>
twg::vector<twg::vector<int>>vv(n);
// 将二维数组每一行中的 vecotr<int>中的元素全部设置为 1
for(size_t i=0; i<n;++i) vv[i].resize(i+1,1);
// 给杨慧三角出第一列和对角线的所有元素赋值
for(int i=2; i<n;++i){
for(int j=1; j<i;++j){
vv[i][j]= vv[i-1][j]+ vv[i-1][j-1];
}
}
}
twg::vectortwg::vector vv(n); 构造一个 vv 动态二维数组,vv 中总共有 n 个元素,每个元素都是 vector 类型的,每行没有包含任何元素,如果 n 为 5 时如下所示:
使用标准库中 vector 构建动态二维数组时与上图实际是一致的
本文解析了 C++ vector 的基础定义、迭代器使用、空间增长机制以及增删查改实战,并通过模拟实现深入探讨了迭代器失效等核心问题。掌握 vector 不仅有助于解决算法题,更能帮助开发者理解代码如何映射到底层内存模型,为后续探索 list、map 等其他容器打下基础。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online