深入理解 C++ vector 源码与迭代器失效问题 - 指南
目录


前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
该文章是vector模拟实现的一部分,因为放在一起写太多了所以分为两篇写,为了不影响阅读体验,建议两篇连起来看,这篇是第一篇~
一、vector源码解析
想要模拟实现vector就要了解vector的底层,而想要了解vector的底层就要了解STL中vector的源码。一般看源码看两个版本,一个是SGI版本(Linux下使用),一个是PG版(Windows下的VS继承),下面对于源代码的解析就是SGI版本(3.0),这是一个老的源码了(十几年前的一个版本),最新的版本框架均已成熟,而且内部嵌入了很多新的语法特性,例如C++14,17等等,内部实现复杂不易理解,这里模拟实现的vector也是一个简易的vector,想要该版本源码的可以添加博主微信
平时包的头文件vector.h就是图上
平时包的vector内部也是包着其他头文件的,除了核心还有其他文件的原因是有时候在vector内部可能会用内存池等等,所以包了这些头文件(内存池就实现在alloc的头文件),而construct头文件就是用来辅助内存池的,内部实现了构造析构等函数来初始化等等,内存池只开空间并不初始化,释放的时候也不清理空间
下面是对vector.h这个核心的头文件的核心内容的梳理
二、vector的模拟实现
2.1 using关键字的新语法
typedef还有一个新语法,在C++14加入进来的,如图:
using除了可以用来展开命名空间,还可以用来重定义(C++有很多地方会复用同一个关键字),using在这里的功能和typedef的功能是一样的:using iterator = T* ; 是类型别名的定义,作用是将 iterator 定义为 T*(T 类型的指针)的别名。
在这个模板类 vector< T > 中,这样定义后:
- 当 T 是 int 时,iterator 就等价于 int* ;
- 当 T 是 double 时,iterator 就等价于 double*;
2.2 迭代器+构造+扩容+尾插
这里迭代器我是使用原生指针来实现的,实际上的例如vector,list的迭代器是用一个类来封装的,因为这里的模拟实现只是实现一个百分之70的底层逻辑,实现一个百分百一摸一样的太复杂了,但是迭代器的底层肯定是和指针有着千丝万缕的关系的
这里没有做声明和定义分离是因为模板不能分离定义到两个文件,分离定义到两个文件会导致链接错误


这里对程序进行运行然而却发生了报错
通过调试可以发现_finish在这里被赋值后居然还是空指针,end_of_storage是没有问题的00变为10,且_start没有问题,那么必然是size()出现问题了,这里size()内_start是新空间的_start(不为0),而_finish没有更新,依旧是旧空间的_finish(为0),所以相减肯定会出问题,size()中_finish - _start;应该为0才对(此时只是开了4个空间,一个数据都没有)
所以应该在前面把_start没有更新之前的size()算好保存起来

此时就没有问题了
2.3 Print 与 const 迭代器
接下来写一个Print函数来完成vector内数据的打印工作
然而又发生报错了,原因是在 Print 函数中,参数是 const vector< int >& v(const 引用)。范围 for 循环会调用 v.begin() 和 v.end(),但当前 vector 类仅实现了非 const 版本的 begin 和 end,而 const 对象无法调用非 const 成员函数,因此会导致编译错误,程序无法运行。
解决方案:添加const迭代器和const版本的begin/end
有兄弟就说了,我Print函数的参数不使用非const 引用 vector< int >& v不就好了吗?
但是Print 函数的参数使用非 const 引用 vector< int >& v的同时也会带来很多缺陷
- 安全性隐患:可能意外修改容器内容
Print 函数的核心功能是 “打印” 容器元素,逻辑上不应该修改容器。但非 const 引用允许函数内部对 v 进行任意修改(比如 v.push_back(10)、v[0] = 20 等),即使开发者原本没有修改意图,也可能因手误导致容器内容被意外篡改,增加代码出错风险 - 兼容性下降:无法接收 const 对象或临时对象
若传入一个 const 修饰的 vector 对象(如 const vector< int > v = {1,2,3};),调用 Print(v) 会直接编译报错。因为非 const 引用 vector< int >& 无法绑定到 const 对象上(C++ 不允许通过非 const 引用修改 const 对象,这是一种 const _cast 级别的危险行为,编译器会直接禁止)。
若传入一个临时 vector 对象(如 Print(vector< int >{1,2,3})),同样会编译报错。因为 C++ 规定非 const 引用不能绑定到临时对象(临时对象具有 const 属性,本质同上)。
再说一下为什么说**vector< int >{1,2,3}**被称为临时对象,因为我第一眼看也有些懵,我相信有人肯定和我有一样的疑问
vector<int> v{1,2,3}; // 这里的“v”就是这个对象的名字
这个 v 是有名字的,它就像你给一个东西起了个代号,以后可以通过 v 反复操作它(比如 v.push_back(4)、cout << v[0] 等)。这种有名字的对象,我们叫 “命名对象”
而 vector< int >{1,2,3} 这个写法,没有给对象起任何名字,是通过列表初始化直接构造的 vector< int > 对象,它没有被赋值给任何变量,就像 **int a = (3 + 5);**这里的 3 + 5 计算出的 8 也是个临时值(临时对象的一种)
其应用场景如下:
临时对象的生命周期规则是:从构造开始,到 “包含它的完整表达式结束” 时销毁
void func() {
Print(vector<int>{1,2,3}); // 临时对象在 Print 调用期间存在,函数返回后立即销毁
vector<int> v = vector<int>{1,2,3}; // 临时对象在赋值完成后销毁,数据被移动/拷贝到 v 中
}
上面的 8 赋值给 a 后,这个 8 就消失了
2.4 operator[ ]
以上是在vector 类中重载 operator[ ],就可以让 vector 对象也能通过 v[i] 访问元素,其内部实现最终依赖于 “指针 _start 的下标访问规则”(即 _start[i] 等价于 * ( _start + i )),_start[i] 等价于 * (_start + i) 是 C++ 对指针和数组下标访问的底层语法规则。即像下面,没有运算符重载,但 p[1] 和 * (p + 1) 完全等价 —— 这就是 C++ 对指针下标访问的原生语法规则。
int arr[3] = {1,2,3};
int* p = arr;
cout << p[1] << endl; // 等价于 *(p + 1),输出2
cout << *(p + 1) << endl;// 输出2
注意这里的重载的[ ]也要写两个版本,以应对const对象的使用

2.5 析构+empty+pop_back


2.6 insert的迭代器失效问题
2.6.1 内存扩容导致全迭代器失效



这里隐藏了一个非常隐蔽的问题
4个和5个头插的区别就是要不要扩容,若扩容了,前面的逻辑就很坑
扩容之后拷贝数据+释放空间,然而当end要挪动数据的时候是和pos这个迭代器位置比较的
然而扩容之后pos依旧指向的是旧空间的位置,这种情况就称之为pos迭代器失效,第一种pos迭代器失效就是野指针,这里的pos相当于野指针了(pos指向的空间已经被释放了)
所以接下来就要让pos指向新空间对应的位置,主要是算出pos相对_start的相对距离,无论新旧空间,这个距离是一定的


此时就没有问题了
如图在 C++ 中,v.begin()返回的是vector容器的起始迭代器,v.begin() + 3是对这个迭代器进行偏移运算后得到的新迭代器(指向容器中第 4 个元素),而auto关键字会自动根据右侧表达式的类型(这里就是迭代器类型)推导出it的类型,因此it必然是一个迭代器。
2.6.2 元素移动导致部分迭代器失效
即使未触发扩容,insert 操作会将 pos 位置及之后的元素集体后移(参考 insert 函数中 while (end >= pos) 的循环逻辑)。此时,指向这些 “被移动元素” 的迭代器会失效,因为它们的逻辑位置被破坏了。
例如测试代码中,auto it = v.begin() + 3 指向某个元素,执行 v.insert(it, 30) 时,it 及之后的元素会被后移,it 原来指向的元素位置发生了改变,因此 it 失效。
且图中it作为实参传递给形参,形参的改变并不会影响实参,虽然insert内部对pos处理了,但是对pos的修改不会影响外面的it对象,所以依旧失效,虽然说扩容了才失效,实际情况C++标准库并没有规定vector什么时候扩容,怎么扩容,只知道按GCC是2倍扩容,VS是1.5倍扩容,所以这里认为无论是否扩容,都认为失效
所以这里就意味着insert以后,it就不能使用了,再使用就有可能出现未知的情况
有兄弟就想到把pos位置改为引用就不存在该问题了吧,修改形参就相当于修改实参了,然而会编译报错,且库中的insert也没有这么干
一个迭代器对象有可能是定义的一个对象(it),也有可能是一个表达式(不限于使用v.begin()返回一个迭代器,或者it迭代器+5),这种表达式的结果是一个临时对象(it + 5的结果),临时对象/匿名对象都具有常性,pos位置给了引用就是权限的放大了
而把const加上又不能修改内部的pos了,陷入了前面问题的死循环,所以该地方的问题是无解的(形参的改变不能影响实参),失效了之后就不能再使用了
2.7 erase的迭代器失效问题
删除pos的位置的值后,把pos+1到_finish之间的值全向前挪动即可

这里erase依然会存在两种失效情况
2.7.1 被删除位置的迭代器直接失效
在 erase 函数中,pos 指向的元素会被删除,该位置的内存被 “回收”(逻辑上不再属于有效元素区域)。因此,指向该位置的迭代器 pos 会直接失效,后续无法再通过它访问有效元素。
例如在 test_vector3 中,auto it = v.begin() + 2; v.erase(it); 执行后,it 指向的元素被删除,it 本身也随之失效。
2.7.2 被删位置之后的迭代器失效
erase 底层会将被删位置之后的所有元素向前移动(如 while (it != _finish) { *(it - 1) = *it; ++it; } 逻辑)。这会导致这些元素的物理位置或逻辑位置被篡改:
若元素向前移动,原来指向这些元素的迭代器,其 “逻辑指向” 会与实际元素不匹配(比如原迭代器指向第 3 个元素,移动后该迭代器可能指向第 2 个元素);
虽然内存未扩容,但元素的相对位置被改变,导致这些迭代器的语义失效。
例如,若有迭代器指向被删位置的下一个元素,在元素前移后,该迭代器的指向会出现逻辑错误,因此失效。
2.8 不同平台下对迭代器失效的处理
且不同平台对这里的失效问题的体现也有所不同,VS2022检查严格,只要erase以后的迭代器对象就失效了,不能访问
这里vector 的 erase 操作会导致两类迭代器失效:
- 被删除位置的迭代器直接失效:执行 v.erase(it) 后,it 指向的元素被删除,it 本身变为无效迭代器。
- 被删位置之后的迭代器失效:erase 会将被删位置之后的元素向前移动,导致这些元素对应的迭代器逻辑指向被破坏,同样失效。
在这段代码中,当执行 v.erase(it) 后,it 已经失效,此时继续执行 ++it,本质是对失效的迭代器进行自增操作,违反了 vector 迭代器的有效性规则,最终触发运行时错误。
而G++下有时候就可以正常删除,程序没有崩溃,结果也是正确的
下面多插入了一个数据2,虽然程序没有崩溃,但是结果并不正确

再多插入一个6,程序就崩溃了

出现内存错误
这里Linux下出现这种碰巧能通过的情况可以根据不同的数据内容按照前面的代码逻辑推理一下就清楚了。
这里迭代器失效的解决方案库里面也有,失效的迭代器不能直接使用,除非重置它
C++ 标准库中std::vector::erase的文档说明,其返回值的含义是:
它返回一个迭代器,指向函数调用中被删除的最后一个元素的下一个元素的新位置。如果此次操作删除了序列中的最后一个元素,该返回迭代器就是容器的end()(即容器末尾的后一个位置,标记容器的结束)。
例如:若vector为[1,2,3,4,5],调用erase删除元素3(对应的迭代器位置)后,返回的迭代器会指向4的新位置;若删除的是最后一个元素5,则返回vector::end()。
G++一样改了代码之后就不会崩溃了
所以模拟实现的vector也应如此


insert也一样,为了防止迭代器失效也有一个返回值。该函数返回一个迭代器,指向新插入元素中的第一个元素。
一般情况下所有容器的插入删除都会有迭代器失效,前面string部分的文章没写迭代器失效的原因是string不太关注迭代器失效,因为vector的insert和erase主要使用迭代器作为接口参数
string的insert和erase主要使用下标作为接口参数,虽然也有迭代器的,但很少使用,若使用迭代器作为接口参数的版本也会失效
三、vector模拟实现源码
vector.h
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
namespace yunze
{
template<class T>
class vector
{
public:
//typedef T* iterator;//定义迭代器
using iterator = T*;//和typedef作用一样
using const_iterator = const T*;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
//新增const版本的begin/end
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
}
~vector()
{
if (_start)
{
delete[] _start;
}
_start = _finish = _end_of_storage = nullptr;
}
bool empty() const
{
return _start == _finish;
}
void reserve(size_t n)
{
//n比capacity小,不进行缩容
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
//把旧空间的数据拷贝过来
//第一次进来没有旧空间,(特殊处理)
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
//不能+capacity(),此时的_end_of_storage还未更新
_end_of_storage = _start + n;
}
}
size_t size() const
{
return _finish - _start;
}
//这里的[]既可以读也可以写
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
//这里的[]只能读
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
size_t capacity() const
{
return _end_of_storage - _start;
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
//满了
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//这里我自己写的vector开空间不用内存池,用new
//new对于自定义类型会调用对应默认构造
//不用担心自定义类型对象赋值给随机值的问题
*_finish = x;
++_finish;
}
void pop_back()
{
assert(!empty());
--_finish;
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
//空间满了,扩容
if (_finish == _end_of_storage)
{
//若扩容要特殊处理
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//此时的_start是新空间的_start
pos = _start + len;
}
//在pos位置插入数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
//返回删除数据的下一个位置
return pos;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 666
#include"vector.h"
namespace yunze
{
//支持迭代器就支持范围for
void Print(const vector<int>& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
////const调const[]版本,只可以读
//for (size_t i = 0; i < v.size(); i++)
//{
// //v[0]++;//const对象只能读不能写
// cout << v[i] << " ";
//}
//cout << endl;
}
void test_vector1()
{
yunze::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(5);
v.push_back(5);
v.push_back(5);
v.push_back(5);
v.push_back(5);
v.push_back(5);
//普通对象调普通[]版本
v[0]++;
Print(v);
}
void test_vector2()
{
yunze::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
Print(v);
v.insert(v.begin(), 0);
Print(v);
//在下标为3的位置插入30
auto it = v.begin() + 3;
v.insert(it, 30);
Print(v);
}
void test_vector3()
{
yunze::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
Print(v);
v.erase(v.begin());
Print(v);
//删除下标为2位置的值
auto it = v.begin() + 2;
v.erase(it);
Print(v);
}
void test_vector4()
{
yunze::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
//Print(v);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
////删除所有偶数
//auto it = v.begin();
//while (it != v.end())
//{
// if (*it % 2 == 0)
// {
// v.erase(it);
// }
// ++it;
//}
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
{
++it;
}
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
}
using namespace yunze;
int main()
{
//test_vector1();
//test_vector2();
//test_vector3();
test_vector4();
return 0;
}
结语

浙公网安备 33010602011771号