C++--vector的使用及其模拟实现
vector的使用及其模拟实现
1. vector 的使用
vector 是我们学习的第一个真正的 STL 容器,它接口的使用方式和 string 有一点点的不同,但大部分都是一致的,所以这里就只演示其中一些常用接口的使用,如果有疑惑的地方直接在 cplusplus上查看对应的文档即可。
1.1 构造函数与初始化
vector 提供了四种构造方式。无参构造、n 个 val 构造、迭代器区间构造以及拷贝构造:
构造函数声明 | 接口说明 |
---|---|
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化 n 个 val |
vector(const vector& x);(重点) | 拷贝构造 |
vector(InputIterator first, InputIterator last); | 使用迭代器进行初始化构造 |
其中构造函数的最后一个参数 alloc 是空间配置器,它和内存池有关,作用是提高空间分配的效率;我们日常使用时不用管这个参数,使用它的缺省值即可,但是可能有极少数的人想要用自己实现的空间配置器来代替 STL 库提供的,所以留出了这一个参数的位置。
常用的初始化方式:
注意:迭代器区间构造是一个函数模版,即可以使用其他对象构造 vector 对象
这里可以利用 string
对象构造 vector
对象,但是在构造的时候选择的构造 vector
对象是 int
类型的,所以就会以字符的 ASCII
码值的形式存储在 vector
中。
如果想以字符形式存储在 vector
中,改变构造时候的模版参数即可。
补充:
vector<char> 和 string 的区别
- 内部真实数据不同:
string
对象的结尾包含\0
。但是vector<char>
不包含。 - 接口的功能不同:
string
可能需要插入字符或者字符串所以有push_back
、append
和+=
这些实现不同情况的插入接口。然而vector<char>
则没有。
vector 和 string 的混合使用
同时因为单参数构造支持隐式类型转换,这里可以通过 vstr.push_back("张三");
这种方式进行传参,这是因为这里 vstr
的模版参数是 string
允许字符串传参,这里的 张三
会通过构造函数构造一个 string
的对象再作为参数传给 vector
。
//代码优化
int main()
{vector<string> vstr;vstr.push_back("张三");vstr.push_back("李四"); vstr.push_back("王五"); return 0;
}
另一种初始化方式
这种初始化方式是调用了一个C++11里面的一个构造函数。
在这里插入图片描述
上面的 vector<int> v1 = { 1, 2, 3, 4, 5 };
可以理解为一种隐式类型转换,因为 { 1, 2, 3, 4, 5 }
的类型是 initializer_list
(是一种只需要遍历不允许修改的容器),这个 initializer_list
类型的对象会构造一个 vector
的临时对象,再拷贝构造得到 v1
。
1.2 扩容机制
vector 的扩容机制和 string 的扩容机制是一样的,因为它们都是动态增长的数组:VS 下大概是 1.5 被扩容,Linux g++ 下是标准的 2 倍扩容,不要固化的认为,vector增容都是 2 倍,具体增长多少是 根据具体的需求定义的。
测试用例如下:
void TestVectorExpand()
{size_t sz;vector<int> v;sz = v.capacity();cout << "making v grow:\n";for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()) {sz = v.capacity();cout << "capacity changed: " << sz << '\n';}}
}
1.3 三种遍历方式
和 string
一样,vector
也支持三种遍历方式 ,下标加[]遍历、迭代器遍历、范围for遍历:
补充:
-
不同容器遍历数据的方式
需要注意的是,
vector
和string
之所以支持 下标 + [] 的方式遍历,是因为它们底层都是数组,而数组支持随机访问,但是像后面要学习的 list set map 等容器,它们的底层不是数组,不支持随机访问,就只能通过迭代器和范围 for 的方式进行遍历了;不过,范围 for 只是一个外壳,它在使用时也是被替换成迭代器,所以其实迭代器遍历才是最通用的遍历方式。 -
范围 for 遍历 vector<string> 对像
//代码优化 int main() {vector<string> vstr;vstr.push_back("张三");vstr.push_back("李四"); vstr.push_back("王五");//遍历vectorfor(const auto& e : vstr){cont << e << endl;}return 0; }
这里的范围
for
的参数使用了const
修饰的传引用传参的方式,这是因为范围for
的本质是通过迭代器(可以理解为指针)进行解引用传给e
,再对e
进行操作。但是这是这里的string
类型的对象在进行传参的时候涉及深拷贝,会造成空间的消耗。所以这里采用传引用的方式,节省空间提升效率。并且使用const
保证数据的安全。
1.4 Iterators
iterator的使用 | 接口说明 |
---|---|
begin + end(重点) | 获取第一个数据位置的iterator/const_iterator ,获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator ,获取第一个数据前一个位置的reverse_iterator |
1.5 Capacity
vector
有如下容量相关的接口:
容量空间接口 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize(重点) | 改变 vector 的 size |
reserve(重点) | 改变 vector 的 capacity |
其中大部分接口与 string
中的使用相同,这里不做过多的介绍。
1.5.1 reserve && resize
其中,最重要的两个函数是 reserve
和 resize
,reserve
只用于扩容,它不改变 size
的大小;而 resize
是扩容加初始化,既会改变 capacity
,也会改变 size
。
reserve
和 resize
,包括后面的 clear
函数都不会缩容,因为缩容就需要开辟新空间,再拷贝数据到新的空间,最后再释放旧空间,而对于自定义类型又有可能存在深拷贝问题,时间开销极大。
vector
中唯一可能缩容的函数就只有 shrink_to_fit
,对于它来说,如果 capacity
大于 size
,它会进行缩容,让二者相等。但是在平时使用的时候很少使用。
1.5.1.1 reserve
//reserve使用场景
// 如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够
// 就可以避免边插入边扩容导致效率低下的问题了void TestVectorExpandOP()
{vector<int> v;size_t sz = v.capacity();v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容cout << "making bar grow:\n";for (int i = 0; i < 100; ++i) {v.push_back(i);if (sz != v.capacity()){sz = v.capacity();cout << "capacity changed: " << sz << '\n';}}
}
1.5.1.2 resize
1.5 Element access
vector 提供了如下接口来进行元素访问:
其中大部分接口与 string
中的使用相同,这里不做过多的介绍。
其中,operator []
和 at
都是返回 pos
下标位置元素的引用,且它们内部都会对 pos
的合法性进行检查;不同的是,operator []
中如果检查到 pos
非法,那么它会直接终止程序,报断言错误,而 at
则是抛异常。
注:release 模式下检查不出断言错误。
补充:
多种容器之间的 operator [] 嵌套使用的辨析
如上图的这中类似于二维数组的调用方式,实际是两种容器对于 operator [] 的嵌套使用 首先 vsrt[0]
取出下标中的第一个数据,也就是 string
类型的 张三 ,之后的 []
调用的是 string
中的 operator []
会访问 张三
中的下标为 2
的元素并 ++
,因为编码的原因,最后由 张三 变成了 **掌三 **。
1.6 Modifiers
vector 增删查改 | 接口说明 |
---|---|
push_back(重点) | 尾插 |
pop_back(重点) | 尾删 |
find | 查找。(注意这个是算法模块实现,不是 vector 的成员接口) |
insert | 在 position 之前插入 val |
erase | 删除 position 位置的数据 |
swap | 交换两个 vector 的数据空间 |
operator [](重点) | 像数组一样访问 |
其中大部分接口与 string
中的使用相同,这里不做过多的介绍。
1.6.1 assign
assign
函数用来替换 vector
对象中的数据,支持 n
个 val
替换,以及迭代器区间替换。注意它不是添加或修改现有元素,而是重新填充。
1.6.2 insert
和 string 不同,为了提高规范性,STL 中的容器都统一使用 iterator
作为 pos
的类型,并且插入/删除后会返回 pos
:
**补充:**因为 string
本质不属于 STL 的容器,所以其 insert
的使用和实现和 vector
并不一样。
在这里插入图片描述
1.6.3 erase
和 insert 一样,erase 也和 string 中的使用不同,使用 iterator
作为 pos
的类型,并且插入/删除后会返回 pos
。
1.7 算法库中的接口
1.7.1 find
和 string
不一样的 vector
并没有单独实现 find
这个接口,这并不是代表 vector
这个容器没有办法去查找内部的数据,而是在算法库中使用模版封装了 find
函数,这样不仅vector
可以调用其他容器也可以调用。
和 insert
和 erase
一样,算法库中的 find
和 string
中的 find
也不一样,其调用的参数和返回值均是 iterator
类型的迭代器。
2. 有关 vector 的面试题
2.1 只出现一次的数字
题目链接:136. 只出现一次的数字 - 力扣(LeetCode)
题目描述:
给你一个 非空 整数数组
nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
class Solution
{
public:int singleNumber(vector<int>& nums) {int value = 0;for(auto e : nums) {value ^= e;}return value;}
};
思路总结:
这题是是经典的单身狗问题,可以通过异或操作将相同的数据都消掉,最后可以得到那个单独的数字。
为什么异或操作能找到只出现一次的数字?
按位异或
^
的特性:
- 任何数
x
与0
异或,结果是x
本身。
即:x ^ 0 = x
- 任何数
x
与它自己异或,结果是0
。
即:x ^ x = 0
- 异或运算满足交换律和结合律。
即:a ^ b ^ c = c ^ a ^ b
,顺序无关。由于数组中除了某个数字只出现一次,其他数字都出现两次,异或遍历:
- 两个相同数字异或完,结果是
0
。- 最终
value
就是只出现一次的数字。
2.2 杨辉三角
题目链接:118. 杨辉三角 - 力扣(LeetCode)
class Solution
{
public:vector<vector<int>> generate(int numRows) {//初始化vv,使其包含numRows个元素,每个元素都是空的//这里的vv存放的是杨辉三角每行数据的vector地址vector<vector<int>> vv(numRows);for(int i = 0; i < numRows; ++i){//vv[i]表示第i行数据,现在需要对每行数据大小进行调整,并都初始化成1vv[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<vector<int>>
的本质思想和C语言的二维数组一致,最外层的 vector 是一个个指向内层 vector 的指针,内层的 vector 存放着杨辉三角每一行到数据。- 然后通过内外层的 operator [] + 下标,实现对具体数据的访问。
3. vector 的模拟实现
下面的实现讲解将在 test.cpp
和 vector.h
这两个文件下进行,因为模版不支持分离编译在两个文件,所以就没有单独创建 vector.cpp
文件。
3.1 浅析 vector 源码
vector 的部分源码如下:
//vector.h
#ifndef __SGI_STL_VECTOR_H
#define __SGI_STL_VECTOR_H#include <algobase.h>
#include <alloc.h>
#include <stl_vector.h>#ifdef __STL_USE_NAMESPACES
using __STD::vector;
//stl_vector.h
template <class T, class Alloc = alloc>
class vector {
public:typedef T value_type;typedef value_type* pointer;typedef const value_type* const_pointer;typedef value_type* iterator;typedef const value_type* const_iterator;typedef value_type& reference;typedef const value_type& const_reference;typedef size_t size_type;typedef ptrdiff_t difference_type;//成员函数protected:typedef simple_alloc<value_type, Alloc> data_allocator;iterator start;iterator finish;iterator end_of_storage;
}
可以看到,vector.h
仅仅是将几个头文件包含在一起,vector 的主要实现都在 stl_vector.h
里面。
3.2 vector 核心框架
3.2.1 vector 的基本结构
//vector.h
namespace tcq
{template<class T>class vector {public:typedef T* iterator;typedef const T* const_iterator;public://成员函数private:T* _start = nullptr;T* _finish = nullptr;T* _endofstorage = nullptr;};
}
可以看到,vector
的底层和 string
一样,都是一个指针指向一块动态开辟的数组,但是二者不同的是,string
是用 _size
和 _capacity
两个 size_t
的成员变量来维护这块空间,而 vector
是用 _finish
和 _end_of_storage
两个指针来维护这块空间。
虽然 vector
使用指针看起来难了一些,但本质上其实是一样的,_size = _finish - _start
,_capacity = _end_of_storage - _start
。
在这里插入图片描述
3.2.2 vector 的常用接口
3.2.2.1 size
//vector.h
size_t size() const
{return _finish - _start;
}
3.2.2.2 capacity
//vector.h
size_t capacity() const
{return _endofstorage - _start;
}
3.2.3 vector 的构造和析构
3.2.3.1 构造函数
3.2.3.1.1 迭代区间构造
//vector.h
// 类模板的成员函数,也可以是一个函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}
注意:
-
这里的迭代区间构造的构造函数的参数使用模版的方式编写,这样这个迭代器不仅可以是
vector
的迭代器,还可以是其他容器的迭代器。但是需要注意其他容器型使用迭代器构造出的
vector
类型的对象,可能显示的结果会不同。比如使用string
的迭代器构造出的vector
对象内部存储的是对应字符的ASCII码值。这反应了其底层的数据都是一些二进制数据,只是通过编码配合不同的容器从而展示出不一样的结果。
3.2.3.1.2 n个val构造
//vector.h
//n个val构造
vector(size_t n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}//n个val构造 -- 重载
vector(int n, const T& val = T()):_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{reserve(n);for (int i = 0; i < n; i++)push_back(val);
}
注意:
-
构造函数错误调用的问题
模拟实现了构造函数中的迭代器区间构造和 n 个 val 构造后,会发现一个奇怪的问题,使用 n 个 val 来构造其他类型的对象都没问题,唯独构造
int
类型的对象时会编译出错。//test.c void test() {tcq::vector<int> v(10, 5);for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl } //报错:“非法间接寻址”
这是由于编译器在进行模板实例化以及函数参数匹配时会调用最匹配的一个函数,当将 T 实例化为
int
之后,由于两个参数都是int
,所以对于迭代器构造函数来说,它会直接将前面所实现的迭代器区间构造中的参数类型InputIterator
实例化为int
。但对于 n 个 val 的构造来说,它不仅需要将 T 实例化为
int
,还需要将第一个参数隐式转换为size_t
;所以编译器默认会调用迭代器构造,同时由于迭代器构造内部会对first
进行解引用,所以这里报错 “非法的间接寻址”;//迭代器区间构造 template<class InputIterator>vector(InputIterator first, InputIterator last):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){while (first != last){push_back(*first);++first;}}//n个val构造 vector(size_t n, const T& val = T()):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);for (size_t i = 0; i < n; i++)push_back(val);}
解决方法有很多种,比如将第一个参数强转为
int
,又或者是将 n 个 val 构造的第一个参数定义为int
,这里和 STL 源码保持一致,提供第一个参数为 int 的 n 个 val 构造的重载函数:
3.2.3.1.3 另一种构造
//vector.h
vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}
注意:
- 这里的实现参考源码,为了适应
vector<int> v1 = {1, 2, 3, 4};
这种初始化构造,所以因引入了一个新的类initializer_list
和这个新的构造函数,来适应这种初始化方式。
3.2.3.2 析构函数
//vector.h
~vector()
{if (_start){delete[] _start;_start = _finish = _endofstorage = nullptr;}
}
注意:
_start
用于指向 vector 容器的首元素地址,也就是底层数组的首元素地址,所以释放空间时只需要写delete[] _start;
即可。- 并且这里释放的空间可能不止一个数据,需要使用
delete []
释放连续的空间。
3.2.3.3 拷贝构造函数
//vector.h
// v2(v1)
vector(const vector<T>& v)
{reserve(v.capacity());for (auto& e : v){push_back(e);}
}
注意:
- 这里的形参
v
是v2
,this
就是v1
,这里复用已经模拟实现的接口push_back
相当于先为v2
开辟一块空的空间,再将v1
中的数据通过范围 for 一个个遍历插入v2
的空间中,这是一个非常新颖的方式。
3.3 vector 的插入和删除
3.3.1 resever
//vector.h
void reserve(size_t n)
{if (n > capacity()){//保存旧空间的大小size_t oldSize = size();//深拷贝T* tmp = new T[n];if (_start){for (size_t i = 0; i < oldSize; i++){//本质调用T的赋值运算符tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + oldSize;_endofstorage = _start + n;}
}
因为后续的插入都涉及会空间容量的处理,所以这里需要先模拟实现 resrver
。
注意:
- 这里使用的开辟空间的方式更推荐使用
new
,而不是malloc
。因为这里的模版参数T
,可能是一些自定义类型,这样使用 new 的话就会自动调用其构造函数进行初始化,否则如果使用malloc
还需要再后面调用构造函数比较麻烦,故不推荐。
3.3.2 push_back
//vector.h
void push_back(const T& x)
{if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish = x;++_finish;
}
注意:
- 因为无法判断扩容的时候字符串内部是否已经存在空间,所以不能直接对
capacity*2
处理,所以使用三目操作符位完善代码功能,这个思想和顺序表部分的扩容思想是一致的。
3.3.3 pop_back
//vector.h
void pop_back()
{assert(!empty());--_finish;
}
注意:
- 因为对于 vector 来说是用
_start
和_finish
来表示有效数据这段区间,所以尾删也就是需要让_finish
前进一格即可。
3.3.4 insert
//vector.h
iterator insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);//扩容if (_finish == _endofstorage){//更新possize_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}//后移数据iterator i = _finish - 1;while (i >= pos){*(i + 1) = *i;--i;}//插入数据*pos = x;++_finish;return pos;
}//test.cpp
int main()
{vector<int> v1;//在下标为3的地方插入10v1.insert(v1.begin()+2, 10);
}
注意:
-
这里的
insert
使用迭代器去实现,不用下标去实现。这样也就避免了之前在实现string
的insert
的接口移动数据时出现的pos
等于0,而出现的死循环问题。因为迭代器可以理解为指针,而指针不会等于0。同样因为使用了迭代器去代替下标在调用的时候也需要使用迭代器去调用。
-
同时因为迭代器失效的问题,要注意对
pos
进行更新。(迭代器失效的问题在后面进行详细介绍)
3.3.5 erase
//vector.h
iterator erase(iterator pos)
{assert(pos >= _start);assert(pos < _finish);//将pos后面的数据依次前移iterator i = pos + 1;while (i < _finish){*(i - 1) = *i;++i;}_finish--;return pos;
}
注意:
- 同样这里模拟实现的思路十分简单,但是需要注意这里需要使用迭代器去实现,不用下标去实现功能。并且其实也涉及了迭代器失效问题需要注意。
- 并且注意 erase 返回的是缩容位置的下一个位置。
3.4 vector的遍历
3.4.1 operator [] + 下标遍历
//vector.h
const T& operator[](size_t i) const
{assert(i < size());return _start[i];
}const T& operator[](size_t i) const
{assert(i < size());return _start[i];
}
- 这里的
operator []
需要写两种版本一种用于处理普通参数,一种用于处理被const
修饰的参数。 - 普通参数可写可读,const参数只可读不可写。
3.4.2 普通迭代器遍历
//vector.h
iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}
注意:
-
同样这里需要使用写两种函数用于接收不同参数(普通参数和被 const 修饰的参数)。
-
前面在
vector
的基本结构处,为了统一规范化不同容器中的itreator
已经对其进行宏定义,所以这里的返回值类型统一为 iterator 或者const_iterator
。
3.6 vector 的其它接口
3.6.1 empty
//vector.h
bool empty()
{return _start == _finish;
}
3.6.2 resize
//vector.h
void resize(size_t n, T val = T())
{//小于删除数据,缩容if (n <= size()){_finish = _start + n;}//大于扩容,插入数据else{reserve(n);while (_finish < _start + n){*_finish = val;++_finish;}}
}
补充:
-
resize 参数缺省值的思考
这里的
resize
中的参数val
使用了隐式对象的方式给了缺省值,但是这里的val
的类型是模版参数T
是不确定,这里需要进行讨论。-
T
为自定义类型自定义类型的
val
使用隐式对象的方式给了缺省值,相当于自动调用自定义类型的默认构造。 -
T
为内置类型以往一般都认为,内置类型是没有构造函数的,但是因为这种写法的存在所以也就肯定了内置类型也是有默认构造的。如果这里的
T
为int
则val
的缺省值是0
;如果这里的T
为double
则val
的缺省值是0.0
。
-
3.6.3 swap
//vector.h
void swap(vector<T>& tmp)
{std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);
}
注意:
- 根据 string 中介绍的三种 sawp的辨析可知,这里如果用
vector
中内置的swap
函数会调用深拷贝,效率较低。更推荐直接使用算法库中的swap
函数,直接交换数据的指针即可。
3.6.4 operator =
//vector.h
// v1 = v3
vector<T>& operator=(vector<T> v)
{swap(v);return *tis;
}
4. 模拟实现的问题讨论
4.1 迭代器失效
4.1.1 问题引入:
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
在 VS 下,insert
和 erase
之后会导致 pos
迭代器失效。
在这里插入图片描述
4.1.2 问题分类
对于vector可能会导致其迭代器失效的操作有:
4.1.2.1 会引起其底层空间改变的操作,都有可能是迭代器失效
比如:resize、reserve、insert、assign、push_back等。
#include <iostream>
#include <vector>
using namespace std;int main()
{vector<int> v{1,2,3,4,5,6};auto it = v.begin();// 情况1:将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容// v.resize(100, 8);// 情况2:reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变// v.reserve(100);// 情况3:插入元素期间,可能会引起扩容,而导致原空间被释放// v.insert(v.begin(), 0);// v.push_back(8);// 情况4:给vector重新赋值,可能会引起底层容量改变//v.assign(100, 8);//解决方法:更新迭代器itwhile(it != v.end()){cout<< *it << " " ;++it;}cout<<endl;return 0;
}
以上情况出错原因:以上操作,都有可能会导致 vector
扩容,也就是说 vector
底层原理旧空间被释放掉,而在打印时,it
还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。
4.1.2.2 指定位置元素的删除操作 – erase
#include <iostream>
#include <vector>
using namespace std;int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 3);// 删除pos位置的数据,导致pos迭代器失效。//v.erase(pos);//cout << *pos << endl; // 此处会导致非法访问,导致崩溃//解决方式:更新 pos 迭代器,使其接收 erase 方法的返回值pos = v.erase(pos);cout << *pos << endl;return 0;
}
出错原因:erase
删除 pos
位置元素后,pos
位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后 pos
刚好是 end
的位置,而 end
位置是没有元素的,那么 pos
就失效了。因此删除 vector
中任意位置上元素时,vs都会认为该位置迭代器失效了。
解决方式:在使用前,对迭代器重新赋值即可。
4.1.3 其他编译器中的失效情况
vector的迭代器失效也与编译器环境有关,这里有关指的是报错情况及运行上,例如在Linux下,g++对于迭代器失效的检查就没有那么严格,一般迭代器失效也能运行,只不过运行结果会出错,并不会直接中断。总之,迭代器失效一定会导致错误,所以在平时使用迭代器的时候为了保证程序的跨平台性,统一认为迭代器失效之后,必须更新后才能再次使用。
4.1.4 string 的迭代器失效
与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效。
#include <string>void TestString()
{string s("hello");auto it = s.begin();// 放开之后代码会崩溃,因为resize到20会string会进行扩容// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了// 后序打印时,再访问it指向的空间程序就会崩溃//s.resize(20, '!');while (it != s.end()){cout << *it;++it;}cout << endl;it = s.begin();while (it != s.end()){it = s.erase(it);// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后// it位置的迭代器就失效了// s.erase(it); ++it;}
}
4.1.5 总结
迭代器失效一定会导致错误,所以在平时使用迭代器的时候为了保证程序的跨平台性,统一认为迭代器失效之后,必须更新后才能再次使用。
4.2 reserve 函数的浅拷贝
首先根据 reserve
的需要模拟实现的 reserve
如下:
void reserve(size_t n)
{if (n > capacity()) //reserve 函数不缩容{T* tmp = new T[n];memcpy(tmp, _start, sizeof(T) * size());size_t oldSize = _finish - _start; //记录原来的size,避免扩容之后无法确定_finishdelete[] _start;_start = tmp;_finish = _start + oldSize;_end_of_storage = _start + n;}
}
但是针对这段代码仍具有缺陷,对于内置类型来说它确实是进行了深拷贝,但是对于需要进行深拷贝的自定义类型来说它就有问题了。
程序报错的原因如图:
当 v 中的元素达到4个再进行插入时,
push_back
内部就会调用reserve
函数进行扩容,空间里面的内容(vector的成员函数)是使用memcpy
按字节拷贝过来的,这就导致原来的v
里面的string
元素和现在v
里面的元素指向的是同一块空间。当拷贝完毕之后使用
delete[]
释放原空间,而delete[]
释放空间时对于自定义类型会调用其析构函数,而v
内部的string
对象又会去调用自己的析构函数,所以delete[]
完毕后原来的v
以及v
中各个元素指向的空间都被释放了,此时现在的v
里面的每个元素全部指向已经释放的空间。从第一张图中也可以看到,最后一次
push_back
之后 v 里面的元素全部变红了;最终,当程序结束自动调用析构函数时,就会去析构刚才已经被释放掉的v
中的各个string
对象指向的空间,导致同一块空间被析构两次,程序出错。
所以,在 reserve 内部,不能使用 memcpy 直接按字节拷贝原空间中的各个元素,因为这些元素可能也指向一块动态开辟的空间,而应该调用每个元素的拷贝构造进行深拷贝,如图:
最终所以实现的 reserve 函数就是上面模拟实现中所展示的函数。