【C++】STL之vector类模拟-2

简介: 【C++】STL之vector类模拟

3)元素访问

  • 对于元素访问的话我们最常用的就是下标 + []的形式,这里给出两种,一个是const版本非const版本


T& operator[](size_t pos)
{
  assert(pos < size());
  return _start[pos];
}
T& operator[](size_t pos) const
{
  assert(pos < size());
  return _start[pos];
}

4)修改操作

接下去的话我们来讲讲有关修改操作的一些接口

  • 首先第一个的话就是push_back,这个我在上面讲【reserve】的时候给出过,现在仔细地再来讲一讲:首先的话我们要考虑的就是扩容的逻辑,上面我们有讲到在VS下是呈现 1.5倍 的增长趋势,但是在g++下呈现的则是 2倍 的扩容逻辑,这里的扩容的话我们就交给【reserve】来实现


void push_back(const T& x)
{
  if (_finish == _end_of_storage)
  {
    size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
  }
  *_finish = x;
  _finish++;
}

然后的话我们来实现一下【insert】这个接口


void insert(iterator pos, const T& x)

这一块的话我们已经讲过很多遍了,要在某一个位置插入数据的话就需要先去挪动部分的数据,这里我们从后往前挪,防止造成覆盖的情况,当数据挪动完毕后,再在pos这个位置插入指定的数据即可

image.png

  • 在一进入函数的时候大家可以去做一个断言的操作,不过很多同学可能会好奇这边的pos >= _start,为什么可以位于首部


assert(pos >= _start && pos <= _finish);

💬 在讲解 string类 的时候我们确实讲到了这种写法的缺陷,但是读者要看清楚了,这里pos的类型是 iterator,为一个迭代器。而我们在 string类  中所讲到的这个pos呢是一个无符号整数

  • 位于首部的迭代器pos不可能是0,因为它是一段空间的地址,有效空间的地址不可能是0,


string& insert (size_t pos, const string& str);
  • 不过呢,既然是插入数据的话就一定会存在容量不足的情况,此时就需要一个扩容逻辑,这里我们直接用上面在push_back()接口中所写的即可


// 1.首先考虑扩容逻辑
if (_finish == _end_of_storage)
{
  size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
  reserve(newCapacity);
}

以下是整体的代码


void insert(iterator pos, const T& x)
{
  assert(pos >= _start && pos <= _finish);
  // 1.首先考虑扩容逻辑
  if (_finish == _end_of_storage)
  {
    size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
  }
  // 2.挪动数据
  iterator end = _finish - 1;
  while (end >= pos)
  {
    *(end + 1) = *end;
    --end;
  }
  *pos = x;
  ++_finish;
}
  • 那么对于push_back()这个接口我们就可以去复用一下【insert】这个接口了


void push_back(const T& x)
{
  /*if (_finish == _end_of_storage)
  {
    size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
  }
  *_finish = x;
  _finish++;*/
  insert(end(), x);
}

💻第三轮测试 — 迭代器失效问题

  • 好,在写完【insert】接口后,我们再来做一个测试。可以发现程序崩溃了

image.png

马上,我们通过调试来观察一下

  • 此时我们已经往【v】中插入了4个数据,马上使用insert(v.begin(), 100)去做一个头插,那么一进到函数中我们就可以知道这个当前对象的_startpos所处的迭代器位置是相同的,也就是同一段空间的地址

image.png

  • 那此时我们知道容器中的空间已经满了,所以会去走一个扩容的逻辑,此时可以看到当前对象this的_start已经发生了改变

image.png

  • 可以看到,在扩完容之后,当前对象的_start和待插入位置的pos已经发生了变化,那么在此时我们再去挪动数据进行插入的时候就会出现问题了

image.png💬 我们可以通过下面的图示来看看到底这个扩完容之后是怎样的

  • 可以看到_start确实发生了一个变化,但是呢pos还是指向原来的那个地方。那读者可以自己去想象一下子在遍历挪动数据的时候究竟何时才是个头呢?

image.png

  • 我们可以通过调试再来观察一下挪动数据这段逻辑,可以看到在挪动完几次数据后就出现了随机值,并且出现了死循环的问题

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9faf5f27479747aba8c13e6efd75a752~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1434&h=386&s=443201&e=gif&f=285&b=cfe9d5🔰 ==以上所出现的这个问题就被称作是 【迭代器失效的问题】==

那我们要如何去解决呢?

💬 有同学说,内部外部无法一起修改的话参数部分加个引用不就行了


void insert(iterator& pos, const T& x)
  • 这其实是不对的,因为有些时候我们所传递的迭代器位置是v.begin() + 3,在这中间会去产生一个临时对象,我们知道临时对象是具有常性的,那么传递进去的时候就会造成【权限放大】的问题


v.insert(v.begin() + 3, 6);

image.png💬 那有同学又说,那防止一下权限放大不就好了,加个const

  • 这肯定是不可以的,看到程序依旧会出现问题

image.png


  • 首先大家要明白的一个点是出错的根本原因在于:_start的位置改变了但是pos的位置没有发生改变
  • 所以我们所要做的一个点就是:pos的位置随着_start的变动而一起变动,这样就不会出现问题了。以下我们需要改进的代码部分,在进行扩容之前,我们可以先去计算一下从【_start】到【pos】的位置有多远;
  • 然后呢我们在执行完扩容的逻辑之后,就要去更新一下这个【pos】迭代器的位置所在,就使用刚才计算出来的这段距离即可


// 1.首先考虑扩容逻辑
if (_finish == _end_of_storage)
{
  // 首先保存一下从_start到pos的距离
  size_t len = pos - _start;
  size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
  reserve(newCapacity);
  // 再扩完容之后更新一下pos, 解决迭代器失效问题
  pos = _start + len;
}

那代码做了更新之后迭代器失效的问题真的解决了呢,我们通过调试一起来看看

  • 可以看到,通过我们的骚操作😛【pos】位置就随着【start】的变化而随着变化


  • 但是呢就上面这样还不够,我们只解决了内部迭代器失效的问题,而外部迭代器失效的问题并没有很好地解决。
  • 外部迭代器,那是什么东西? 我们来看下这段代码


bit::vector<int>::iterator it = v.begin();
v.insert(it, 33);
bit::print(v);
cout << *it << endl;
bit::print(v);

可以看到,在使用完这个这个迭代器之后再去访问就出现了问题

image.png如果直接其换成库里面的【vector】的话,就直接崩溃了

image.png👉 所以,对于迭代器这一块我们在使用的时候一定要慎重,在使用完之后不要去轻易地修改它


it = v.insert(it, 33);
  • 如何执意要进行修改的话也不是没有办法,我们只需要在【insert】之后去接受一下当前所操作的这个迭代器的位置即可,记住这个位置,下次在访问的时候也就不会出问题

image.png具体代码如下:


iterator insert(iterator pos, const T& x)
{
  assert(pos >= _start && pos <= _finish);
  // 1.首先考虑扩容逻辑
  if (_finish == _end_of_storage)
  {
    // 首先保存一下从_start到pos的距离
    size_t len = pos - _start;
    size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
    // 再扩完容之后更新一下pos, 解决迭代器失效问题
    pos = _start + len;
  }
  // 2.挪动数据
  iterator end = _finish - 1;
  while (end >= pos)
  {
    *(end + 1) = *end;
    --end;
  }
  *pos = x;
  ++_finish;
  return pos;
}
相关文章
|
1天前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
1天前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
1天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
1天前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
4天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
4天前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
70 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5