【C++STL】模拟实现vector容器

简介: 【C++STL】模拟实现vector容器

前言

本文带你进入vector的模拟实现,对于vector,是我们深入学习STL的必要途径。


一、vector的成员函数

根据库的实现方式,成员函数如下:

iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;

c++11开始可以对成员变量使用缺省值,在这里我们可以使用缺省值。

二、增删查改工作

说明size()和capapcity()

size的大小为_finish - _start

capacity的大小为_end_of_storage - _start

2.1reserve()

该函数的作用是:扩容。

void reserve (size_type n);
  • 思路:
  • 1.如果要扩容的大小n小于capacity,则不会缩容。
  • 2.否则执行扩容。
  • 先申请一块n大小的空间
  • 拷贝原空间的数据到新空间,并释放原空间
  • 将新空间的数据交给_start管理。
//扩容
void reserve(size_t n)
{
  //防止有些情况是直接调用
  if (n > capacity())
  {
    size_t oldsize = size();//必须要记录旧的size,旧的size是用旧的_finish - _start获得的
    iterator tmp = new T[n];
    //memcpy(tmp, _start, sizeof(T) * size());
    //这里如果使用memcpy函数的话,如果vector对象是自定义类型,比如string,会造成浅拷贝问题。
    for (size_t i = 0; i < oldsize;i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
    {
      tmp[i] = _start[i];
    }
    delete[] _start;
    _start = tmp;
    //_finish = _start + size();
    //如果这样用的话,size()是用旧的_finish - 新的_start,不是连续的空间,没有意义
    _finish = _start + oldsize;//_finish必须更新,旧的_finish迭代器已经失效了
    _end_of_storage = _start + n;
  }
  //tmp不用处理,出了作用域自动调用析构
}

注意:在扩容时,如果是自定义类型,比如string,使用memcpy函数进行拷贝数据,会出现浅拷贝问题。

v1会拷贝给v2,v2的指针仍然指向v1的string对象所指向的空间。这就造成了两次调用析构函数释放同一块空间的问题。

正确的解法是:

在拷贝时调用string的赋值运算符重载,完成深拷贝。

还有一个点是:

注意:在这里会出现迭代器失效的问题。

此时空间已满,则需要重新申请空间。

重新申请空间后,_start和_end_of_stortage都会指向新的空间,唯独_finish还指向旧的已经被释放的空间,此时如果这样更新_finish位置:

_finish = _start + size();

这里的size()是用旧的_finish - 新的_start得到,是没有意义的。

解决方案:

保存旧的size,随后_start加上旧的size即为_finish的位置。

2.2 resize()

该函数的功能是:重新改变vector容器的大小,让其到达n。新的空间默认初始化为val

思路:

  • 1.如果n小于size,则让_finish指向_start + n位置。
  • 2.如果n大于等于size,先调用reserve函数进行扩容,再将新的空间填充为val。
void resize(size_t n, const T& val = T())
{
  if (n < size())
  {
    _finish = _start + n;
  }
  else
  {
    //扩容到n,如果n<capacity(),则啥事不做,否则,扩容到n
    reserve(n);
    //将多的空间全部设置成缺省值
    while (_finish != _start + n)
    {
      *_finish = val;
      ++_finish;
    }
  }
}

注意这里的缺省值为T(),是一个T类型的匿名对象。

不管T是什么,都可以使用模板实例化出具体的匿名对象,即使是内置类型,比如int,也可以实例化出int(),这里的值为0

2.3 insert()

在这里我们只实现最常用的接口。

该函数的作用是:

在pos位置插入模板对象val。

思路:

  • 插入元素前先检查容量
  • 如果容量满了,则调用reserve函数执行扩容操作
  • 然后从pos位置开始将其之后的元素全部都往后挪。
  • 再在pos位置插入val。
void insert(iterator pos, const T& val)
{
  assert(pos <= _finish);
  //插入前先检查扩容
  if (_finish == _end_of_storage)
  {
    size_t oldpos = pos - _start;//记录相对位置
    size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
    reserve(newcapacity);
    pos = _start + oldpos;
  }
  //记录旧的size,防止出现迭代器失效问题。
  //将pos之后的位置往后挪,再插入
  iterator end = _finish - 1; // reserve之后,_finish也跟着更新了。
  while (end >= pos)
  {
    *(end + 1) = *end;
    --end;
  }
  *pos = val;
  _finish += 1;
}

注意:insert在扩容时仍然会出现迭代器失效的问题。

解决方案:提前记录pos的相对位置,即oldpos = pos - _start;

然后再扩容完成后,更新pos 的位置,pos = _start + oldpos;

否则在扩容后,旧的pos迭代器就已经失效了。

2.4 erase()

该函数的功能是:删除pos位置的值。

//在删除最后一个位置或者只剩下一个待删除元素时,会出现迭代器失效问题。
iterator erase(iterator pos)
{
  assert(pos < _finish);
  iterator end = pos;
  while (end != _finish)
  {
    *end = *(end + 1);
    ++end;
  }
  --_finish;
  return pos;
}

有一个需要注意的点:当我们删除元素后,pos迭代器就会失效,因为空间已经释放,这时候不能使用pos迭代器。

解决方法:返回删除位置的下一个元素的位置,即pos位置。

然而,在只剩下一个待删除元素或者删除最后一个位置的元素时,不能再使用pos迭代器。

2.5 push_back()和pop_back()

顾名思义,这两个函数的功能是在最后一个位置插入元素和删除最后一个位置的操作。

直接复用insert和erase函数即可,这里不再赘述。

三、[]重载和迭代器

typedef T* iterator;
typedef const T* const_iterator;

3.1begin迭代器

返回_start地址即可。

iterator begin()
{
  return _start;
}
const_iterator begin() const
{
  return _start;
}

3.2 end迭代器

返回_finish地址即可。

iterator end()
{
  return _finish;
}
const_iterator end() const
{
  return _finish;
}

3.3 []运算符重载

思路:只需给定pos下标即可。
在此之前需要判断pos位置不能超过整个顺序表的size大小。

T& operator[]( size_t pos)
{
  assert(pos < size());
  return *(_start + pos);
}
//不可修改的
const T& operator[]( size_t pos) const
{
  assert(pos < size());
  return *(_start + pos);
}

四、默认成员函数

4.1构造函数

构造函数可分为3种:

  • 无参构造
  • 构造n个val对象
  • 根据迭代区间构造

对于无参的构造,我们只需要初始化成员函数即可。

  • 1.无参构造
//无参构造函数
vector()
//:_start(nullptr)
//,_finish(nullptr)
//,_end_of_storage(nullptr)
{}
  • 2.构造n个val对象
//构造n个对象
vector(size_t n, const T& val = T())
  :_start(nullptr)
  , _finish(nullptr)
  , _end_of_storage(nullptr)
{
  //1.开空间
  reserve(n);
  //2.将空间填充为val对象
  for (size_t i = 0; i < capacity(); i++)
  {
    _start[i] = val;
  }
  //也可以复用resize
  //resize(n, val);
}

注意:这里在构造时必须初始化,如果不初始化,在调用reserve函数开空间时会出现访问野指针的问题。

也可以调用resize函数进行拷贝构造n个对象。

  • 3.使用迭代区间进行构造

同理,如果不初始化,会出现扩容后访问野指针的情况。

template<class Inputiterator>
vector(Inputiterator first, Inputiterator last)
{
  while (first != last)
  {
    push_back(*first);
    ++first;
  }
}

4.2 拷贝构造函数

  • 1.拷贝构造传统写法
//拷贝构造,必须实现深拷贝
//传统写法
//v1(v)
vector(const vector<T>& v)
  //这里必须初始化,否则扩容时会访问不属于自己的空间
  :_start(nullptr)
  ,_finish(nullptr)
  ,_end_of_storage(nullptr)
  //如果私有成员给了缺省值,就不用再再次初始化了
{
  reserve(v.capacity());
  //memcpy(_start, v._start, sizeof(T) * v.size());
  //这里使用memcpy的错误跟扩容的错误一样,不再赘述
  for (size_t i = 0; i < v.size(); i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
  {
    _start[i] = v._start[i];
  }
  _finish = _start + v.size();
}

拷贝构造需要注意的几个问题:

  • 1.拷贝构造同构造函数一样,如果不初始化,在开空间时会出现访问野指针的情况。
  • 2.拷贝构造必须进行深拷贝,否则如果vector的对象是自定义类型,比如string,会出现两次释放同一块空间的问题。

4.3 析构函数

释放_start指向的空间,然后全部置为空即可。

~vector()
{
  if (_start)
  {
    delete[] _start;
    _start = _finish = _end_of_storage;
  }
}

总结

vector的模拟实现就到此结束了,如果对你有帮助的话,不妨来一个关注吧!

相关文章
|
7月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
176 2
|
7月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
344 73
|
8月前
|
存储 缓存 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 的奥秘,从入门到高效编程
|
8月前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
7月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
287 3
|
8月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
382 1
|
9月前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
188 21
|
8月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
10月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
202 1
|
3月前
|
存储 监控 测试技术
如何将现有的应用程序迁移到Docker容器中?
如何将现有的应用程序迁移到Docker容器中?
279 57