当前位置: 首页 > news >正文

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_backappend+= 这些实现不同情况的插入接口。然而 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遍历

在这里插入图片描述

补充:

  • 不同容器遍历数据的方式

    需要注意的是,vectorstring 之所以支持 下标 + [] 的方式遍历,是因为它们底层都是数组,而数组支持随机访问,但是像后面要学习的 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

其中,最重要的两个函数是 reserveresizereserve 只用于扩容,它不改变 size 的大小;而 resize扩容加初始化,既会改变 capacity,也会改变 size

reserveresize,包括后面的 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 对象中的数据,支持 nval 替换,以及迭代器区间替换。注意它不是添加或修改现有元素,而是重新填充。

在这里插入图片描述

1.6.2 insert

和 string 不同,为了提高规范性,STL 中的容器都统一使用 iterator 作为 pos 的类型,并且插入/删除后会返回 pos

**补充:**因为 string 本质不属于 STL 的容器,所以其 insert 的使用和实现和 vector 并不一样。

image-20221130202538822

在这里插入图片描述

1.6.3 erase

和 insert 一样,erase 也和 string 中的使用不同,使用 iterator 作为 pos 的类型,并且插入/删除后会返回 pos

在这里插入图片描述

在这里插入图片描述

1.7 算法库中的接口

1.7.1 find

string 不一样的 vector 并没有单独实现 find 这个接口,这并不是代表 vector 这个容器没有办法去查找内部的数据,而是在算法库中使用模版封装了 find 函数,这样不仅vector 可以调用其他容器也可以调用。

在这里插入图片描述

inserterase 一样,算法库中的 findstring 中的 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;}
};

思路总结:

这题是是经典的单身狗问题,可以通过异或操作将相同的数据都消掉,最后可以得到那个单独的数字。

为什么异或操作能找到只出现一次的数字?

按位异或 ^ 的特性:

  • 任何数 x0 异或,结果是 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.cppvector.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);}
}

注意:

  • 这里的形参 vv2this 就是 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 使用迭代器去实现,不用下标去实现。这样也就避免了之前在实现 stringinsert 的接口移动数据时出现的 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 为内置类型

      以往一般都认为,内置类型是没有构造函数的,但是因为这种写法的存在所以也就肯定了内置类型也是有默认构造的。如果这里的 Tintval 的缺省值是 0;如果这里的 Tdoubleval 的缺省值是 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 下,inserterase 之后会导致 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 函数就是上面模拟实现中所展示的函数。

相关文章:

  • 线夹金具测温在线监测装置:电力设备安全运行的“隐形卫士”
  • 通过paramiko 远程在windows机器上启动conda环境并执行python脚本
  • 定制化5G专网服务,助力企业数字化转型
  • 谷歌浏览器油猴插件安装方法
  • 从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
  • 2025年Splunk的替代方案:更智能的安全选择
  • 实时数据湖架构设计:从批处理到流处理的企业数据战略升级
  • 用布局管理器grid实现计算机界面
  • 扫地机产品--材质传感器算法开发与虚拟示波器
  • [蓝桥杯]小计算器
  • 分布式互斥算法
  • sqli-labs靶场38-45关(堆叠注入)
  • Qt 中实现文本截断(ellipsis)的功能。Qt 提供了此方法来处理过长的文本显示问题,例如在界面中限制文本长度并添加省略号(...)
  • Flutter面试题
  • AI编程规范失控?三大策略用Cursor Rules精准约束
  • 边缘计算网关赋能沸石转轮运行故障智能诊断的配置实例
  • Redis常见使用场景解析
  • mysql 悲观锁和乐观锁(—悲观锁)
  • PLC远程控制网关支持多塘口水环境数据边缘计算与远程安全传输的配置指南
  • 对抗性提示:大型语言模型的安全性测试
  • 昆明靠谱的网站开发公司有哪些/网络广告的形式有哪些
  • 深圳营销培训班/丁的老头seo博客
  • 万网域名管理网站/宣传方式有哪些
  • 襄阳网站seo厂家/360推广登录平台
  • 网站建设公司价格表/网络营销是指什么
  • 建设中英文网站/徐州百度seo排名