分块刨析从函数原型到分块实现C++STL(vector)

简介: 分块刨析从函数原型到分块实现C++STL(vector)

一, 函数原型部分分析(简单举例使用便于理解)

  • 简单测试使用:
int main() {
  std::vector<int> vint;
  vint.reserve(3);
  cout << vint.capacity() << endl;
  vint.resize(10, 5);
  cout << vint.size() << endl;
  for (auto& e : vint) {
    cout << e << " ";
  }
  cout << endl;
  return 0;
}

如上可以证明的就是resize改变的size ,reserve改变的是capacity是预先开出空间,至于使用reserve的使用可以减少扩容的次数.. 提高效率..

迭代器函数, 返回迭代器. 支持迭代器, 有了迭代器才能支持范围的遍历方式. 像是上述使用的for (aoto& e : vint) 的遍历方式都是因为存在迭代器作为支撑才可以的


为了验证, 简单的使用迭代器模板类模拟实现一下  for_each


引入一下小小的知识点,迭代器类型   +   仿函数类型   作为类型模板是一种很常见的使用方式.    这种方式常用在对于一段区间的元素做函数处理的场景下:  (很重要的trait)

template<class InputIterator, class Function>
  void Func(InputIterator first, InputIterator last, Function f) {
    while (first != last) {
      f(*first);
      ++first;
    }
    return;
  }

eg :      

template<class T >
  struct Print {
    void operator()(T& val)const {
      cout << val << " ";
    }
  };
  template<class InputIterator, class Function>
  void for_each(InputIterator first, InputIterator last, Function f) {
    while (first != last) {
      f(*first);
      ++first;
    }
    return;
  }
}
int main() {
  std::vector<int> vint;
  vint.reserve(3);
  cout << vint.capacity() << endl;
  vint.resize(10, 5);
  cout << vint.size() << endl;
  //for (auto& e : vint) {
  //  cout << e << " ";
  //}
  tyj::for_each(vint.begin(), vint.end(), tyj::Print<int>());
  cout << endl;
  return 0;
}
  • 所以可以证明的是for(aoto& e : 容器)的这种遍历方式应该底层也是基于迭代器实现的

二、 迭代器设计思想,vector迭代器分析

因为  vector 的底层就是一个动态数组.维护的是一个线性空间,  所以普通的原生指针可以直接使用做迭代器, 因为本身指针就是支持operator++  operator-- operator*这些运算的

    typedef T* iterator;
    typedef const T* const_iterator;

三,整体的大体可以快速看见效果的框架

对于上述先解释一下,很多时候我们要写一个很大的东西, 为了降低deBug的成本, 其实可以先实现必要性的可以看见效果的区块, 大体的框架性写好, 效果看到了然后


插入元素时候的扩容机制: 如果是需要扩容,采取的一般是二倍扩容的方式进行扩容的,扩容分为三部,新开空间,转移数据,销毁原有空间...      (记住转移数据调用深拷贝,不可以memcpy) why?

namespace tyj {
  template <class T>
  class vector {
    typedef T* iterator;
    typedef const T* const_iterator;
  public:
    vector() //默认构造
      : start(nullptr)
      , finish(nullptr)
      , end_of_storage(nullptr) {
    }
    vector(int n, const T& val = T()) //带参构造
      : start(new T[n]) {
      finish = start + n;
      end_of_storage = finish;
      for (int i = 0; i < n; ++i) {
        start[i] = val;
      }
    }
    iterator begin() {
      return start;
    }
    const_iterator begin() const {
      return start;
    }
    iterator end() {
      return finish;
    }
    const_iterator end() const {
      return finish - 1;
    }
    size_t size() const {
      return finish - start;
    }
    size_t capacity() const {
      return end_of_storage - start;
    }
    void reserve(int n) {//预先开空间
      if (n <= capacity()) return;
      //说明需要新开空间, 三步
      //1.新开空间
      T* pTemp = new T[n];
      //2.转移数据
      int _size = size();
      for (int i = 0; i < _size; ++i) {
        pTemp[i] = start[i];
      }
      //3.销毁原有空间
      delete[] start;
      start = pTemp;
      finish = start + _size;
      end_of_storage = start + n;
    }
    void push_back(const T& val) {
      if (size() == capacity()) {
        reserve(size() == 0 ? 8 : (size() << 1));
      }
      //放入数据
      *finish = val;
      finish++; //尾部迭代器后移
    }
  private:
    //[start, finish)
    iterator start;//当前使用空间起点
    iterator finish;//当前使用空间末尾
    iterator end_of_storage; //空间末尾
  };
  template <class T >
  struct Print {
    void operator()(T& val) const {
      cout << val << " ";
    }
  };
  template<class InputIterator, class Function>
  void for_each(InputIterator first, InputIterator last, Function f) {
    while (first != last) {
      f(*first);
      ++first;
    }
  }
}
int main() {
  tyj::vector<int> vint;
  for (int i = 0; i < 5; ++i)
    vint.push_back(i);
  cout << vint.size() << endl;
  tyj::for_each(vint.begin(), vint.end(), tyj::Print<int>());
  cout << endl;
  return 0;
}

三,重点函数分块分析图解实现, 仅仅只是写哪些可能大家不清楚的,都可以写的部分写了也没用

  • 然后就是一个一个函数的分析应该如何设计的问题

各种构造函数的设计思想,如何只写一个然后实现代码复用

  • 区间范围range构造

  • swap为复用代码做准备

  • 赋值重载

同样的道理,已经复用了拷贝构造函数构造出来了  vec 对象直接  窃取  vec 换成自己的,马匪看过吗,让我成为你, 你成为空气

  • 移动构造   (更加强盗,不齿,直接抢将死之人的身份)

insert  +  erase 的设计,以及分析返回值,加之分析迭代器失效会出现的位置,和抛出设计中的细节之处

  • insert

  • erase

  • 再看   insert   +  erase   都存在一个  iterator作为返回值,为啥,跟新旧的迭代器,防止迭代器失效。。。   向如下的代码为啥要  接收一下  erase 之后的返回值也就可以理解了,防止使用失效的迭代器
void test() {
  std::vector<int> vec(5, 1);
  for (auto it = vec.begin(); it != vec.end(); ++it) {
    it = vec.erase(it);
  }
}
it = v.begin();
PrintVector(v);
while (it != v.end()) {
  if (*it % 2 == 0)
    it = v.erase(it);
  else
    ++it;
}

  • 至此比较有用的部分都已经分析完成了。

四. 整体代码  (实现代码)

#include <vector> 
#include <algorithm>
#include <iostream>
#include <assert.h>
using namespace std;
namespace tyj {
  template <class T >
  class vector {
  public:
    //原生指针作为迭代器
    typedef T* iterator;
    typedef const T* const_iterator;
    //无参构造函数
    vector()
      : start(nullptr)
      , finish(nullptr)
      , end_of_storage(nullptr) {
    }
    vector(int n, const T& val = T()) 
      : start(new T[n]), 
      finish(start + n), 
      end_of_storage(start + n) {
      //放入数据
      for (int i = 0; i < n; ++i) {
        start[i] = val;
      }
    }
    //写范围构造函数
    template<class InputIterator>
    vector(InputIterator first, InputIterator last) {
      //利用范围进行构造
      while (first != last) {
        push_back(*first++);//直接进行复用接口
      }
    }
    //接下来写拷贝构造, 赋值运算符重载,移动构造,全部进行复用接口
    void swap(vector<T>& vec) {
      ::swap(start, vec.start);
      ::swap(finish, vec.finish);
      ::swap(end_of_storage, vec.end_of_storage);
    }
    vector(const vector<T>& vec) 
      : start(nullptr)
      , finish(nullptr)
      , end_of_storage(nullptr) {
      vector<T> tmp(vec.begin(), vec.end());
      swap(tmp);//复用范围构造代码
    }
    vector<T>& operator=(vector<T> vec) {
      swap(vec);//复用拷贝构造代码
      return *this;
    }
    //移动构造, 窃取资源,窃取将亡对象的底层堆区资源
    //反正你都要死亡了不如用来给我构造
    vector(vector<T>&& vec) 
      : start(nullptr)
      , finish(nullptr)
      , end_of_storage(nullptr) {
      swap(vec);
    }
    iterator begin() {
      return start;
    }
    const_iterator begin() const {
      return start;
    }
    iterator end() {
      return finish;
    }
    const_iterator end() const {
      return finish;
    }
    void resize(size_t n, const T& val = T()) {
      if (n <= size()) return;  //啥事不用做
      //至此说明了超出的部分需要设置为val
      if (n > capacity()) reserve(n);//先扩容
      //然后赋值
      while (finish != start + n) {
        *finish++ = val;
      }
    }
    void reserve(int n) {
      //预先开空间,三部曲目
      if (n <= capacity()) return;
      //至此说明需要重新开空间
      T* pTemp = new T[n];
      size_t sz = size();//保存之前容器中的元素大小
      //转移数据
      for (int i = 0; i < sz; ++i) {
        pTemp[i] = start[i];
      }
      delete [] start;//删除掉原有的空间
      start = pTemp;
      finish = pTemp + sz;
      end_of_storage = start + n;
    }
    void push_back(const T& val) {
      if (finish == end_of_storage) {
        reserve(capacity() > 0 ? capacity() << 1 : 4);
      }
      //有了空间,然后就是进行数据插入
      *finish++ = val;//放入数据
    }
    bool empty() {
      return start == nullptr;
    }
    size_t size() {
      return finish - start;
    }
    size_t capacity() {
      return end_of_storage - start;
    }
    //网pos位置插入一个元素,插入一定谨防的是插入扩容的原有迭代器失效问题
    iterator insert(iterator pos, const T& val) {
      assert(pos >= start && pos <= finish);//断言位置合法
      if (finish == end_of_storage) {
        size_t step = pos - start;
        //扩容首先
        reserve(capacity() > 0 ? capacity() << 1 : 4);
        //扩容之后原有的pos迭代器会失效跟新
        pos = start + step;
      }
      //拿到finish迭代器同时finish迭代器后移动
      iterator it = finish++;
      //腾出空间
      while (it != pos) {
        *it = *(it - 1);
        --it;
      }
      *pos = val;//放入插入数据
      return pos;//返回插入位置元素
    }
    //删除一定注意的是删除的位置其实还是pos 因为返回下一个元素其实是覆盖到pos位置了
    iterator erase(iterator pos) {
      //覆盖删除整个元素
      assert(pos >= start && pos < finish);
      iterator it = pos + 1;
      while (it <= finish) {
        *(it - 1) = *it;//覆盖删除
        ++it;
      }
      --finish;//尾部迭代器前移动
      return pos;
    }
    ~vector() {
      if (start) {
        delete[] start;
      }
      start = finish = end_of_storage = nullptr;
    }
  private:
    T* start;
    T* finish;
    T* end_of_storage;
  };
  template<class InputIterator, class Function>
  void for_each(InputIterator first, InputIterator last, Function f) {
    while (first != last) {
      f(*first);
      ++first;
    }
  }
  template<class T>
  struct Print {
    void operator()(T& val) const {
      cout << val << " ";
    }
  };
}
template<class T>
void PrintVector(tyj::vector<T>& vec) {
  tyj::for_each(vec.begin(), vec.end(), tyj::Print<T>());
  cout << endl;
}

测试代码

void TestVector3()
{
  int a[] = { 1, 2, 3, 4 };
  tyj::vector<int> v(a, a + sizeof(a) / sizeof(a[0]));
  // 使用find查找3所在位置的iterator
  auto pos = find(v.begin(), v.end(), 3);
  //pos迭代器可以失效,  插入操作迭代器也会失效
  // 在pos位置之前插入30
  v.insert(pos, 30);
  PrintVector(v);
  // 删除pos位置的数据
  pos = find(v.begin(), v.end(), 3);
  v.erase(pos);
  PrintVector(v);
}
void TestVector1()
{
  // constructors used in the same order as described above:
  tyj::vector<int> first; // empty vector of 
  tyj::vector<int> second(4, 100); // four ints with value 
  tyj::vector<int> third(second.begin(), second.end()); // iterating through 
  tyj::vector<int> fourth(third); // a copy of third
  // the iterator constructor can also be used to construct from arrays:
  int myints[] = { 16, 2, 77, 29 };
  tyj::vector<int> fifth(myints, myints + sizeof(myints) / sizeof(int));
  std::cout << "The contents of fifth are:";
  for (tyj::vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
    std::cout << *it << " ";
  std::cout << endl;
  // 测试T是string时,拷贝问题
  tyj::vector<string> strV;
  strV.push_back("1111");
  strV.push_back("2222");
  strV.push_back("3333");
  strV.push_back("4444");
}
void TestVector2()
{
  // 使用push_back插入4个数据
  tyj::vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  PrintVector(v);
  // 使用迭代器进行修改
  auto it = v.begin();
  while (it != v.end())
  {
    *it *= 2;
    ++it;
  }
  PrintVector(v);
  // 这里可以看出C++11支持iterator及接口,就支持范围for
  for (auto e : v)
    cout << e << " ";
}
// iterator失效问题
void TestVector4()
{
  int a[] = { 1, 2, 3, 4 };
  tyj::vector<int> v(a, a + sizeof(a) / sizeof(a[0]));
  // 删除pos位置的数据,导致pos迭代器失效
  auto pos = find(v.begin(), v.end(), 3);
  v.erase(pos);
  cout << *pos << endl; // 此处会导致非法访问
  // 在pos位置插入数据,导致pos迭代器失效。
  // insert会导致迭代器失效,是因为insert可
  // 能会导致增容,增容后pos还指向原来的空间,而原来的空间已经释放了。
  pos = find(v.begin(), v.end(), 3);
  v.insert(pos, 30);
  cout << *pos << endl; // 此处会导致非法访问
  // 实现删除v中的所有偶数
  // 下面的程序会崩溃掉,如果是偶数,erase导致it失效
// 对失效的迭代器进行++it,会导致程序崩溃
  auto it = v.begin();
  //while (it != v.end())
  //{
  //  if (*it % 2 == 0)
  //    v.erase(it);
  //  ++it;//每一次erase之后进行跳跃
  //}
  // 以上程序要改成下面这样,erase会返回删除位置的下一个位置
  it = v.begin();
  PrintVector(v);
  while (it != v.end())
  {
    if (*it % 2 == 0)
      it = v.erase(it);
    else
      ++it;
  }
}
int main() {
  //tyj::vector<int> vec1;
  //for (int i = 0; i < 5; ++i) {
  //  vec1.push_back(i);
  //}
  //PrintVector<int>(vec1);
  //tyj::vector<int> vec2;
  //vec2.resize(5, 1);
  //PrintVector<int>(vec2);
  //int nums[5] = { 1, 2, 3, 4, 5 };
  //tyj::vector<int> vec3(nums, nums + sizeof(nums) / sizeof(int));
  //PrintVector<int>(vec3);
  //tyj::vector<int> vec4(vec3);
  //PrintVector<int>(vec4);
  //TestVector3();
  //TestVector2();
  //TestVector4();
  TestVector1();
  return 0;
}

五. 整合小结一下,以及特别谈一谈如何写一个STL容器和迭代器设计思想(迭代器是什么),迭代器失效的问题.

如何实现一个STL书写的方法论


首先分析如果要写一个STL容器应该如何下手的问题,找到一个官方文档分析组件 + 接口


推荐官方文档网址:   www.cplusplus.com  


写一个STL 需要将迭代器设计和整体的设计分开来,先实现迭代器的设计 + 实现最简单的插入接口 (完成框架),测试插入接口,一来可以获得一定成就,而来可以知道自己基本框架是否搭建正确


阅读文档分析重点接口函数,可以买一本STL源码刨析,结合侯捷老师的讲述去设计接口,   将有用的接口一一实现.   (在实现的过程中不断测试以及发现细节坑点)


总结实现细节中出现的坑点   +  快速复现重写整个框架  (记忆熟练)


其实谈迭代器的设计在此处不算很合适,因为T* 就是一个原生指针,而且还是连续的线性存储空间,原生指针作为迭代器,本身迭代器就支持  ++  --  *  这些操作,加之元素本身就是连续存储的,  所以此处没有对迭代器的设计画太多功夫。


迭代器,算是一个智能指针类的感觉,但是也不完全是,可以帮助我们管理访问容器中的元素,支持 ++  -- *  这些操作来访问复杂存储结构中的  元素。


迭代器的设计是为了统一,是为了做粘合剂,做通用各式各样容器和算法之间的粘合剂,  有了迭代器,我们才能做各式各样容器的统一泛型算法操作,才能统一各种格式各样的容器元素的遍历方式,提供统一遍历接口.   是STL中特别重要的组件

上述这些算法,没有迭代器根本不可能这样写


迭代器失效,说白了其实本质是内存问题,扩容后,原来的迭代器全部不可以用了,因为原来的迭代器指向原来的那片空间,可是 扩容后原来那片空间都释放掉了,你如何敢访问,所以每一次关于迭代器的操作我们需要接收返回值就是这个道理,跟新旧的迭代器,避免使用失效迭代器,     it = e.erase(it)            不能是    e.erase(it++); ; 因为删除之后你不清楚,it   的情况如何,但是返回迭代器是指向下一个元素的有效迭代器,所以需要接收返回值.              删除it 之后 it 本质是一个失效迭代器了,如何赶直接  ++  因为他的本质在接收返回值之前还是哪个即将别删除的迭代器


所以 删除,插入扩容,都可能导致迭代器失效,插入失效是因为扩容,删除失效是因为它已经别干掉了,很可能是delete  或者其他操作


相关文章
|
6月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
342 2
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
687 73
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
249 0
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
存储 缓存 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 的奥秘,从入门到高效编程
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
549 3
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
798 6
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
387 21
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
864 1