从C语言到C++_15(vector的模拟实现)+迭代器失效问题(上)

简介: 从C语言到C++_15(vector的模拟实现)+迭代器失效问题

1. vector的基本框架

STL的源代码整体考虑的东西比较多,还要考虑和其他地方的结合,因此整体的设计是比较复杂的。基于这一系列原因,我们会以简单的形式去实现其核心框架接口,方便去学习 vector。我们可以先看一看STL源代码的整体框架,一些要实现的接口函数不会实现的时候才去看看细节。现在自己看源码还不太好,且看不懂,跟着这篇博客看就挺好的(自夸+1)

以下是基于《STL源码剖析》用到的vector的部分源码:

其中最重要的就是三个私有成员 start 、finish 和 end_of_storage。

想想模拟代码的实现,为了和库中的 vector 进行区分,这里依然用命名空间包含起来。

造一个 vector 的类模板去适应各种类型,用 typedef 将 T* 重命名为 iterator。

1.1 构造析构和容量

经过前面string的学习和上面的源码,直接放vector.h :

#pragma once
 
#include<iostream>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  template<class T>
  class vector
  {
  public:
    typedef T* iterator;
 
    vector()
      :_start(nullptr)
      , _finish(nullptr)
      , _end_of_storage(nullptr)
    {}
    ~vector()
    {
      delete[] _start;
      _start = _finish = _end_of_storage = nullptr;
    }
 
    size_t size() const
    {
      return _finish - _start;
    }
    size_t capacity() const
    {
      return _end_of_storage - _start;
    }
 
  private:
    iterator _start;
    iterator _fnish;
    iterator _end_of_storage;
  };
}

1.2 push_back,reserve和operator[ ]

我们这里先实现一个简单的 push_back,以便于我们能先把 vector 跑起来。我们的 push_back 没有空间配置器,我们就用 new 去替代它。

尾插需要做哪些事?

1:检查是否需要增容

需要增容,就先增容后再插入数据;不需要增容,就直接插入数据。

我们先去思考,如何判断是否需要增容 ——

我们之前的判断方式是 size == capacity 的时候需要增容问题是,这次我们没有定义 _size 和 _capacity,取而代之的是 _start 、 _finish 和_end_of_storage的形式。


想想: 当 _finish  ==  _end_of_storage时,不就说明容量不够了吗?

2:如果需要增容

前面已经知道,vector是有reserve接口的,增容我们先实现reserve:

如果要增容:

① 开一块带有新容量的空间存到 tmp 中。

② 再把原空间的数据拷贝到新空间。(还能用memcpy吗,我们先用着)

③ 并释放原有的旧空间

④ 最后将 _start 、_finish  和 _end_of_storage指向新的空间。

值得注意的是,最后一步如果先将 _start 指向 tmp 后,再计算 _finish 时,


此时不能现场算 size() ,现场算会出问题,因为 _start 已经被更新成 tmp 了,


如果不想改变顺序,还是想按 _start、_finish 和 _eos 的顺序赋值,


我们可以提前把 size() 算好,存到一个变量中。


此外,真 vector 这里扩容是要调空间配置器的,开空间和初始化是分开的。


我们这里的实现也没有空间配置器,对于空间配置器的知识我们放到后面再说,


我们目前的重点不是空间配置器,重点是 vector。


至于新容量给多少,我们还是按照自己的习惯,首次给4默认扩2倍的方式去增容。


reserve代码:

    void reserve(size_t n)
    {
      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 = tmp + sz;
        _end_of_storage = tmp + n;
      }
    }

3:插入数据

查增容和增容都已经分析完了,最后就只剩一个插入了,插入是最简单的,

因为_finish指向的是最后一个元素的下一个位置,我们直接让要插入的元素赋值给_finish,

然后++_finish就行了。push_back代码:

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

为了方便测试尾插的效果,我们先来实现一下 operator[] ,利用 "下标+方括号" 的方式遍历。

这有两个接口,我们一起实现了:

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

T:由于我们不知道返回值类型,所以给 T。

T&:引用返回减少拷贝。

const是给const对象用的:这里 cosnt 修饰 T 和 this,是为了限制写。

测试一下:(vector.h:)

#pragma once
 
#include<iostream>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  template<class T>
  class vector
  {
  public:
    typedef T* iterator;
 
    vector()
      :_start(nullptr)
      , _finish(nullptr)
      , _end_of_storage(nullptr)
    {}
    ~vector()
    {
      delete[] _start;
      _start = _finish = _end_of_storage = nullptr;
    }
 
    size_t size() const
    {
      return _finish - _start;
    }
    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);
      }
      *_finish = x;
      ++_finish;
    }
    void reserve(size_t n)
    {
      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 = tmp + sz;
        _end_of_storage = tmp + n;
      }
    }
 
    T& operator[](size_t pos)
    {
      assert(pos < size());
      return *(_start + pos);
    }
    const T& operator[](size_t pos) const
    {
      assert(pos < size());
      return *(_start + pos);
    }
 
  private:
    iterator _start;
    iterator _finish;
    iterator _end_of_storage;
  };
}

Test.c:

#include "vector.h"
 
namespace rtx
{
  void Test1()
  {
    vector<int> v;
    cout << v.size() << " " << v.capacity() << endl;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    cout << v.size() << " " << v.capacity() << endl;
    v.push_back(5);
    cout << v.size() << " " << v.capacity() << endl;
 
    for (size_t i = 0; i < v.size();++i)
    {
      cout << v[i] << " ";
 
    }
    cout << endl;
  }
}
 
int main()
{
  rtx::Test1();
 
  return 0;
}

2. vector的迭代器

2.1 四个基本迭代器

vector 的迭代器是一个原生指针:

template<class T>
class vector 
{
public:
  typedef T* iterator;
  typedef const T* const_iterator;

前面源码看到,begin() 和 end() ,直接分别返回 _start _finish 即可,

const 类型的迭代器,即可读不可写。在实现的时候用 const 修饰即可:

测试迭代器的效果:(范围for也能用了)

  void Test2()
  {
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
 
    for (size_t i = 0; i < v.size();++i)
    {
      ++v[i];
      cout << v[i] << " ";
    }
    cout << endl;
 
    vector<int>::iterator it = v.begin();
    while (it != v.end())
    {
      --(*it);
      cout << *it << " ";
      ++it;
    }
    cout << endl;
 
    for (const auto& e : v)
    {
      cout << e << " ";
    }
    cout << endl;
  }

2.2 迭代器区间初始化

上一篇说到vector是支持迭代器区间初始化的:

拷贝构造放在后面再讲,先实现迭代器区间初始化:

因为传过来的迭代器可以是任意的,所以这又是一个模板:

(一个类模板的成员函数,又可以是一个函数模板):

  void Test2()
  {
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
 
    for (size_t i = 0; i < v.size();++i)
    {
      ++v[i];
      cout << v[i] << " ";
    }
    cout << endl;
 
    vector<int>::iterator it = v.begin();
    while (it != v.end())
    {
      --(*it);
      cout << *it << " ";
      ++it;
    }
    cout << endl;
 
    for (const auto& e : v)
    {
      cout << e << " ";
    }
    cout << endl;
 
    string s("hello world");
    vector<int> v1(s.begin(), s.end());// 存了ASCII码
    for (const auto& e : v1)
    {
      cout << e << " ";
    }
    cout << endl;
  }

为什么这里要叫 InputIterator?不用它行不行?

想要知道这个问题,我们先讲解一下迭代器的分类。

2.3 迭代器的分类

迭代器可以分为这么几类:

① 输入 / 输出迭代器:input_iterator  /  output_iterator

特点:单步向前迭代,不可写 / 单步向前迭代,可写,无对应类型

② 单向迭代器:forward_iterator

特点:满足以上所有功能,并且能 ++

(不能 - -没有 rbegin / rend)

        (C++11)

 (C++11)

   (C++11)


③ 双向迭代器:bidirectional_iterator

特点:满足以上功能,并且能 ++,还能 - -



④ 随机迭代器:randomaccess_iterator

特点:满足以上所有功能,能 ++ 能 - -,还能 + 和 -  


它们本质上是一个继承关系:下面是子类,上面是父类。


子类都是一个父类,因为它满足父类的所有特征。


也就是说,虽然在语法上它是个模板,允许你传任意类型的迭代器,


但是在更深层次上存在着更进一步的限制 ——

它要求你传随机迭代器,你就不能用双向迭代器。因为只有随机迭代器才能满足随机迭代器的所有操作。换言之,你不能用功能比它指定的迭代器少的迭代器。(可以理解为权限的放大)

它要求你用双向迭代器,你就不能用单向迭代器,因为单项迭代器不能满足所有双向迭代器的操作。但是你可以用比它功能多的迭代器,比如随机迭代器,因为随机迭代器也能满足双向迭代器的操作。因为随机迭代器是双向迭代器的子类,它满足父类(双向迭代器)的所有功能。(可以理解为权限的缩小)


我们弄明白了这些,我们再回到刚才提的问题 —为什么这里要叫 InputIterator?


首先,InputIterator 是输入迭代器,这么写是为了满足命名规范。


可以不用,我们可以传单向迭代器、双向迭代器,也可以传随机迭代器。


因为这些迭代器都满足输入迭代器的所有功能。

从C语言到C++_15(vector的模拟实现)+迭代器失效问题(中):https://developer.aliyun.com/article/1521294

目录
相关文章
|
5月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
202 26
|
10月前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
699 4
|
12月前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
|
12月前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
277 0
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
445 0
|
存储 算法 C语言
从C语言到C++_39(C++笔试面试题)next_permutation刷力扣
从C语言到C++_39(C++笔试面试题)next_permutation刷力扣
246 5
|
存储 编译器 C语言
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(下)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
250 1
|
存储 编译器 Linux
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(中)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
181 1
|
编译器 C语言 C++
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(上)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
213 0